Skip to Content

← All archived runs

Run: fix-admin-table-rsc-boundary

run.md

Run: fix-admin-table-rsc-boundary

  • lane: bug
  • branch: fix-admin-table-rsc-boundary
  • pr: #748

lane/output/notes.md

Bug: fix-admin-table-rsc-boundary

  • observed: /users and /finances return an error instead of the list · expected: both pages render their AdminTable
  • cause: packages/ui/tsup.config.ts stamps "use client" as a build banner on the whole component barrel, so AdminTable ships as a client component — but all 11 of its call sites are React Server Components passing function props (render, rowKey, rowHref, buildPageQuery), and functions cannot cross a server/client boundary
  • fix: packages/ui: AdminTable moves to a third, banner-free build unit exported as @sustentus/ui/server; the 11 web call sites (+ the Storybook story) import it from there
  • changelog: entry added (apps/help/app/changelog/2026-08-03-fix-admin-table-rsc-boundary/) — two pages were unreachable for a week, which is user-visible

Reproduction

Confirmed from Vercel runtime telemetry on the web project (not inferred from the code):

Error: Functions cannot be passed directly to Client Components unless you explicitly
expose it by marking it with "use server".
count=33  users=1  routes=/users.rsc, /finances.rsc
first=2026-07-27T10:25:58Z  last=2026-08-03T15:04:20Z

  {key: "email", header: "Email", render: function render}
                                          ^^^^^^^^^^^^^^^
  {columns: ..., rows: ..., rowKey: function rowKey, rowHref: ..., buildPageQuery: ...}
                                    ^^^^^^^^^^^^^^^

The named props are exactly AdminTable's signature, so the failing component is identified by the error itself rather than by deduction. /users and /finances are simply the two routes being visited — all 11 call sites are server components, so all 11 were broken:

users-table · skills-list · services-list · projects-list · products-list · integrations-list
quotes-list · invoices-list · customer-csat-table · user-management-card · industry-list

Why this fix, and why it is safe

AdminTable has no hooks, no state and no event handlers — it is purely presentational. It was only ever a client component because the banner is applied to the entire barrel indiscriminately. The same is true of the two primitives it renders, base/table.tsx and base/typography.tsx.

So rather than rewrite 11 call sites to precompute cells (a much larger diff that would also make the component harder to use), the fix removes the incorrect classification:

  • new packages/ui/src/server.ts — a server-safe barrel, built without the banner.
  • packages/ui/tsup.config.ts — a third build unit for it, alongside the existing banner-free lib/utils unit. Precedent, not a new pattern.
  • base/table.tsx — dropped its source-level "use client". It was a shadcn copy-paste default; the file uses no client-only feature. Left in place it would have leaked into the server bundle and silently undone the fix. The client barrel is unaffected — the banner re-applies it there.
  • compound/index.tsadmin-table is deliberately no longer re-exported from the client barrel. Exporting from both would leave a "use client" copy one import away, and the next server-component caller would hit the identical error with no clue why.

Verified before push

Built packages/ui and inspected the output directly, because CI checks that the build succeeds, not where the directive lands:

  • dist/server.js — zero occurrences of use client
  • dist/index.js — still starts with "use client";, so the client barrel is unchanged
  • dist/server.d.ts — emits AdminTable, AdminTableColumn, AdminTablePagination

The Vercel preview is the end-to-end proof; hitting /users on it is the operator check.

Incidental — a build race this fix forced into the open

clean: true on the first tsup unit raced the other units' .d.ts emits: all three type files were written, then two were deleted. That is why dist/lib/utils.d.ts has been missing on main (the existing @sustentus/ui/utils subpath ships without types today, masked by typescript: { ignoreBuildErrors: true } in apps/web/next.config.ts).

Reproduced deterministically across three clean builds. Fixed by dropping clean from all units and wiping dist once in the build script. Not a drive-by: without it dist/server.d.ts would not survive its own build and pnpm typecheck would fail on the new import.

Not fixed here

The banner is still applied barrel-wide, so any other presentational component that needs to take a function prop will hit this same error and need the same treatment. Making the boundary explicit per-file across packages/ui is a real piece of work with a design decision in it — that belongs in /pipeline scope, not in a bug fix.