RSC Data Access Without Leaking Secrets Into the Client Bundle
Patterns for fetching internal APIs from React Server Components without shipping tokens or row-level data to the browser.
Kabir Hossain
Founder, Chainweb Solutions
RSC Data Access Without Leaking Secrets Into the Client Bundle
RSC secret leakage occurs when server-fetched data reaches the client through the flight payload. In Next.js App Router projects, this happens during serialization when an object containing restricted fields is passed as a prop or closed over by a Client Component.
Flight payload reveals what reaches the client
React Server Components emit a stream with MIME type text/x-component. The stream encodes rendered fragments and any serializable values referenced by Client Components. A single object containing an apiKey or internalFlag field will be included unless the reference is blocked before the boundary.
Enforcing server-only boundaries
Import the server-only package at the top of any module that performs database access or holds secrets. This produces a build-time error if the module is imported from a Client Component.
// data/user.ts
import 'server-only';
import { db } from './db';
export async function getUser(id: string) {
return db.query('SELECT id, name, token FROM users WHERE id = ?', [id]);
}
The package adds no runtime cost. It only checks the import graph during next build.
Tradeoff between direct queries and data access layers
Placing queries directly inside Server Components reduces round trips and avoids an intermediate API. The same pattern increases the surface where an entire result row can be passed to a Client Component.
A data access layer centralizes queries behind narrow functions. Each function can wrap its result with React cache so repeated calls within one request return the memoized value without additional database work:
import { cache } from 'react';
import 'server-only';
export const getUser = cache(async (id: string) => {
const row = await db.query(/* ... */);
return { id: row.id, name: row.name }; // projection only
});
The layer adds one file per domain and requires callers to accept the returned shape. In exchange, the projection step is enforced at a single location rather than repeated at every call site.
Taint tracking at the data access boundary
Use experimental_taintObjectReference to mark objects that must never reach the client. Call the function immediately after the query returns:
import { experimental_taintObjectReference } from 'react';
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
experimental_taintObjectReference(
'User record contains internal token',
user
);
return user;
Any later attempt to pass user (or a property derived from it) to a Client Component produces a runtime error during render. Apply the call inside the data access function so every consumer inherits the protection.
Failure mode and mitigation
A Server Component fetches a full user row and spreads it into a Client Component that only needs display fields. The token column travels in the flight payload.
Mitigation: require every data access function to return a projected object containing only the fields listed in a colocated type. Add the projection step to the pull-request checklist for any file under app/ or components/.
Automating flight payload audits in CI
Intercept the RSC response in an end-to-end test and assert on its content. The example below uses Playwright to capture the text/x-component stream and fail if a disallowed key appears:
test('no secret fields in flight payload', async ({ page }) => {
const response = await page.waitForResponse(
(r) => r.headers()['content-type']?.includes('text/x-component')
);
const body = await response.text();
expect(body).not.toMatch(/"token"|"apiKey"/);
});
Run the test on every pull request that modifies Server Components or data access modules. The check executes against the production build, not development mode.
Custom PWA
Need an app your customers or staff will actually install?
We ship installable PWAs around one core flow, on the phones people already carry. Start with a quick build and grow it from there.
Related articles
Continue with articles on similar topics.