Zurück zum Blog
The Agency Handoff Problem: How to Transfer a Codebase Without Losing 3 Months

The Agency Handoff Problem: How to Transfer a Codebase Without Losing 3 Months

Dennis Reinkober30. März 20265 Min. Lesezeit

Here's the dirty secret of agency development: most projects end with a zip file, a Slack message saying "let us know if you have questions," and silence.

The client's new in-house team opens the codebase. They see 200 files. No documentation. No explanation for why things are structured the way they are. No context on which workarounds are intentional and which are technical debt. They spend the next three months just understanding what they inherited before they can ship a single feature.

We've been on both sides of this. We've inherited codebases from other agencies, and we've handed off our own work. After too many painful experiences, we built a handoff process that actually works.

Why Handoffs Fail

The problem isn't that agencies write bad code. The problem is that code without context is unusable.

Consider this file structure:

src/
├── lib/
│   ├── pricing.ts          # Why three pricing functions?
│   ├── pricing-v2.ts       # Is v1 still used?
│   ├── pricing-legacy.ts   # What's "legacy" about it?
│   └── utils.ts            # 800 lines of... everything
├── api/
│   ├── orders/
│   │   └── route.ts        # Has a 200-line function
│   └── webhooks/
│       └── stripe/
│           └── route.ts    # "DO NOT TOUCH" comment, no explanation

Every developer who wrote this code knew exactly why pricing-v2.ts exists. They knew that the Stripe webhook handler has a specific retry quirk that breaks if you refactor it. They knew utils.ts is a dumping ground they planned to clean up.

But they didn't write any of that down. And now they're gone.

The Knowledge Half-Life

Within 2 weeks of project end, the original developers forget half the context. Within 2 months, they forget most of it. If you don't document during development, the knowledge is gone forever.

Our Handoff Checklist

We treat handoff as a deliverable — not an afterthought. It starts on day one, not on the last day.

1. Architecture Decision Records (ADRs)

Every significant decision gets a short document explaining the what, why, and alternatives considered. These live in the repo, not in Notion or Confluence (which the client might not have access to in six months).

<!-- docs/decisions/003-polling-over-websockets.md -->

# ADR 003: Polling Over WebSockets for Real-Time Updates

## Status
Accepted

## Context
The ordering system needs near-real-time updates for order status.
WebSockets would provide true real-time but add significant complexity:
- Requires a persistent connection server
- Reconnection logic for mobile networks
- Additional infrastructure (Redis pub/sub)

## Decision
Use HTTP polling with SWR (stale-while-revalidate) at 3-second intervals.

## Rationale
- Order status changes are infrequent (every few minutes)
- 3-second polling provides "good enough" real-time for this use case
- No additional infrastructure required
- Works reliably on flaky mobile connections
- SWR handles caching and deduplication automatically

## Consequences
- Slightly higher server load from polling (measured: ~2% CPU increase)
- Maximum 3-second delay for status updates (acceptable for this domain)
- If real-time requirements change, WebSocket migration is straightforward

We typically write 5–10 ADRs per MVP. They take 10 minutes each. They save weeks of guesswork.

2. The README That Actually Helps

Most READMEs are either empty or contain setup instructions from six months ago that no longer work. Ours follows a specific template:

# Project Name

## Quick Start
1. Clone this repo
2. Copy `.env.example` to `.env` and fill in values
3. Run `docker compose up`
4. Open http://localhost:3000
5. Seed data: `npm run db:seed`

## Architecture Overview
- **Frontend:** Next.js 15, React 19, Tailwind CSS v4
- **Backend:** Next.js API routes + Prisma ORM
- **Database:** PostgreSQL 16
- **Auth:** better-auth (session-based)
- **Hosting:** Hetzner VPS with Docker Compose

## Key Concepts
- Orders flow through statuses: DRAFT → PENDING → CONFIRMED → SHIPPED → DELIVERED
- Pricing is calculated server-side only (see `lib/pricing.ts`)
- File uploads go to Hetzner Object Storage (S3-compatible)

## Environment Variables
| Variable | Required | Description |
|---|---|---|
| DATABASE_URL | Yes | PostgreSQL connection string |
| AUTH_SECRET | Yes | Random 32-char string for session encryption |
| STRIPE_SECRET_KEY | Yes | Stripe API key (use test mode for dev) |
| S3_ENDPOINT | Yes | Object storage endpoint |

