--- name: Server Actions and Auth Guard Order description: Zod validation before data touch, auth guard sequence, authz inside the action, security headers. category: code applicable_phases: [code_gen] applicable_stacks: [nextjs-clerk-supabase] version: 2 --- Conventions for server actions, mutations, and auth guard sequencing. Source: guide-code.md §6, guide-arch.md. ## Auth guard order (source: guide-arch.md) Every route handler and server action must follow this exact sequence — **auth before validation**, not the other way around. Checking auth first means an unauthenticated caller never sees validation-error details or triggers any parsing/business-logic work; it's also what the `resolve-account-route-handler` and `api-error-handler` skills already establish (`resolveAccount()` as the first line), so this skill previously conflicted with those two. 1. **Auth check** — verify the caller is authenticated; return 401 if not. 2. **Schema validate** — parse input with zod; return 400 on failure. 3. **Authz check** — verify the caller is authorized to act on the resource; return 403 if not. 4. **Business logic** — only after all three guards pass. ```typescript // Good — correct guard sequence export async function PUT(req: Request, { params }: { params: { id: string } }) { // 1. Auth check const account = await resolveAccount(); if (!account) return NextResponse.json({ error: "unauthorized" }, { status: 401 }); // 2. Schema validate const body = await req.json(); const parsed = updateFlowSchema.safeParse(body); if (!parsed.success) return NextResponse.json({ error: "validation" }, { status: 400 }); // 3. Authz check const existing = await flowsRepository.getById(params.id, account.id); if (!existing) return NextResponse.json({ error: "not_found" }, { status: 404 }); // 4. Business logic await flowsRepository.update(params.id, account.id, parsed.data); return NextResponse.json({ ok: true }); } ``` ## Server actions / mutations (source: guide-code.md §6) - Check auth **before** validating input (see "Auth guard order" above — auth first, not validate first). - Validate all input with zod before touching data. - Perform authz checks inside the action/handler — **not** only in UI. - Return typed success/error shapes; avoid leaking raw DB errors. - Never trust client-side auth state for authorization decisions. ```typescript // Good "use server"; export async function updateFlow(id: string, input: unknown) { const account = await resolveAccount(); if (!account) return { error: "Unauthorized" }; const parsed = updateFlowSchema.safeParse(input); if (!parsed.success) return { error: "Invalid input" }; await flowsRepository.update(id, account.id, parsed.data); return { ok: true }; } // Bad — no zod, no authz check, trusting the caller ("use server"); export async function updateFlow(id: string, data: FlowData) { await db.update(flows).set(data).where(eq(flows.id, id)); } ``` ## RLS + app-layer filtering (source: guide-code.md §6, guide-db.md) - Whether RLS is the primary boundary or the app-layer `accountId` filter is depends on whether your product's DB connection is JWT-scoped per request, or a single shared pool that authenticates as one fixed role — see `skills/db/rls-policies` for the full explanation. Don't assume RLS is doing the work without checking which model your product uses. - Both layers should be present regardless of which one is primary for your product. - Never disable RLS as a workaround; fix the policy instead. ## Security headers (source: guide-code.md §10) Security headers are configured in `next.config.mjs` via the `headers()` function: - `Content-Security-Policy` — restrict script/style/connect sources. - `Strict-Transport-Security` — HSTS with preload. - `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`. When adding a new external service (analytics, CDN, etc.), update `connect-src` and any other relevant CSP directives. ## Performance defaults (source: guide-code.md §13) - Prefer Server Components and streaming; minimize client component boundaries. - Use `next/image` for all images — never raw `` with external URLs. - Cache reads intentionally; revalidate explicitly after mutations. ```tsx // Good import Image from "next/image"; Logo // Bad Logo ```