| name | nextjs-server-actions |
| description | Use this skill when you implement or review Next.js Server Actions or server-side data fetching that touches tenant/workspace data (auth provider + ORM + i18n, tenant-safe). |
Server Actions & Data Fetch Skill
Use this skill whenever you add or review server actions, server components fetching data, or route handlers that touch tenant/workspace data. Optimized for Next.js App Router + React + TypeScript + an auth provider + an ORM.
Core rules
- Derive tenant/workspace from the server-side session; never trust client-provided IDs. Every query must filter by your tenant/workspace identifier (e.g.,
workspace_id).
- Server Actions only for internal mutations. Route Handlers only for webhooks/external. Keep secrets server-side.
- Money stored as integer cents + currency code. Time stored in UTC.
- i18n: URLs are
/{locale}/... (en|de). Use next-intl for formatting.
Required steps for any mutation
- Add "use server" at top.
- Auth/tenant check: derive
{ workspace/tenant, userId } via your server-side auth/session + tenant guard helper.
- Validate inputs (zod or guards). Reject unexpected fields; prefer typed inputs.
- Perform Drizzle query with
workspace.id in where clause. Never query unscoped tables.
- Revalidate relevant paths or tags (e.g.,
revalidatePath('/[locale]/(dashboard)/clients')).
- Return a shared
ActionResult-style union (ok/error) if your project uses one. Use stable error identity:
- set
code using your project’s stable error codes (don’t hardcode strings)
- prefer
messageKey for UI localization; keep message as a safe fallback
- do not throw raw errors across the RSC boundary
Data fetching (Server Components)
- Default to server components. Use
cache() for shared read helpers.
- Always include
workspace_id filter. Derive workspace via requireWorkspaceCached().
- For dynamic data, use
fetch(..., { cache: "no-store" }). For ISR/tagged data, use next: { revalidate, tags }.
- Do not import server-only modules into client components.
Route Handlers (webhooks/external)
- Prefer a non-React cached tenant/workspace guard in Route Handlers.
- Still scope every DB query/mutation by
workspace_id.
PDF and snapshots
- Finalized invoices must render from
invoice_snapshots; drafts render from current state.
- Finalization must create snapshot (seller/client/items/totals/locale/template) and lock edits.
Auth provider & tenancy
- If you are in a “solo” tenancy model: derive a single personal tenant/workspace and guard all protected routes/actions.
- Don’t rely on client-provided org/tenant identifiers for tenancy.
Patterns
-
Helper example:
import { cache } from "react";
import { db } from "@/db";
import { clients } from "@/db/schema";
import { eq } from "drizzle-orm";
export const getClientsByWorkspace = cache(async (workspaceId: string) => {
return db.query.clients.findMany({
where: eq(clients.workspaceId, workspaceId),
orderBy: (c, { desc }) => desc(c.createdAt),
});
});
Edge cases checklist
- Prevent cross-tenant access: every query filters
workspace_id.
- Money: never use floats; keep cents integer.
- Time: UTC storage; convert at display.
- Locale: ensure path includes locale; format currency/dates with next-intl.
- Do not expose sequential IDs; use UUIDs in routes.
Revalidation tips
- Use
revalidatePath for route segments, revalidateTag for tagged fetches.
- Revalidate after create/update/delete of clients, projects, time entries, invoices, expenses.
Cost & performance notes (Vercel-friendly)
- Prefer one Server Action per user intent (avoid multi-action waterfalls).
- Prefer narrow invalidation:
revalidateTag() is usually cheaper than broad revalidatePath().
- Avoid invalidating large route subtrees “just to be safe”.
- Avoid sequential independent reads in a Server Component/Action—use
Promise.all.
- Use React
cache() for shared read helpers (request-level de-dupe) and ensure workspaceId is part of the inputs.
Deep dive reference: nextjs-cost-performance skill.
Testing
- Add integration/unit tests for tenant isolation and snapshot immutability.
- Type-check with
bun run lint and tsc --noEmit.
References
- Product spec: your PRD/spec doc (if present)
- Schema: your schema module(s)
- Tenant/workspace helper: your server-side tenant guard module
- i18n routing: your i18n routing module (if present)