## Common Tasks
- **Add a new API route:** See `app/api/orders/route.ts` as template
- **Run migrations:** `npx prisma migrate dev`
- **Deploy:** Push to `main` branch, CI handles the rest
- **Debug payments:** Check Stripe dashboard, then `app/api/webhooks/stripe/route.ts`

## Known Issues / Tech Debt
- [ ] `lib/utils.ts` needs to be split into domain-specific utilities
- [ ] Email templates are hardcoded HTML, should use React Email
- [ ] No rate limiting on public API endpoints yet

3. Recorded Walkthrough Videos

We record two videos before every handoff:

Video 1: Architecture Walkthrough (30 min) Screen share walking through the codebase structure, data flow, and key design decisions. This is the video a new developer watches on day one.

Video 2: Deployment & Operations (15 min) How to deploy, how to roll back, where to check logs, how to handle common issues. This is the video you watch at 2 AM when something breaks.

We use Loom. The videos live in a shared folder linked from the README. They cost almost nothing to produce and save hours of back-and-forth.

4. Docker-First Local Development

If a new developer can't run the project locally within 30 minutes of cloning the repo, the handoff has failed.

# docker-compose.yml — everything runs locally
services:
  app:
    build: .
    ports: ["3000:3000"]
    volumes: ["./:/app", "/app/node_modules"]
    env_file: .env
    depends_on: [db, redis]

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: localdev
    ports: ["5432:5432"]
    volumes: [pgdata:/var/lib/postgresql/data]

  redis:
    image: redis:7
    ports: ["6379:6379"]

volumes:
  pgdata:

One command. Everything works. No "install PostgreSQL 16 and configure it manually" instructions.

The .env.example Pattern

Always include a .env.example with placeholder values and comments. Never make a developer guess which environment variables exist or what format they need.

5. Test Suites as Living Documentation

Tests aren't just for catching bugs — they're documentation that can't go stale. When a new developer reads a well-written test, they understand the business logic:

describe("Order pricing", () => {
  it("applies 10% bulk discount for orders over 50 items", async () => {
    const order = createOrder({ items: 60, pricePerItem: 10 });
    expect(order.total).toBe(540); // 600 - 10% = 540
  });

  it("does not stack bulk discount with promo codes", async () => {
    const order = createOrder({
      items: 60,
      pricePerItem: 10,
      promoCode: "SAVE20",
    });
    // Promo code wins because it's a bigger discount
    expect(order.total).toBe(480); // 600 - 20% = 480
  });

  it("charges delivery fee for orders under €50", async () => {
    const order = createOrder({ items: 3, pricePerItem: 10 });
    expect(order.deliveryFee).toBe(4.99);
    expect(order.total).toBe(34.99);
  });
});

A new developer reads these tests and immediately understands: bulk discounts exist, they don't stack with promo codes, and there's a delivery fee threshold. No documentation needed.

6. The Knowledge Transfer Meeting

The final step isn't a document — it's a structured 2-hour meeting with the client's team. Our agenda:

  1. Architecture overview (30 min) — walk through the system, answer questions
  2. Day-in-the-life demo (30 min) — add a simple feature together to show the development workflow
  3. Operations runbook (30 min) — deployment, monitoring, incident response
  4. Open questions (30 min) — anything the team is unsure about

We record this meeting. It becomes the single most referenced artifact in the first month after handoff.

What This Costs

Let's be transparent: our handoff process adds about 2–3 days to a 4-week MVP engagement. That's roughly 5–8% of the project budget.

What it saves: 2–3 months of the client's in-house team struggling to understand the codebase. At in-house developer rates, that's 5–10x the cost of doing the handoff properly.

The Bottom Line

Our job isn't done when we deploy. It's done when your team can maintain the code without us.

That's not a marketing line. It's a business decision. Clients who have a smooth handoff come back for the next project. Clients who inherit a codebase they can't understand don't come back — and they tell other founders about it.

Document as you build. Record the walkthroughs. Containerize the setup. Write the tests. Hand off a codebase someone can actually work with.

The best compliment we've ever received from a client: "Your team left, and we didn't even notice."


We build MVPs designed for handoff from day one. Learn more about our MVP Development process.

Sources

Ähnliche Beiträge