Building the PM / Outliner — Tech-Stack RPQA

Should we build it ourselves, headless, on Supabase, and let Claude keep it tidy? A researched gut-check before committing.

Deep-research run · 108 agents · 26 sources · 120 claims → 25 verified by 3-vote adversarial check (0 overturned) · 2026-06-26. Library facts move fast — re-check at build time.

Verdict

Go. Build it yourself on Next.js + Supabase, draw the screen with a headless toolkit (TanStack Table + Virtual), and let Claude maintain it through the Supabase connection. Nothing the research verified argues against this. Two things to prove out early: (1) buttery scrolling when rows are nested, different heights, and expanding; (2) guardrails so the AI can't quietly break the data.

Plain English — left column. No jargon. What it means for you.
Technical — right column. The libraries, terms, and gotchas.

1 · Drawing the screen

ValidatedConfidence: highUpdate: flat-DOM is the low-risk default
Plain English

Use a toolkit that does the heavy lifting (smooth scrolling through huge lists) but lets you control every pixel of how a row looks. The makers' own demo scrolls 50,000 rows of different heights smoothly, so the engine is proven.

The honest catch: that demo is a flat list. Our list is nested and folds open/closed, and rows change height when you expand them — and that exact combination isn't proven anywhere. So we build a quick throwaway test of it first, before betting on it.

We looked at the popular ready-made grids and they don't fit: one hides the feature we need behind a paywall (and can't do our shared-item trick anyway); another is blazing fast but draws cells in a way that can't show a foldable outline.

Update — the third path (Checkvist follow-up): there's a simpler option than any scrolling-engine: just draw the whole list and keep each row cheap. That's essentially what your board already does. The research says this is the right, lower-risk choice up to a few thousand rows, and it dodges the scrolling-engine's height-measuring bugs entirely. Only switch to the heavy scrolling-engine if a list ever grows into the tens of thousands. Honest caveat: public sources could not confirm how Checkvist actually works — that "flat, cheap, hand-updated" picture comes from our own earlier teardown, not verified evidence.

Technical

TanStack Table (logic-only, no built-in virtualization) + TanStack Virtual (headless windowing). Official example renders makeData(50_000) with estimateSize + measureElement. Variable heights mean dropping native table layout: keep semantic tags but use display:grid/flex.

