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"
Step 3: Link Frontend Repository
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:
- Speck reads
spec.mdfrom../shared-specs/specs/001-user-auth/ - Generates frontend-specific plan at
specs/001-user-auth/plan.md - 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
Step 5: Link Backend Repository
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:
- Speck reads the same
spec.mdas frontend - Generates different backend-specific plan at
specs/001-user-auth/plan.md - 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:
- Break down the plan into actionable tasks
- Guide you through implementing each task
- 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:
- Start backend:
cd backend
npm start
# Server running on http://localhost:3000
- Start frontend:
cd ../frontend
npm start
# App running on http://localhost:3001
- 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 --allshows both repos in multi-repo mode
What You Learned
- Shared specifications: One spec, multiple implementations
- Contract-first development: APIs defined before implementation
- Independent plans: Each repo uses appropriate tech stack
- Multi-repo coordination: Symlink-based detection and linking
- Aggregate view: Monitor progress across all repos
Next Steps
- Try Worktree Integration for parallel feature development
- Try Monorepo support as an alternative approach
- Review Capability Matrix for advanced features
- Explore Multi-Repo Concepts for deeper understanding
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.