Invite-only.
Architecture

RSC + streaming migration plan

The dashboard is fast after Phase 3 shipped (Tier A/B/C plus C3 memoization + C1 virtualization), but cold-cache first visits still eat a client-side fetch waterfall. Migrating to React Server Components + streaming cuts that by ~60% on cold loads. This is the plan a future contributor (agent or human) can execute against without re-deriving the decisions.

Why this matters

Current /[org]/[workspace] on cold cache:

  1. HTML + JS bundle: 100-300ms
  2. Hydrate: 50-200ms
  3. /api/me +/api/workspaces/[slug] +/api/rows +/api/members: 200-400ms wall (parallelized but network-bound)
  4. React re-render with data: 50-100ms

Total: ~1-2s to first interaction. Industry- standard for an SPA, but the bottom of the published budget in /docs/performance.

With RSC, steps 1 + 3 collapse into the initial HTML response. The server fetches via Prisma directly and streams rendered HTML as data resolves. Target: ~400-800ms to first interaction on cold cache.

Scope — what migrates, what stays

Not everything becomes a server component. The rules:

  • Server component: pure read of DB or computed value. No React state, no hooks, no event handlers. Streams data as it resolves.
  • Client component ("use client"): interactive. Selection, editors, drag-and-drop, TipTap, MCP-connected UIs. Hydrates on the client.

Phase-1: workspace detail page shell (server)

The /[org]/[workspace]/page.tsx route becomes an async server component that fetches workspace + members + initial rows from Prisma directly (no HTTP round-trip) and passes them as props to a new client component<WorkspaceClient>. All the interactive state (selection, filters, mode, SSE stream hook, useDocPersistence) moves into the client component.

Files touched:

src/app/(dashboard)/[org]/[workspace]/page.tsx         ← becomes async server component
src/app/(dashboard)/[org]/[workspace]/WorkspaceClient.tsx  ← new client component (current page.tsx body)
src/lib/workspace-loader.ts                            ← new: server-side fetchers (Prisma, not fetch)

Phase-2: rows + doc streaming

The shell renders immediately with workspace metadata. Rows and doc body stream in via Suspense boundaries so the UI is interactive before all data lands:

<WorkspaceShell workspace={workspace}>
  <Suspense fallback={<TableSkeleton />}>
    <RowsLoader slug={slug} />
  </Suspense>
</WorkspaceShell>

Server emits the shell HTML + a Suspense boundary; as Prisma resolves rows, the server streams them down as RSC payload inserts. Client never makes a /rows fetch.

Phase-3: stays client-only

These do NOT migrate — they're fundamentally interactive and RSC offers zero benefit:

  • TableView (selection, drag-fill, clipboard, virtualization)
  • DocView (TipTap editor)
  • ShareModal, InviteAgentModal, FileTicketFab (modals)
  • useWorkspaceStream (SSE subscribe)
  • useDocPersistence (debounced writes)

Migration phases

Phase 1 — workspace shell (week 1)

  • Split page.tsx into server page + WorkspaceClient
  • Create src/lib/workspace-loader.tswith loadWorkspace(slug, principal), loadMembers(id), loadRows(id, limit). Each is a direct Prisma call. ReusesrequireWorkspaceAccess.
  • Server page authenticates via session cookie (using a server-side equivalent ofrequireAuth), fetches data, passes to client.
  • Client component is the current page.tsx, minus the loadAll effect and useState initial fetch. The client just hydrates with server-provided data.
  • Paint budget spec updated in tests/nfr/workspace-paint-budget.spec.ts— tighten FCP <700ms, LCP <1200ms for workspace detail.

Phase 2 — streaming rows + doc (week 2)

  • Wrap row list + doc body in Suspense boundaries so the workspace shell paints before data resolves.
  • RowsLoader is a new async server component that awaits Prisma then passes to the TableView client. Same forDocLoader.
  • Skeletons: reuse existing TableSkeleton/DocSkeleton components (to be extracted — they exist inline today).

Phase 3 — cleanup (week 2 end)

  • Delete /api/workspaces/[slug] + /api/workspaces/[slug]/rows +/api/workspaces/[slug]/members only if nothing else consumes them. MCP still needs them, so likely keep.
  • Delete loadAll + its effect from the client component. The client still handles mutations (POST/PATCH/DELETE row) via fetch — those paths don't change.
  • Update /docs/performance with new cold-load numbers.

Risks + mitigations

  • Hydration mismatch — if server render and client render diverge (theme state, locale, feature flags), React throws a console warning and Dock renders flicker. Mitigation: theme state moved into cookie so server can read it; never computed from window.
  • Prisma edge vs node runtime — Next 16 App Router server components need Node runtime for Prisma (not edge). Each migrated route declaresexport const runtime = "nodejs". Neon supports both; no connection-string changes.
  • Auth in server components — noreq object available, butcookies() from Next gives access to the session cookie. Wrap inrequireAuthServer() helper.
  • Streaming during write — if a user navigates to a workspace while another tab is actively writing to it, the server render races the SSE update. Client reconciles via the existing useWorkspaceStream forward cursor, so no lost updates — just a ~1.5s window where the server-rendered version might lag by one event. Acceptable.

Rollback plan

Each phase's PR is an independent revert target. The server page can coexist with the client component'sloadAll effect during migration (server passes props + client overrides with its fetch result — last-write-wins). If a phase goes red on prod smoke, Prod Rollback workflow fires automatically via the paint-budget CI gate.

Not in scope

  • Marketing pages (/, /pricing, /docs/*) — already SSR.
  • Search palette (⌘K) — stays client-side; server-side doesn't help with an interactive search.
  • MCP surface — untouched.
  • Database schema — zero changes.

Success criteria

  • Cold-cache FCP on /[org]/[workspace]: <800ms (down from ~1.5s)
  • Cold-cache LCP: <1200ms (down from ~2.5s)
  • Per-PR paint-budget spec enforces the new ceiling; CI regression if someone re-introduces a fetch waterfall.
  • Mutations (add row, update cell, etc.) continue to work unchanged. MCP surface continues to work unchanged.

Related: Performance reference · MCP server · REST reference