Risk: 50k demo is flat. Nested + variable-height + expand/collapse is the unverified regime; open dynamic-measure bugs (#235/#425/#659/#832/#896) cause upward-scroll stutter. Mitigate: good estimateSize, overscan, "block translation" layout. Prototype before committing.

Rejected: AG Grid Community Tree Data = Enterprise-only + single-parent. Glide = canvas (millions of rows) but all cells via Canvas API, no tree/expand.

Follow-up — flat DOM as a 3rd option. content-visibility:auto = flat DOM + paint-skip (your current board), not virtualization — nodes stay in DOM + a11y tree. Comfortable to low-thousands; Lighthouse flags ~800 (warn)/~1,400 (excessive); verified painful ~20k (content-visibility can't match a virtual list there — Nolan Lawson trace). TanStack Virtual's variable-height cost is real & primary-sourced: runtime measureElement, scroll-up jank (#659/#832), and a now-fixed bug (#1133) triggered by exactly expand/collapse/delete. Recommendation: stay flat for low-thousands; adopt TanStack Virtual only at ~10k+. Checkvist internals UNVERIFIED publicly; Logseq does virtualize (DataScript in-memory + virtualized-list). Shared real pattern across outliners = in-memory store + adjacency list, not "render flat everything."

2 · How the nesting is stored

Validated (core)Mirrors: needs a spike
Plain English

The standard way to store "things inside things" is for each item to remember its parent. Its one classic weakness — "show me everything underneath this" — is basically solved on our database: it can walk the whole tree in one fast query (200k items in under a tenth of a second).

The twist is your mirrors — one item living under several parents. A plain tree can't do that without making copies (which we refuse). The fix is to store an item's identity separately from where it's placed, so one real item can appear in many spots. That part is sound in principle but we should prove the exact design (and how to stop loops) before locking the schema.

Technical

Adjacency list (self-ref parent_id) is the conventional choice; Karwin flags it only for descendant queries — neutralized by recursive CTEs (WITH RECURSIVE, PG 8.4+): ~200k rows <0.1s with a parent_id index.

Mirrors → DAG, not a tree. Decouple identity from placement with an edge/junction table (node_id, parent_id, position); one record, many parents, shared state. Closure-table / materialized-path / ltree are heavier (unvalidated). Open: cycle-prevention strategy + DAG query patterns not source-verified — spike before schema.

3 · Counting "done" up the tree

Not validated — biggest risk
Plain English

When a big thing's progress is the sum of its parts, a mirror creates a trap: if one shared item sits under two parents, you must not count it twice. The research found no proven recipe for this — it's the single most important thing to figure out before building progress bars. It needs its own focused research before we rely on it.

Technical

Zero verified claims. Roll-up over a DAG must count each distinct node once per ancestor set (no double-count). Decide client-compute vs Postgres recursive CTE, and on-read vs incremental (triggers/materialized). Dedicated spike required — likely an on-read recursive CTE counting distinct descendants; optimize only if slow.

4 · Ordering & dragging

Ordering validatedNested drag: open
Plain English

The trick for "drop an item between two others" is the same one Figma uses, and it's the approach you already picked — so that's confirmed. Because of mirrors, an item's position is remembered per-spot (the same item can sit 2nd under one parent and 5th under another).

The drag-and-drop details for a nested, foldable list — and moving an item from one parent to another — weren't pinned down by the research and need a look before building.

Technical

Fractional indexing (Figma): positions as arbitrary-precision base-95 strings, not floats (floats exhaust precision ~50 between-inserts); insert = average of neighbors; collisions resolved server-side. Matches the existing fractional-indexing dep. Order is per-edge for mirrors.

Open: dnd-kit nested/tree drag + cross-parent move patterns not verified — research before building.

5 · The backend

Live-updates validatedRest: conventional
Plain English

The database can push changes to the screen instantly — perfect for a one-person tool. The known downsides of that feature only bite at big multi-user scale, which doesn't apply to you.

The remaining backend choices (how saves and instant-undo are wired) are standard, well-trodden territory — low risk, just not separately fact-checked here.

Technical

Supabase Realtime postgres_changes (INSERT/UPDATE/DELETE/*) over WebSocket — ideal single-user. Its limits (no DELETE filter, single-threaded, auth fan-out) are multi-user concerns, irrelevant here.

Not separately verified: Server Actions vs route handlers, RLS-for-single-user, optimistic-update patterns, ltree. Plan: Server Actions + optimistic UI (standard).

6 · One dataset, two views

Not validated
Plain English

You want the same items shown two ways — the foldable outline and a "what I'm doing now" board. The research didn't pin down the best way to keep both in sync. The safe principle: both are just different windows onto the same single set of items, never separate copies. Confirm the approach before building the second view.

Technical

Zero verified claims. Treat outline (TanStack) and kanban (dnd-kit) as projections of one row set (client selectors or SQL views) — single source of truth, no state divergence. Validate the projection pattern before the second view.

7 · Claude as the librarian

ValidatedGuardrails mandatory
Plain English

This is the deciding reason to build your own: the database connection lets Claude actually read and rewrite the data to keep it tidy — something the off-the-shelf apps won't allow. Confirmed.

The catch, stated bluntly: AI agents reliably make safety mistakes on databases unless you give them written rules — and the early version of this connection had no "are you sure?" on destructive changes. The good news: when the rules are written down as a checklist, the AI follows them correctly. So a guardrail layer — safe-by-default, an audit log, an undo, and a "confirm before deleting" — isn't polish, it's required.

Technical

Supabase MCP: execute_sql, apply_migration (WRITE default; read_only opt-in). Dev flow: iterate via SQL → formalize migrations after advisors; scoped to local/staging, not prod. Satisfies the read+write requirement.

Non-negotiable: agents skip RLS, ship RLS-bypassing views (missing security_invoker=true), hallucinate CLI cmds; destructive-op confirm was a roadmap item at the Apr-2025 launch. Build a guardrail/"tidy-rules" skill: read_only default, project_ref scoping, audit log + undo, idempotent fixes, human-confirm on destructive writes. Never user_metadata for authz; never ship service_role to the browser.

What to prove before committing

  1. Smooth scrolling for a nested, folding, mixed-height listlargely resolved by the Checkvist follow-up: stay flat (what your board already does) and it handles this natively up to a few thousand rows, dodging the scrolling-engine's height bugs. Only the tens-of-thousands case needs the heavy engine — measure your real row-count band before worrying about it.
  2. Counting "done" without double-counting mirrorsstill the #1 open risk. No proven recipe exists; needs its own research spike.

(Plus two smaller open items: stopping loops in the nesting, and nested drag-and-drop.)

Recommended stack

Next.js (App Router) · VercelSupabase / PostgresReact + TypeScript Flat DOM + content-visibility first · TanStack Virtual only at ~10k+TanStack Table (row logic)dnd-kit (drag)fractional-indexing (per-edge order) Adjacency list + edge table (mirrors)Recursive CTEs (traversal + roll-up) Supabase Realtime + Server ActionsSupabase MCP + guardrail skill (librarian)

Phased build plan

  1. Spike A — render: confirm flat rendering (your current content-visibility approach) stays smooth at your real row count (~2–5k). Only evaluate TanStack Virtual if you're credibly heading to ~10k+.
  2. Spike B — model: edge-table DAG + roll-up that doesn't double-count mirrors. Go / no-go on the data model.
  3. Core: schema (things / steps / edges / order), Server Actions, Realtime, the outline view.
  4. Librarian: guardrail skill (read-only default, audit log, undo, confirm-on-destructive) + deterministic tidy-rules.
  5. Second view: kanban "doing" board as a projection of the same rows.

What would change the direction

Nothing verified does. The only direction-relevant risk is the unproven nested-render smoothness (Spike A). If Spike A fails: keep the hand-rolled content-visibility approach, or reconsider a canvas grid (losing React-component cells + easy tree).
Sources (26 fetched · 25 claims verified)