codexmachina
App schema

Project management, built on every verified stack.

Project-management layer: user-owned projects, kanban tasks with status/priority, many-to-many assignees, per-project labels, and comment threads.

40 verified stacks40 greenLast updated 2026-08-23How we verify →

40 verified stacks

Showing 12 across 1 app types. Every stack page links its neighbours along each axis, so any combination is a click or two from here.

Next.js 16 (App Router) + MySQL 8Auth.js (NextAuth) · Resend
Next.js 16 (App Router) + MySQL 8Better Auth
Next.js 16 (App Router) + MySQL 8Clerk
Next.js 16 (App Router) + Postgres (Neon)Better Auth
Next.js 16 (App Router) + Postgres (Neon)Clerk
Next.js 16 (App Router) + Postgres (Neon)Supabase Auth · Resend
Nuxt 4 + MySQL 8Supabase Auth
Nuxt 4 + Postgres (Neon)Better Auth
Nuxt 4 + Postgres (Neon)Supabase Auth · Resend
React Router v8 + MySQL 8Better Auth · Resend
React Router v8 + MySQL 8Supabase Auth
React Router v8 + Postgres (Neon)Better Auth · Resend

What Project management gives you

Six tables sit on top of Better Auth's identity layer, and their shape is a tracker rather than a tenant. projects is the root, tasks hang off a project, and assignments, labels and comments all hang off a task. Ownership is one owner_id column on projects referencing user.id as text — text because that is what Better Auth issues — and there is no organization or membership table anywhere in the schema. A project has exactly one owner; participation is expressed by task_assignees rows, not by joining a team. tasks carries the state a board reads: status (todo, in_progress, done), priority (low, medium, high, urgent) and a nullable due_date.

Both graded columns are text guarded by named CHECKs, tasks_status_check and tasks_priority_check, instead of pgEnum, so adding a 'blocked' column is a constraint swap on a live table rather than an ALTER TYPE. The one composite index, idx_task_project_status on (project_id, status), is the board itself: a single kanban column is one range on that index, and a project-wide read uses the same index by prefix. Both many-to-many edges are modelled as sets, not logs. task_assignees is unique on (task_id, assignee_id), so re-assigning the same person is idempotent instead of duplicating a row, and idx_assignee_user on assignee_id is the only path into work that does not start from a project — it is what makes a cross-project 'assigned to me' view cheap.

task_labels repeats that shape against the per-project labels catalog, unique on (task_id, label_id) with idx_task_label_label covering the reverse read from a label back to its tasks. task_comments is append-only and indexed on (task_id, created_at), which supplies the filter and the ordering out of one structure. Every foreign key cascades downward on delete: removing a project takes its tasks, and each task takes its assignments, label links and comments with it in one statement. What is not indexed is due_date and priority, so an overdue-everywhere report scans — add that index before you build the screen, not after it starts timing out.

Built for

One kanban column for a project

tasks, served by idx_task_project_status on (project_id, status): equality on both columns is a single index range, and the same index answers a project-wide read through its leading column.

Every task assigned to me, across projects

task_assignees, via idx_assignee_user on assignee_id — the only index that reaches work without going through projects. Each hit joins to tasks by primary key.

Assigning someone who may already be assigned

task_assignees carries the composite unique task_assignees_task_user_unique on (task_id, assignee_id), so a repeat insert is a constraint violation and the handler can stay an idempotent ON CONFLICT DO NOTHING.

The comment thread on a task, oldest first

task_comments, with idx_comment_task_time on (task_id, created_at) supplying both the filter and the sort order, so the thread comes back without a separate sort step.

Which tasks wear this label

task_labels, read backwards through idx_task_label_label on label_id; labels themselves are scoped per project by idx_label_project on project_id.

Why Project management, specifically

note

task_assignees carries a composite unique on (task_id, assignee_id) — a user can be assigned to a task at most once; the per-user index on assignee_id backs the 'tasks assigned to me' feed.

note

Status and priority are stored as text + CHECK (not pgEnum) so new values like 'blocked' or 'critical' ship without an ALTER TYPE migration dance.