Multi-Repo Workflow Tutorial

Build a user authentication feature across separate frontend and backend repositories using shared specifications and independent implementation plans.

What You’ll Build

A complete JWT-based authentication system with:

  • Shared specification: Requirements and API contracts
  • Backend implementation: Node.js/Express API with token generation
  • Frontend implementation: React login form with token storage

Prerequisites

  • Speck plugin installed in Claude Code
  • Two git repositories (or create new ones for this tutorial)
  • Basic familiarity with React and Node.js

Setup Your Workspace

Create the workspace structure:

# Create workspace directory
mkdir auth-tutorial
cd auth-tutorial

# Create three directories
mkdir shared-specs
mkdir frontend
mkdir backend

# Initialize each as git repo
cd shared-specs && git init && cd ..
cd frontend && git init && cd ..
cd backend && git init && cd ..

Your workspace now looks like:

auth-tutorial/
├── shared-specs/    (root)
├── frontend/        (child)
└── backend/         (child)

Step 1: Create Shared Specification

From the shared-specs directory, create the feature specification:

cd shared-specs

In Claude Code, run:

/speck:specify "User authentication with JWT tokens"

This creates:

shared-specs/
└── specs/
    └── 001-user-auth/
        ├── spec.md
        ├── contracts/
        └── data-model.md

Step 2: Define API Contract

Edit the specification to add the API contract that both repos will implement:

File: shared-specs/specs/001-user-auth/contracts/api.md

# Authentication API Contract

## POST /api/auth/login

Authenticate user and return JWT token.

**Request**:
```json
{
  "email": "user@example.com",
  "password": "securepassword123"
}

Response (200 OK):

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "name": "John Doe"
  }
}

Response (401 Unauthorized):

{
  "error": "Invalid email or password"
}

Response (400 Bad Request):

{
  "error": "Email and password are required"
}

POST /api/auth/verify

Verify JWT token validity.

Headers:

Authorization: Bearer <token>

Response (200 OK):

{
  "valid": true,
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com"
  }
}

Response (401 Unauthorized):

{
  "valid": false,
  "error": "Invalid or expired token"
}

Save and commit this contract:

```bash
git add specs/
git commit -m "Add authentication API contract"

From the frontend directory, link to shared specs:

cd ../frontend

In Claude Code, run:

/speck:link ../shared-specs

Expected output:

✓ Created .speck/root symlink → ../shared-specs
✓ Multi-repo mode enabled
✓ Found 1 shared specification: 001-user-auth

Verify the link:

ls -la .speck/

You should see:

.speck/
└── root → ../shared-specs

Step 4: Generate Frontend Plan

Still in the frontend directory, generate the implementation plan:

/speck:plan

What happens:

  1. Speck reads spec.md from ../shared-specs/specs/001-user-auth/
  2. Generates frontend-specific plan at specs/001-user-auth/plan.md
  3. Plan includes React components, API integration, state management

Example plan structure (generated by Speck):

# Implementation Plan: User Authentication (Frontend)

## Technical Context
- React 18 with TypeScript
- React Router for navigation
- Axios for API calls
- LocalStorage for token persistence

## Phase 1: Setup
- Install dependencies (axios, react-router-dom)
- Create API client configuration
- Set up authentication context

## Phase 2: Core Components
- LoginForm component with validation
- Protected route wrapper
- Auth context provider with token management

## Phase 3: Integration
- Call POST /api/auth/login from login form
- Store token in localStorage
- Implement token refresh logic
- Handle 401 responses globally

From the backend directory, link to the same shared specs:

cd ../backend

In Claude Code, run:

/speck:link ../shared-specs

Step 6: Generate Backend Plan

Generate the backend implementation plan:

/speck:plan

What happens:

  1. Speck reads the same spec.md as frontend
  2. Generates different backend-specific plan at specs/001-user-auth/plan.md
  3. Plan includes Express routes, JWT generation, database integration

Example plan structure (generated by Speck):

# Implementation Plan: User Authentication (Backend)

## Technical Context
- Node.js + Express
- PostgreSQL for user storage
- jsonwebtoken for JWT generation
- bcrypt for password hashing

## Phase 1: Setup
- Install dependencies (express, jsonwebtoken, bcrypt, pg)
- Configure JWT secret
- Set up database connection

