Opinionated Next.js 16 Patterns
Implementation-pattern reference for Next.js 16 (App Router) codebases that want a single, opinionated architecture. Contains 50 rules across 8 categories, prioritised by execution-lifecycle cascade impact — authorization and (optional) tenant modeling first, then the request boundary, server fetching, mutations, client boundaries, architecture, and UI conventions.
The rules are backend-agnostic in principle but use Supabase as the concrete example. Each rule teaches the transferable idea (e.g. "authorize at the data layer", "read through a typed repository"); where the backend genuinely matters, a *Transferable:* note explains the pattern for other stores (Drizzle, Prisma). The structure is a Turbo monorepo with @app/* packages you own — built on canonical libraries (next-safe-action, @supabase/ssr, @tanstack/react-query, react-hook-form + zod, shadcn/ui, next-intl, pino), not a vendored starter kit.
When to Apply
Reach for these rules when:
- Writing new code — pages, layouts, server actions, route handlers,
proxy.ts, feature packages, client components, hooks, the data-access package, SQL/migrations, forms. - Reviewing a PR — authorization slips (privileged client without a guard, missing
'server-only'), waterfalls (sequential awaits, client-fetching server data), drift (hand-edited generated types, deep package imports, hardcoded i18n strings). - Refactoring — moving code between
apps/webandpackages/*, splitting actions and services, lifting'use client'boundaries, replacing raw queries with a typed data-access factory, swapping a backend behind the data-access package. - Designing a feature — choosing the right client (request-scoped vs privileged vs browser), deciding action vs route handler, planning the form/server-action contract, scoping a tenant (if multi-tenant).
- Onboarding — understanding why the codebase looks the way it does, with concrete, transferable examples.
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Authorization & Data-Layer Access | CRITICAL | auth- |
| 2 | Multi-Tenancy (optional, SaaS only) | CRITICAL | tenant- |
| 3 | Request Boundary (Proxy) | HIGH | proxy- |
| 4 | Server-Side Data Loading | HIGH | server- |
| 5 | Mutations: Actions & Route Handlers | HIGH | mutate- |
| 6 | Client/Server Boundaries | MEDIUM-HIGH | client- |
| 7 | Architecture & Services | MEDIUM | arch- |
| 8 | Forms & UI Conventions | MEDIUM | ui- |
Quick Reference
1. Authorization & Data-Layer Access (CRITICAL)
auth-use-standard-server-client— Use the request-scoped, auth-bound client; never default to the privileged one.auth-gate-admin-client— Authorize before constructing the service-role client.auth-trust-rls-no-duplicate-checks— Authorize once at the data layer; don't re-check in app code.auth-use-sql-policy-helpers— Centralize scoping predicates in reusable SQL policy helpers.auth-use-require-user— Centralize the auth gate in onerequireUser()helper.auth-server-only-imports— Mark privileged modules withimport 'server-only'.auth-mfa-in-middleware— Enforce MFA at theproxy.tsboundary, not per-page.
2. Multi-Tenancy (CRITICAL — optional, SaaS only)
tenant-accounts-as-tenant-root— One tenant-root table both personal and team workspaces reference.tenant-account-id-on-product-tables— Tenant key + index + scoping policy on every product table.tenant-slug-in-team-urls— Use a human-readable slug in team URLs, not the UUID.tenant-storage-paths-include-account-id— Namespace object-storage paths by tenant id.tenant-never-edit-generated-types— Treat generated DB types as build output; regenerate, never hand-edit.
3. Request Boundary: Proxy (HIGH)
proxy-single-pipeline— Compose the whole request pipeline in oneproxy.ts.proxy-redirect-auth-at-boundary— Perform auth redirects at the proxy, not in pages.proxy-url-pattern-matching— Match proxy routes withURLPattern, not string comparisons.proxy-set-correlation-id— Set a correlation ID at the request boundary.proxy-secure-headers-flagged— Apply strict CSP headers behind an environment flag.
4. Server-Side Data Loading (HIGH)
server-cache-workspace-loaders— Wrap per-request loaders withcache()from React.server-promise-all-parallel-loads— Load independent data in parallel withPromise.all.server-use-feature-api-factories— Read through a typed data-access factory, not rawfrom('table').server-redirect-on-missing-workspace— Redirect from the loader when workspace state is invalid.server-fetch-in-server-components— Fetch initial data in server components, not on the client.server-use-tables-generic-for-types— Use generated row types, not hand-written interfaces.server-services-receive-client— Services receive the data client as a constructor argument.
5. Mutations: Actions & Route Handlers (HIGH)
mutate-use-safe-action-clients— Route mutations through a typed action client you build on next-safe-action.mutate-zod-schema-separate-file— Put Zod schemas in their own*.schema.tsshared by client and server.mutate-thin-action-service-holds-logic— Keep the action thin; put business logic in a service.mutate-use-getlogger-not-console— Log through a structured logger you own, notconsole.log.mutate-revalidate-path-after-write— CallrevalidatePath()after a successful write.mutate-enhance-route-handler— Wrap route handlers in a typed handler that owns auth and validation.mutate-webhook-verify-signature— Webhook routes skip user-auth and verify the provider signature.
6. Client/Server Boundaries (MEDIUM-HIGH)
client-use-client-at-leaves— Mark'use client'at leaf components, not page roots.client-pass-server-data-as-props— Pass server data to client components as props, don't refetch.client-use-supabase-with-react-query— Pair a memoized browser client with TanStack Query for client reads.client-realtime-cleanup-subscription— Tear down any subscription or event source in theuseEffectreturn.client-use-action-hook— Call server actions withuseActionfromnext-safe-action/hooks.client-stable-query-keys— Use stable, hierarchical query keys.
7. Architecture & Services (MEDIUM)
arch-app-vs-packages-boundary— Reusable capabilities inpackages/, product-specific code inapps/web.arch-data-access-adapter— Confine the backend to one data-access package with a stable surface.arch-feature-package-layout— Feature packages follow acomponents / hooks / schema / serverlayout.arch-import-via-package-exports— Import via the packageexportsmap, never deep internal paths.arch-provider-gateway-pattern— Hide vendor SDKs behind a gateway interface.arch-policy-engine-for-business-rules— Model business rules in a policy layer you own, not inline conditionals.arch-config-driven-navigation— Define routes and navigation inconfig/, not hardcoded in components.
8. Forms & UI Conventions (MEDIUM)
ui-rhf-zod-no-generics— LetzodResolverinfer form types; don't adduseFormgenerics.ui-form-message-per-field— IncludeFormMessagefor every field.ui-kit-ui-package-imports— Import UI from your@app/uidesign-system surface, never internal paths.ui-semantic-tailwind-tokens— Use semantic Tailwind tokens, not hardcoded colors.ui-base-ui-render-not-aschild— Use Base UIrenderprop, not RadixasChild.ui-trans-for-display-text— Render display text through<Trans>oruseTranslations.
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions — Category structure, impact levels, lifecycle rationale.
- Rule template — Template for adding new rules.
- AGENTS.md — Auto-generated TOC for fast navigation.
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering by lifecycle impact |
| assets/templates/_template.md | Template for adding new rules |
| metadata.json | Version, organization, references |
Related Skills
base-ui-migrator— bulk-migrate RadixasChildpatterns to Base UIrenderprops.tailwind-refactor— refactor hardcoded colors to semantic tokens.react-optimise— performance optimisations for React components.nextjs-bundle-optimizer— bundle analysis and reduction for Next.js apps.