Build a kanban task board from the records pattern
Start state: nothing. End state: a drag-and-drop kanban board with workflow columns, priorities, and a due-date calendar — without writing any board code. The trick: the shell's entity-view kit already contains a kanban board; you get it by declaring a statusKey whose select options are your workflow stages.
1. Scaffold with records
pnpm new tracker --title "Tracker" --type webapp --features records
pnpm install
cd apps/tracker && npx convex dev --once && pnpm exec tsc --noEmit
Expected: apps/tracker with features dashboard, settings, records; tsc clean. All edits below are inside apps/tracker.
2. Define the workflow columns
The board groups rows by the field named in statusKey, and its columns come from that field's select options — one option, one column, in order. In convex/recordsShared.ts, replace the 3 sample statuses with a 4-stage workflow (tones color the badges: accent, warn, ok, danger):
// convex/recordsShared.ts
export const STATUS_VALUES = ["backlog", "doing", "review", "done"] as const;
export type RecordStatus = (typeof STATUS_VALUES)[number];
export const STATUS_META: { value: RecordStatus; label: string; tone?: "ok" | "warn" | "danger" | "accent" }[] = [
{ value: "backlog", label: "Backlog" },
{ value: "doing", label: "Doing", tone: "accent" },
{ value: "review", label: "Review", tone: "warn" },
{ value: "done", label: "Done", tone: "ok" },
];
This file is the single source of truth: the backend validators in convex/records.ts and the frontend select options both import it, so a stage can never exist in one place and not the other.
3. Shape the config for a task tracker
In src/features/records/pages, the one CONFIG is the entire frontend. For a task board you want status (columns), priority (triage), and dueDate (calendar + timeline):
const CONFIG: EntityConfig = {
labels: ["Task", "Tasks"],
titleKey: "title",
statusKey: "status", // board columns = STATUS_META, in order
dateKey: "dueDate", // powers calendar + timeline views
fields: [
{ key: "title", label: "Title", type: "text", required: true },
{ key: "status", label: "Status", type: "select", options: STATUS_META },
{ key: "priority", label: "Priority", type: "select", options: PRIORITY_META },
{ key: "tags", label: "Tags", type: "tags" },
{ key: "dueDate", label: "Due", type: "date" },
{ key: "notes", label: "Notes", type: "longtext" },
],
};
PRIORITY_META already ships in recordsShared.ts (low / medium with tone "warn" / high with tone "danger") — keep it as-is. Dropping imageKey, positionKey, and amountKey from the config automatically hides gallery, canvas, and stats totals: availableViews(CONFIG) only offers views the config can power.
4. How drag persists
You do not write drag code. The board calls the onPatch callback the page already wires:
<EntityViews
cfg={CONFIG}
rows={rows}
view={view}
onOpen={(row) => setEditing(row)}
onPatch={(id, patch) => run(update({ id: id as Id<"records">, ...patch }))}
onDelete={(id) => run(remove({ id: id as Id<"records"> }))}
/>
Dragging a card from Doing to Review fires onPatch(id, { status: "review" }); the updateRecord mutation validates against STATUS_VALUES, writes, and Convex reactivity moves the card for every open tab.
If you renamed statuses, existing rows still hold the old values — either clear the table in the Convex dashboard or add a tiny migration mutation. The board shows a column only for values present in STATUS_META.
5. Codegen, typecheck, run
npx convex codegen && pnpm exec tsc --noEmit
pnpm dev
Expected: zero tsc errors. Sign up, open Records, switch to the board view — four columns, cards draggable between them. Switch to calendar: tasks land on their dueDate; drag through timeline for the same data over time. Every view is the same rows, re-projected.
What you built
- A kanban task tracker: 4 tone-colored workflow columns, drag-to-move persisted via one onPatch line
- Priority triage and a due-date calendar from two extra field declarations
- Proof of the pattern: the "board" is just a select field the view kit knows how to group by