## Phase 2: Core Implementation
- User model with password hashing
- POST /api/auth/login route
- POST /api/auth/verify route
- JWT generation and validation middleware

## Phase 3: Security
- Rate limiting on auth endpoints
- Token expiration handling
- Password validation requirements

Step 7: Implement Backend

From the backend directory, generate tasks and implement:

/speck:tasks
/speck:implement

Speck will:

  1. Break down the plan into actionable tasks
  2. Guide you through implementing each task
  3. Create the Express API following the contract

Example implementation (Claude Code generates):

File: backend/src/routes/auth.js

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const User = require('../models/User');

const router = express.Router();

// POST /api/auth/login
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    if (!email || !password) {
      return res.status(400).json({ error: 'Email and password are required' });
    }

    const user = await User.findByEmail(email);
    if (!user) {
      return res.status(401).json({ error: 'Invalid email or password' });
    }

    const validPassword = await bcrypt.compare(password, user.password_hash);
    if (!validPassword) {
      return res.status(401).json({ error: 'Invalid email or password' });
    }

    const token = jwt.sign(
      { userId: user.id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: '24h' }
    );

    res.json({
      token,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
      },
    });
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

module.exports = router;

Step 8: Implement Frontend

From the frontend directory, generate tasks and implement:

cd ../frontend

In Claude Code:

/speck:tasks
/speck:implement

Example implementation (Claude Code generates):

File: frontend/src/components/LoginForm.tsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { login } from '../api/auth';

export function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const navigate = useNavigate();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');

    try {
      const { token, user } = await login({ email, password });

      // Store token
      localStorage.setItem('auth_token', token);

      // Navigate to dashboard
      navigate('/dashboard');
    } catch (err: any) {
      setError(err.response?.data?.error || 'Login failed');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      {error && <div className="error">{error}</div>}
      <button type="submit">Log In</button>
    </form>
  );
}

File: frontend/src/api/auth.ts

import axios from 'axios';

const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:3000';

export async function login(credentials: { email: string; password: string }) {
  const response = await axios.post(`${API_BASE}/api/auth/login`, credentials);
  return response.data;
}

export async function verifyToken(token: string) {
  const response = await axios.post(
    `${API_BASE}/api/auth/verify`,
    {},
    { headers: { Authorization: `Bearer ${token}` } }
  );
  return response.data;
}

Step 9: Verify Integration

Test that frontend and backend implement the same contract:

  1. Start backend:
cd backend
npm start
# Server running on http://localhost:3000
  1. Start frontend:
cd ../frontend
npm start
# App running on http://localhost:3001
  1. Test login flow:
    • Open http://localhost:3001
    • Enter credentials
    • Verify login succeeds and token is stored
    • Check network tab for API calls matching contract

Step 10: View Aggregate Status

From the root directory, see status across all repos:

cd ../shared-specs

In Claude Code:

/speck:env --all

Output:

Repository Mode: multi-repo-root
Root Directory: /workspace/auth-tutorial/shared-specs
Active Specifications: 1

Linked Child Repositories: 2
  ├── frontend/
  │   Branch: main
  │   Status: 001-user-auth implementation complete
  └── backend/
      Branch: main
      Status: 001-user-auth implementation complete

Success Criteria

You’ve successfully completed the tutorial if:

  • Shared spec visible from both frontend and backend repos
  • Each repo generated independent implementation plans
  • Backend implements API contract exactly as defined
  • Frontend calls API using contract structure
  • Login flow works end-to-end
  • /speck:env --all shows both repos in multi-repo mode

What You Learned

  1. Shared specifications: One spec, multiple implementations
  2. Contract-first development: APIs defined before implementation
  3. Independent plans: Each repo uses appropriate tech stack
  4. Multi-repo coordination: Symlink-based detection and linking
  5. Aggregate view: Monitor progress across all repos

Next Steps

Troubleshooting

Common issues in this tutorial:

  • Symlink not working → Verify with ls -la .speck/root, recreate with absolute path
  • Plans are identical → Each repo needs its own constitution for different tech stacks
  • Contract mismatch → Both repos should read from the shared root specs

For detailed solutions, see Multi-Repo Issues in the Troubleshooting Guide.