workflow Nextjs Server Client Boundary
Enforce the server/client module boundary in Next.js App Router projects. Use when creating client components, providers, hooks, or debugging "node: protocol" errors in Storybook/webpack/test environments. Prevents client components from importing server-side modules.
Install this skill
git submodule add https://github.com/modh-labs/playbook.git .playbookOne submodule installs all 17 skills. Reference .playbook/skills/nextjs-server-client-boundary/SKILL.md from your AGENTS.md.
Next.js Server/Client Boundary
When This Skill Activates
- Creating or modifying a
"use client"component - A component imports from a
repositories/,server/, or database module - Storybook/webpack build fails with
node:async_hooks,node:crypto, or similar - A provider component needs to trigger a server-side mutation
- Reviewing code for proper separation of concerns
The One Question
Does this
"use client"file import — directly or transitively — from any module that uses Node.js built-ins, database clients, or server-only APIs?
If yes, you have a boundary violation. Fix it before it breaks your tooling.
Why This Matters
Next.js App Router has two worlds:
SERVER WORLD CLIENT WORLD
───────────── ─────────────
Server Components Client Components ("use client")
Server Actions ("use server") React hooks (useState, useEffect)
Repositories (DB access) Browser APIs (localStorage, Intl)
Node.js built-ins Event handlers
The boundary between them is one-directional:
Client → can call → Server Actions (via RPC)
Client → can import → types (type-only imports are erased at build)
Client → CANNOT import → repositories, server-only modules
Server → can import → anything
Server → passes data → to Client via props
When a client component imports a server module, the bundler (webpack/Turbopack) tries to include that module in the browser bundle. Server modules import node:async_hooks, node:crypto, etc. — these don't exist in browsers. The result:
- Storybook:
UnhandledSchemeError: Reading from "node:async_hooks" is not handled - Tests: Module resolution failures in jsdom/vitest
- Production: Works in Next.js (it has special handling), but the code is architecturally broken
Decision Tree
Is this a "use client" file?
├─ NO → You can import anything. No boundary issue.
└─ YES → Check each import:
├─ From types/ or lib/ (no node: deps) → SAFE
├─ From actions/ ("use server") → SAFE (Next.js handles the RPC)
├─ From repositories/ → VIOLATION — fix it
├─ From server-only services/ → VIOLATION — fix it
└─ From @clerk/nextjs/server, @supabase/supabase-js (server) → VIOLATION
Core Rules
Rule 1: Client components import from actions/, never repositories/
// ❌ VIOLATION: Client importing repository
"use client"
import { updateUser } from "@/repositories/users.repository";
// This pulls in Supabase → node:async_hooks → build crash
// ✅ CORRECT: Client importing server action
"use client"
import { updateUserAction } from "@/actions/user.actions";
// Server action is called via RPC — no server code in client bundle
Rule 2: Providers accept data as props, don't fetch internally
// ❌ VIOLATION: Provider imports repository
"use client"
import { getCurrentUser } from "@/repositories/users.repository";
export function UserProvider({ children }) {
useEffect(() => { getCurrentUser().then(setUser) }, []);
// ...
}
// ✅ CORRECT: Server component fetches, provider receives props
// In page.tsx (server component):
const user = await getCurrentUser();
return <UserProvider user={user}>{children}</UserProvider>;
// In UserProvider.tsx (client component):
"use client"
export function UserProvider({ user, children }) {
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}
Rule 3: If a provider MUST trigger a server mutation, use a server action
// ❌ VIOLATION: Provider calls repository function
"use client"
import { updateTimezone } from "@/repositories/users.repository";
useEffect(() => {
updateTimezone(detected); // Direct repository call from client
}, []);
// ✅ CORRECT: Provider calls server action
"use client"
import { persistTimezone } from "@/actions/timezone.actions";
useEffect(() => {
persistTimezone(detected); // Server action — Next.js handles the RPC
}, []);
Rule 4: Type-only imports from server modules are safe
// ✅ SAFE: type-only imports are erased at build time
import type { Call } from "@/repositories/calls.repository";
// ⚠️ BUT PREFER: importing from a dedicated types file
import type { Call } from "@/types/calls.types";
// This is cleaner and makes the boundary explicit
Implementation Pattern
When you need to add server functionality to a client component:
Step 1: Create a server action
// actions/timezone.actions.ts
"use server"
import { updateTimezone } from "@/repositories/users.repository";
export async function persistTimezone(tz: string): Promise<void> {
await updateTimezone(tz);
}
Step 2: Import the action in the client component
// providers/TimezoneProvider.tsx
"use client"
import { persistTimezone } from "@/actions/timezone.actions";
// NOT: import { updateTimezone } from "@/repositories/users.repository"
Step 3: The action works everywhere
- Next.js: Calls the action via RPC (automatic)
- Storybook: Mocked via webpack alias
- Tests: Mocked via
vi.mock()
Anti-Patterns
1. "It works in Next.js, so it's fine"
Next.js has special handling that makes boundary violations work in production. But they break everywhere else. Design for portability.
2. "I'll just mock it in Storybook"
If you need a webpack mock to render a component, the component has a boundary violation. Fix the component, don't add mocks. Mocks should only be needed for third-party services (Clerk, Sentry), not your own code.
3. "It's just a type import"
import type { X } is safe. But import { X } (even if X is only used as a type) pulls in the module. Use explicit type imports.
4. Dual-mode components
// ❌ Component that sometimes fetches, sometimes receives props
function Details({ id, data }: { id?: string; data?: Data }) {
if (data) return <Render data={data} />;
// Falls back to fetching — this is two components pretending to be one
const fetched = useFetch(id);
return <Render data={fetched} />;
}
Split into a data-fetching parent and a pure rendering child.
Audit Checklist
Run this against any "use client" file:
- No imports from
repositories/(excepttypeimports) - No imports from server-only
services/ - No imports from modules that use
node:built-ins - Props interface is exported
- Component receives data via props (not internal fetching)
- Any server mutations use server actions, not repository functions
- Component renders correctly in Storybook without custom webpack mocks
- Provider components accept data as props from server parent
The Import Chain
┌─────────────────────────┐
│ Server Components │
│ (page.tsx, layout) │
└──────────┬──────────────┘
│ fetches data via
▼
┌─────────────────────────┐
│ Repositories │
│ (database access) │
└──────────┬──────────────┘
│ returns typed data
▼
┌─────────────────────────┐
props ──▶ │ Client Components │ ◀── "use client"
│ (interactive UI) │
└──────────┬──────────────┘
│ mutations via
▼
┌─────────────────────────┐
│ Server Actions │
│ ("use server") │
└──────────┬──────────────┘
│ calls
▼
┌─────────────────────────┐
│ Repositories │
└─────────────────────────┘
Data flows down (props), mutations flow through server actions. Client never touches repositories directly.
Related Skills
Background Job Right Sizing
Choose the LIGHTEST durable mechanism a background/async/side-effect job actually needs, instead of reaching for a full workflow engine by reflex. A four-rung ladder — in-process fire-and-forget → durable queue → workflow engine → dedicated orchestrator — routed by four axes: durability, step count, concurrency shape, and cross-app/bus membership. Activates when adding any background job, emitting a side-effect from a request handler, choosing between Vercel Queues / Workflows / Inngest / Temporal / SQS, or auditing an existing job fleet for over-engineering ("do we still need the orchestrator for this?").
Workflow / ProcessworkflowBug Cleanup Triage
Framework for backlog bug cleanup sessions. Triage is three sequential activities — Linear hygiene, root-cause investigation, and code fixing — that must be executed in order as a hard phase gate. Includes git log pre-flight, Sentry module-tag verification, and umbrella breakdown auto-detection. Use when planning to clean up N bugs from a backlog, before dispatching research agents, or when a ticket has been In Progress for weeks without shipping.
Workflow / ProcessworkflowCI Pipeline Standards
Enforce CI pipeline conventions. Use when adding CI checks, modifying GitHub Actions workflows, or discussing CI vs deployment. Prevents deployment steps in CI and ensures the extensible step pattern is followed.
Workflow / Process