For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

Server / client boundaries

Anything passed into a Client Component is public. Use package ./server and ./client entry points so secrets and DB clients never reach the browser.

The most important Next.js rule still applies in TurboStarter: whatever you pass into a Client Component is serialized and sent to the browser.

That includes nested objects. If a server component builds a config with an API key and hands the whole object to a 'use client' child, the key leaks - even if the child never renders it.

Never pass secrets into client components

Unsafe - config contains a server secret
async function ServerPage() {
  const config = {
    apiKey: process.env.STRIPE_SECRET_KEY,
    storeId: process.env.NEXT_PUBLIC_URL,
  };

  const data = await loadBillingData(config);

  // Leaks STRIPE_SECRET_KEY to the client bundle / RSC payload
  return <BillingClient data={data} config={config} />;
}

Keep secrets inside server-only modules. Return only the fields the UI needs:

Safe - secrets stay in the server module
import { getBillingSummary } from "~/modules/billing/lib/server";

async function ServerPage() {
  const summary = await getBillingSummary();

  return <BillingClient summary={summary} />;
}

Props are a public API

Treat every prop on a Client Component as public. Prefer narrow DTOs over passing database rows, full session objects, or provider configs.

Use the right package entry points

Shared packages expose separate entry points for server and client code. Import from those - do not deep-import into files that pull in secrets or Node-only SDKs.

PackageServer / privilegedClient-safe
@workspace/auth@workspace/auth/server@workspace/auth/client/web
@workspace/db@workspace/db/serverschema types via @workspace/db/schema when needed
@workspace/billing-web@workspace/billing-web/serverschemas via @workspace/billing-web/schema
@workspace/storage@workspace/storage/server- (call the API instead)
@workspace/i18n@workspace/i18n/server@workspace/i18n

Example from the web app: session helpers import the auth server entry, never the Better Auth instance into client code:

apps/web/src/lib/auth/server.ts
import { auth } from "@workspace/auth/server";

export const getSession = cache(async () => {
  const data = await auth.api.getSession({
    headers: await getHeaders(),
  });

  return {
    session: data?.session ?? null,
    user: data?.user ?? null,
  };
});

On the client, use the dedicated client builders:

apps/web/src/lib/auth/client.ts
import { createAuthClient } from "@workspace/auth/client/web";

Do not mix server and client in one barrel

Avoid a single index.ts that re-exports both a server SDK and a Client Component. That pattern makes it easy for a client import to accidentally pull server code into the browser bundle.

TurboStarter already splits exports in package.json:

packages/auth/package.json
{
  "exports": {
    ".": "./src/index.ts",
    "./env": "./src/env.ts",
    "./server": "./src/server.ts",
    "./client/*": "./src/client/*.ts"
  }
}

When you add a new shared package:

  1. Put privileged code behind ./server (or similarly named) exports
  2. Put browser-safe helpers behind ./client (or the package root if it is truly isomorphic)
  3. Keep env presets in ./env and only import them from server code
  4. Never re-export server modules from a client entry

Server Components vs Client Components

Default to Server Components. Add 'use client' only when you need interactivity, browser APIs, or client-only libraries.

When a layout or page is a Server Component:

  • Fetch with getSession() from ~/lib/auth/server
  • Call Hono through ~/lib/api/server so cookies and headers stay on the server
  • Pass serializable, non-sensitive props down to client islands

Dashboard layouts already follow this pattern - they resolve the session on the server and redirect before rendering protected UI:

apps/web/src/app/[locale]/dashboard/(user)/layout.tsx
const { user } = await getSession();

if (!user) {
  return redirect(pathsConfig.auth.login);
}

UI gates are useful UX, but they are not security. Always enforce the same rules again in the API.

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter