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 |
The layout of the source
Section titled “The layout of the source”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, eventse2e/ Playwright specsexamples/ a small agent, in one file, and the Claude Code skilldrizzle/ generated migrationsdesign-reference/ the source design this interface followsThe decisions that shaped the rest
Section titled “The decisions that shaped the rest”No field is hardcoded
Section titled “No field is hardcoded”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.
An agent is a row in users
Section titled “An agent is a row in users”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.
Ordering is a string, not an integer
Section titled “Ordering is a string, not an integer”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.
The event says what, never how
Section titled “The event says what, never how”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.
The lease is on the read path
Section titled “The lease is on the read path”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.