Skip to content

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 dispatcher

Every 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

PackageWhat it is
packages/sharedZod entity + DTO schemas and the scope model. The contract layer.
packages/coreThe domain spine shared by the api and the MCP server (domain functions, the MCP tool catalog, resource formatters).
apps/apiFastify service: REST + WS + webhooks over the domain layer; Drizzle/Postgres; ioredis; owns + applies migrations.
apps/mcpThe standalone MCP server (Streamable HTTP), sharing packages/core, Postgres, and Redis with the api. Runs no migrations.
apps/webReact + Vite SPA: TanStack Query (optimistic mutations), Zustand, dnd-kit, Tailwind (VS Code "Dark High Contrast" tokens).
packages/plugin-sdkThe out-of-process integration SDK (webhook verification, REST client, EntityLinks). See Plugins.
e2ePlaywright 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_id idempotency 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 archived flag; the append-only Activity log 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):

bash
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 E2E

Unit/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.

Released under the AGPL-3.0 license.