Architecture
Tracker is a pnpm-workspace monorepo. TypeScript strict throughout. The premise — humans and agents as first-class principals — is held up by one architectural rule: there is a single domain layer, and every surface is a thin adapter over it.
Four adapters, one spine
┌─────────────── packages/shared ───────────────┐
│ Zod schemas = the single source of truth for │
│ every DTO / contract (REST, MCP, tests, web) │
└────────────────────────┬───────────────────────┘
│ imported by
┌──────────────────────┬──────────────┼───────────────┬──────────────────┐
│ │ │ │ │
REST routes WebSocket gateway MCP server outbound webhooks
│ │ │ │
└──────────┬───────────┴──────┬───────┴───────────────┘
▼ ▼
domain functions — `(ctx, validatedInput)`
authorize once · write Postgres · append Activity · publish to Redis
│ │
▼ ▼
Postgres (source of truth) Redis pub/sub (event fan-out)
Drizzle ORM + versioned migrations → WS subscribers + webhook dispatcherEvery mutation flows through a shared domain function that authorizes once, writes Postgres, appends an Activity row (the audit trail + webhook payload source + agent poll history), and publishes to Redis. REST handlers, MCP tools, WS commands, and the webhook dispatcher are thin adapters with no privileged path — a contract test proves REST and MCP exercise the identical domain path. Because Redis already fans out events, scaling 1→N instances needs no code change.
Packages
| Package | What it is |
|---|---|
packages/shared | Zod entity + DTO schemas and the scope model. The contract layer. |
packages/core | The domain spine shared by the api and the MCP server (domain functions, the MCP tool catalog, resource formatters). |
apps/api | Fastify service: REST + WS + webhooks over the domain layer; Drizzle/Postgres; ioredis; owns + applies migrations. |
apps/mcp | The standalone MCP server (Streamable HTTP), sharing packages/core, Postgres, and Redis with the api. Runs no migrations. |
apps/web | React + Vite SPA: TanStack Query (optimistic mutations), Zustand, dnd-kit, Tailwind (VS Code "Dark High Contrast" tokens). |
packages/plugin-sdk | The out-of-process integration SDK (webhook verification, REST client, EntityLinks). See Plugins. |
e2e | Playwright cross-client drag-and-drop E2E. |
Stack: Fastify · Drizzle ORM + Drizzle Kit · Zod · ioredis · @modelcontextprotocol/sdk · Argon2 (human passwords) / SHA-256 (agent tokens) · React + Vite · TanStack Query · Zustand · dnd-kit · Tailwind · Vitest · Playwright · Postgres · Redis · Docker Compose.
Load-bearing discipline
These rules keep the architecture honest — violating them defeats the project's premise:
- Single source of truth for shapes. All DTOs/contracts are Zod schemas in
packages/shared. Never redefine a shape inline — REST, MCP, and test data all import from there. MCP tool input schemas are generated from these DTOs, so MCP and REST can never disagree. - Domain functions are the spine. Business logic lives in the domain layer, takes
(ctx, validatedInput), and never touches HTTP/WS/MCP types. A contract test (rest-mcp-equivalence) proves REST and MCP hit the identical path — keep it passing. - Fractional indexing for list/card
position(generateKeyBetween) — never integer positions, which would cascade under concurrent drag-and-drop. client_mutation_ididempotency key on every optimistic mutation — required for correct reconciliation/rollback against live WebSocket events.- UUID v7 ids, generated in the app (
uuidv7()), not by the DB. - Soft-delete via an
archivedflag; the append-onlyActivitylog is never rewritten — it is the audit trail, webhook payload source, and agent poll history.
Data & real-time
- Postgres is the source of truth. Schema changes are versioned Drizzle migrations (
apps/api/drizzle); the api applies them idempotently on start. - Redis is the event bus (pub/sub fan-out) and the store for rate-limit buckets, sessions, and WS handshake tickets.
- The SPA applies mutations optimistically and reconciles against authoritative WebSocket events keyed by
client_mutation_id.
Developing
From the repo root (pnpm workspaces):
pnpm install
docker compose up -d postgres redis # backing services
pnpm --filter @kanban/api exec drizzle-kit migrate
pnpm --filter @kanban/api dev # api on :3000
pnpm --filter @kanban/web dev # SPA on :5173
# The gate (keep green)
pnpm -r typecheck
pnpm -r lint
pnpm -r test # Vitest unit + integration
pnpm test:e2e # Playwright cross-client E2EUnit/integration tests use a separate kanban_test database; E2E uses kanban_e2e. Rate limiting is auto-disabled under Vitest. See the repository README and CONTRIBUTING for the full contributor workflow.