Skip to content

How it is built

Part Choice
Framework Next.js 16, App Router, server components for the first paint
Language TypeScript, strict. ESLint refuses any
Database PostgreSQL 18 with Drizzle ORM
Drag dnd-kit, one context for cards and columns
Live Server-sent events fed by Postgres LISTEN/NOTIFY
Auth scrypt password hashes, a random session id in the database
Styling CSS modules. No framework
Tests Vitest for the rules, Playwright for the screens
src/
app/ routes: pages and the JSON API
components/
board/ the board, the columns, the cards, the detail panel
settings/ properties, views and members
ui/ button, input, tag, card, toasts, confirm row
db/ schema, client, seed
lib/ auth, tokens, ranks, grouping, value rules, runs, events
e2e/ Playwright specs
examples/ a small agent, in one file, and the Claude Code skill
drizzle/ generated migrations
design-reference/ the source design this interface follows

task_values is one row per task and property, with the value shape decided by the property’s type. That is the whole trick, and everything else follows from it: columns are a property’s options, a view is a grouping property, and adding a property type means one branch in the value rules and one control in the panel.

A change that adds a fixed field works against the idea of the product, however small it looks.

A member of the project with kind = "agent" and no password. Nothing else in the schema knows about agents, which is why an assignee, a comment author and an activity actor all work for them without a second code path anywhere.

Cards, properties, options and views all carry a base-62 fractional rank, so a drag writes one row instead of renumbering a column. See Fractional ranks.

A write publishes pg_notify; every process fans it out to the boards it is serving. The payload names the scope and nothing else, so there is no state to reconcile and no ordering to get wrong — a listener refetches. Two processes on one database stay in step with no broker.

Geometry lives in one place, except on the board

Section titled “Geometry lives in one place, except on the board”

Off the board, a button, an input, a tag and a card are declared once in components/ui/. They used to be declared three times each, at three different heights. The board keeps its own CSS on purpose — it has geometry nothing else shares.

A run that has not reported for thirty minutes is closed when somebody reads that board or that task, not by a scheduler. The board is read far more often than any schedule would fire, and one indexed update costs less than a job this project would then have to run, watch and ship.