Architecture

Deckgauge is a pnpm + Turborepo monorepo: four apps and three shared packages, each with one job. This page maps what owns what, how a browser request reaches the database, and how source data becomes the numbers on an intelligence widget.

deckgauge · pnpm-workspace.yaml
deckgauge
pnpm + Turborepo
apps/*
web · api · worker · advisor-bridge
packages/*
shared · db · ui

The workspace's two package globs — everything under apps/* and packages/* is a workspace member.

What each app and package owns

The workspace is defined by pnpm-workspace.yaml (apps/* and packages/*), and turbo.json pipelines build, dev, test, and lint across all of them. Four apps ship as separate Docker containers; three packages are pure library code with no server of their own.

  • apps/web — the Next.js 14 App Router frontend. The board, dashboards, timesheet, org trees, and settings all live under apps/web/app. It never talks to Postgres or ClickHouse directly — every read and write goes through apps/api over HTTP, from server actions in apps/web/app/actions.
  • apps/api — a Fastify server (apps/api/src/server.ts) that is the only thing with a database connection. Routes are grouped by domain (boards, widgets, intelligence-query, org-trees, and so on), each registered as a Fastify plugin. Everything under the app's main plugin registration runs behind two layered plugins: a Keycloak JWT auth plugin (apps/api/src/auth/keycloak-auth.plugin.ts) that verifies a bearer token and resolves it to a local User read as request.user — it doesn't reject anything by itself; a missing or invalid token just means no request.user gets set — and a policy plugin (apps/api/src/auth/policy.plugin.ts) that does the actual enforcing. Every route declares a config.policy (public, authenticated, a board or roadmap role, comparison, or connectionOwner — see apps/api/src/auth/policy.ts), and a second preHandler evaluates it on every request, answering 401 when there's no valid token and 403 when there is one but it doesn't meet the policy. A route registered with no declared policy doesn't just fail its own requests — it fails the server's boot entirely, with every offending route collected and reported together rather than one at a time. The resulting map of all 278 routes and their policies is committed at apps/api/src/route-inventory.snapshot.json, so there's no route left to "assume" about. The one way around all of it: DECKGAUGE_SINGLE_USER=true, which bypasses every policy — see Configuration reference.
  • apps/worker — a BullMQ worker with no HTTP surface. It runs the scheduled and manually-triggered sync jobs that pull from Jira, GitHub, GitLab, and Azure DevOps, writes board rows to Postgres, and — for intelligence — dual-writes normalized rows to ClickHouse (apps/worker/src/github-dual-writer.ts and its Jira/GitLab/ADO equivalents).
  • apps/advisor-bridge — a standalone process (started with pnpm deckgauge:advisor or scripts/advisor-bridge.sh, not part of docker compose) that exposes the AI advisor over an ACP/WebSocket bridge so a local coding agent can act on the board. The published open-source snapshot excludes most of scripts/ pending audit — pnpm deckgauge:advisor doesn't depend on it and still works there.
  • packages/shared — every request/response shape as a Zod schema, plus type-only constants both sides agree on: widget type unions (widget-types.ts), DORA thresholds (dora.ts), sync-queue names, and provider adapters. Both apps/web and apps/api import from it; it has no dependency on either.
  • packages/db — the source of truth for database types. It re-exports the generated Prisma client and model types (PrismaClient, Board, User, and so on) from packages/db/prisma/schema.prisma, and exports the shared ClickHouse client (clickhouse, chInsertMany) and migration runner. Nothing outside this package touches Prisma or ClickHouse connection details directly.
  • packages/ui — shared React components used across the board and dashboard surfaces (row and column primitives, badges, comment editor, keyboard-navigation provider) — presentation, not data-fetching.

The request path: browser to database

A page in apps/web renders a Server Component or calls a server action, which calls apps/api over plain HTTP — apps/web/app/lib/api-server.ts resolves the API's base URL from API_URL (defaulting to http://localhost:3001) and attaches the signed-in user's Keycloak access token as a bearer header. In apps/api/src/server.ts, that request passes through the Keycloak auth plugin, then the policy plugin's authorization check, then a domain's Fastify route handler, which delegates to a *.service.ts for the actual Prisma query against Postgres. The response is a plain JSON object shaped by a Zod schema from packages/shared, so both sides can trust its shape without re-validating it.

deckgauge · request path
Browser
signed-in session
apps/web
Next.js server action
apps/api
Fastify + Keycloak JWT + policy
Postgres
boards, users, sources

A board read: browser through web, api, and Postgres.

Where intelligence data comes from

Widgets under apps/web/app/components/dashboard/widgets don't read Postgres at all — their numbers come from ClickHouse, populated on a separate path. apps/worker runs a sync job per source (jira-sync.handler.ts, github-sync.handler.ts, gitlab-sync.handler.ts, azure-devops-sync.handler.ts); alongside writing board rows to Postgres, each source has an intelligence handler and a dual-writer (for example github-dual-writer.ts) that inserts normalized rows — pull requests, reviews, commits, issues — into ClickHouse tables like github_pull_requests and github_commits via chInsertMany from packages/db.

On the read side, apps/api/src/intelligence-query/builders holds one file per widget type, each building a parameterized ClickHouse SQL query and self-registering with registerBuilder(widgetType, builderFn). apps/api/src/widgets/widget-data.service.ts looks up a widget's builder, runs the query against the shared clickhouse client, and shapes the rows into the response the widget component expects. The full path from a Jira issue landing to a tile updating: source API → worker adapter → ClickHouse dual-write → intelligence-query builder → widget-data service → widget component.

Jira is read-onlyEvery hop above is a read from Jira's side. Deckgauge never writes an issue, comment, or transition back to it — see the Jira source page.

Related

Last updated