Files
Codewalkers/docs/preview.md
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

5.2 KiB

Preview Deployments

apps/server/preview/ — Docker-based preview deployments for reviewing changes in a running application.

Overview

When a phase enters pending_review, reviewers can spin up the app at a specific branch in local Docker containers, accessible through a single port via a Caddy reverse proxy.

Key design decision: No database table. Docker IS the source of truth. Instead of persisting rows, we query Docker directly via compose project names, container labels, and docker compose CLI commands.

Architecture

PreviewManager
  ├── ConfigReader (discover .cw-preview.yml / compose / Dockerfile)
  ├── ComposeGenerator (generate docker-compose.yml + Caddyfile)
  ├── DockerClient (thin wrapper around docker compose CLI)
  ├── HealthChecker (poll service healthcheck endpoints)
  └── PortAllocator (find next available port 9100-9200)

Lifecycle

  1. Start: discover config → allocate port → generate compose + Caddyfile → docker compose up --build -d → health check → emit preview:ready
  2. Stop: docker compose down --volumes --remove-orphans → clean up .cw-previews/<id>/ → emit preview:stopped
  3. List: docker compose ls --filter name=cw-preview → parse container labels → reconstruct status
  4. Shutdown: stopAll() called on server shutdown to prevent orphaned containers

Docker Labels

All preview containers get cw.* labels for metadata retrieval:

Label Purpose
cw.preview "true" — marker for filtering
cw.initiative-id Initiative ID
cw.phase-id Phase ID (optional)
cw.project-id Project ID
cw.branch Branch name
cw.port Host port
cw.preview-id Nanoid for this deployment

Compose Project Naming

Project names follow cw-preview-<nanoid> convention. This enables filtering via docker compose ls --filter name=cw-preview.

Configuration

Preview configuration is discovered from the project directory in this order:

1. .cw-preview.yml (explicit CW config)

version: 1
services:
  frontend:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    port: 3000
    route: /
    healthcheck:
      path: /
      interval: 5s
      retries: 10
    env:
      VITE_API_URL: /api

  backend:
    build:
      context: .
      dockerfile: packages/api/Dockerfile
    port: 8080
    route: /api
    healthcheck:
      path: /health
    env:
      DATABASE_URL: postgres://db:5432/app

  db:
    image: postgres:16-alpine
    port: 5432
    internal: true    # not exposed through proxy
    env:
      POSTGRES_PASSWORD: preview

2. docker-compose.yml / compose.yml (existing compose passthrough)

If found, the existing compose file is wrapped with a Caddy sidecar.

3. Dockerfile (single-service fallback)

If only a Dockerfile exists, creates a single app service building from . with port 3000.

Reverse Proxy: Caddy

Caddy runs as a container in the same Docker network. Only Caddy publishes a port to the host. Generated Caddyfile:

:80 {
  handle_path /api/* {
    reverse_proxy backend:8080
  }
  handle {
    reverse_proxy frontend:3000
  }
}

Module Files

File Purpose
types.ts PreviewConfig, PreviewStatus, labels, constants
config-reader.ts Discovery + YAML parsing
compose-generator.ts Docker Compose YAML + Caddyfile generation
docker-client.ts Docker CLI wrapper (execa)
health-checker.ts Service readiness polling
port-allocator.ts Port 9100-9200 allocation with bind test
manager.ts PreviewManager class (start/stop/list/status/stopAll)
index.ts Barrel exports

Events

Event Payload
preview:building {previewId, initiativeId, branch, port}
preview:ready {previewId, initiativeId, branch, port, url}
preview:stopped {previewId, initiativeId}
preview:failed {previewId, initiativeId, error}

tRPC Procedures

Procedure Type Input
startPreview mutation {initiativeId, phaseId?, projectId, branch}
stopPreview mutation {previewId}
listPreviews query {initiativeId?}
getPreviewStatus query {previewId}

CLI Commands

cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>]
cw preview stop <previewId>
cw preview list [--initiative <id>]
cw preview status <previewId>

Frontend

PreviewPanel component in the Review tab:

  • No preview: "Start Preview" button
  • Building: Spinner + "Building preview..."
  • Running: Green dot + http://localhost:<port> link + Stop button
  • Failed: Error message + Retry button

Polls getPreviewStatus with refetchInterval: 3000 while active.

Container Wiring

  • PreviewManager instantiated in apps/server/container.ts with (projectRepository, eventBus, workspaceRoot)
  • Added to Container interface and toContextDeps()
  • GracefulShutdown calls previewManager.stopAll() during shutdown
  • requirePreviewManager(ctx) helper in apps/server/trpc/routers/_helpers.ts

Dependencies

  • js-yaml + @types/js-yaml — for parsing .cw-preview.yml
  • Docker must be installed and running on the host