fix-admin-table-rsc-boundaryrun.mdlane/output/notes.md/users and /finances return an error instead of the list · expected: both pages
render their AdminTablepackages/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 boundarypackages/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 thereapps/help/app/changelog/2026-08-03-fix-admin-table-rsc-boundary/) — two
pages were unreachable for a week, which is user-visibleConfirmed 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
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:
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.ts — admin-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.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 clientdist/index.js — still starts with "use client";, so the client barrel is unchangeddist/server.d.ts — emits AdminTable, AdminTableColumn, AdminTablePaginationThe Vercel preview is the end-to-end proof; hitting /users on it is the operator check.
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.
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.