Skip to content
VerifiedUpdated 2026-06-27
On this page

The rest of the handbook explains the reasoning. This page is the reasoning's conclusions: the decisions themselves, stripped of the argument, so you can see what I actually build with at a glance. It is the fastest answer to "what does this person pick, and what do they refuse to do?"

Every row is a real call made somewhere in the handbook. The Read the case link under each one jumps to the page that argues for it, so when a default does not fit your project you can go read why it exists before you override it.

How firm each call is

Not every decision carries the same weight. Each row is tagged so you know how hard to hold the line:

Non-negotiable: a rule I treat as absolute, usually because breaking it is a security hole, a data leak, or a bug that only shows up in production. Override these only if you genuinely understand the trade you are making.

Strong default: the choice I reach for unless a project gives me a specific reason not to. Deviating is fine; deviating without a reason is how a codebase drifts.

This is also the page to drop into your AI when you want it to adopt the whole methodology's positions without the long-form prose. Use the Copy for AI button at the top: the markdown version of this page is the decision set as a flat, agent-readable list.

The decisions

Foundations

Language

Non-negotiable

TypeScript, scaffolded with --typescript

Types pay for themselves the first time a refactor would silently break call sites.

Read the case

Router and rendering

Non-negotiable

App Router with Server Components by default; fetch on the server, ship the browser only what it needs

It is the single biggest lever for shipping less JavaScript to the browser.

Read the case

Client boundary

Non-negotiable

Push "use client" down to the smallest interactive leaf; never mark a whole page client for one button

Once a file carries the directive, every module it imports and child it renders joins the client bundle.

Read the case

Route files

Non-negotiable

Keep page.tsx, layout.tsx, and route.ts thin; data access, validation, and integrations live in lib/

A route's job is to wire data to UI, not own fetching, parsing, or validation.

Read the case

App layering

Non-negotiable

Four layers: thin routes (app/), server-only data access (lib/data/), Zod validation (lib/validation/), isolated integrations (lib/integrations/), each talking only to the layer below

Every kind of logic gets one home and dependencies stay one-directional, so the skeleton scales from a weekend to years.

Read the case

Vendor SDKs

Non-negotiable

Never import a database client or vendor SDK inside a component or route handler; one module per external service

It couples UI to infrastructure and makes the data layer impossible to reuse or swap.

Read the case

Next.js 16 async APIs

Non-negotiable

Treat params, searchParams, headers(), and cookies() as Promises and always await them

In Next.js 16 they are no longer plain objects, so synchronous access fails typechecking and throws at runtime.

Read the case

Data at the boundary

Non-negotiable

Validate and type external data into a named type once at the edge; never pass res.json() or any any-typed value into the tree

res.json() is typed any and any spreads, so untyped data at the boundary becomes untyped data everywhere.

Read the case

Mutations

Strong default

Writes go through a Server Action that validates input with a Zod schema before the data layer sees it

Invalid input never reaches the database.

Read the case

Linting

Strong default

ESLint wired in from the first commit, enforcing no-unused-vars and no stray console

Problems surface while they are still cheap to fix and the conventions cannot quietly erode.

Read the case

Dependency versioning

Strong default

Pin the Next.js version (e.g. next@16.x) in real projects

A pinned version will not silently jump a major and break under you later.

Read the case

When to Reach Elsewhere

Default framework

Non-negotiable

Reach for Next.js whenever the deliverable is HTML in a browser that also needs server-side logic

That shape covers the large majority of builds and reaching for it there is never wrong.

Read the case

Mismatched workloads

Non-negotiable

Never put persistent sockets, on-device native, long-running compute, or static-only content inside Next.js; run the right tool beside it over an API

Bolting those onto a Next.js app produces a fragile hybrid.

Read the case

Many concurrent websockets

Strong default

Use a dedicated socket server (Phoenix Channels, Go) or a managed service like Ably or Pusher

Request/response handlers cannot hold thousands of long-lived connections and serverless bills brutally for them.

Read the case

Heavy ML or batch jobs

Strong default

Use a Python service behind a queue-and-worker model; Next.js takes the upload, enqueues, and shows status

Serverless has time limits and cold starts while the ML tooling and time budget live in Python.

Read the case

Architecture granularity

Strong default

Decide per surface, not per project, and run the fit checklist before scaffolding

Most real products mix tools and the expensive failure is sunk-cost commitment after the architecture sets.

Read the case

Design and Branding

Type roles

Non-negotiable

Exactly three roles (display, body, mono); never set a condensed display face on body copy

Three roles create hierarchy without a ransom-note page, and condensed faces wreck paragraph readability.

Read the case

Font loaders

Non-negotiable

Call each next/font loader exactly once in app/fonts.ts and import the object everywhere, declaring subsets: ["latin"]

Each loader call hosts a separate instance, so calling it twice ships the same font twice and breaks preloading.

Read the case

Accent color

Non-negotiable

Pick exactly ONE accent reserved for emphasis; never add a second to balance it

The accent earns its weight precisely because it is rare, and a second accent halves the impact of both.

Read the case

Accent on prose

Non-negotiable

Never set body copy in the accent color; keep it on affordances only

The accent marks where to act while reading wants a high-contrast neutral, not a saturated hue that fails contrast.

Read the case

Base colors

Non-negotiable

Near-black like #0b0b0c for dark and off-white like #fafafa for light, never pure #000 or #fff

Pure black crushes shadows and vibrates against white, leaving no headroom for a layered surface system.

Read the case

Body contrast

Non-negotiable

Body text must clear WCAG AA 4.5:1 (large text 3:1) on the actual surface it sits on

Contrast is the step people skip and it decides whether the product is usable.

Read the case

Token architecture

Non-negotiable

Two layers, a raw scale and a semantic role layer that points at it; components touch only semantic roles, never raw hex

Raw values rarely change but roles get reassigned constantly, so the split lets you re-theme by editing one place.

Read the case

Components from tokens

Non-negotiable

Components read only token utilities (bg-accent, ring-hairline); never hardcode hex, rgb(), one-off grays, or arbitrary brackets

A hardcoded shade stops tracking the token, so the next theme change leaves the component the wrong color.

Read the case

Re-theming

Non-negotiable

A token change is the only way to restyle the brand; never edit components to re-theme

If re-theming requires editing components, they are holding colors they should be borrowing and the system has drifted.

Read the case

Tailwind v4 wiring

Strong default

Expose semantic roles via @theme inline, and give --accent-foreground its own token

The inline keyword makes utilities resolve to var() at runtime so reassigning a variable actually re-themes.

Read the case

Readable measure

Strong default

Cap body copy at a 66ch measure, negative tracking on large display, positive on small caps

Measure is the single biggest lever on readability and past it the eye loses the next line's start.

Read the case

Card depth

Strong default

Use bg-surface-raised plus ring-1 ring-hairline plus shadow-soft-md, a ring not a hard border

A ring sits outside the box model so the layout never shifts by a pixel on hover or focus.

Read the case

Responsive and Theming

Authoring order

Non-negotiable

Style the smallest screen first (unprefixed), then layer sm:/md:/lg: to widen; never start desktop and patch down

A layout born on the hardest screen works everywhere while squeezing a desktop layout down makes every override a patch.

Read the case

Tap targets and overflow

Non-negotiable

Give anything tappable a 44x44px hit area and contain overflow so the page never scrolls sideways on mobile

Fingers are not cursors and one element wider than the viewport is the most common mobile bug.

Read the case

Theme default and flash

Non-negotiable

Put dark values directly on :root, resolve the stored theme with a tiny synchronous inline script before paint, and add suppressHydrationWarning to <html>

Dark is the product so it must render with zero conditions met, and effects run after paint which is exactly when the flash happens.

Read the case

Inline script safety

Non-negotiable

Keep the dangerouslySetInnerHTML string a fixed literal, never interpolate request data, and validate the stored value is exactly "light" or "dark"

A fixed literal introduces no injection surface and validation ensures a tampered entry can only produce a known-good attribute.

Read the case

Mobile navigation

Strong default

Below lg, collapse nav into a static hamburger that opens a side drawer with a real close X inside it

The morph-to-X animation reads as generic and a drawer with a real close button is clearer about what is happening.

Read the case

Component responsiveness

Strong default

Prefer container queries (@container) over viewport breakpoints for components that adapt to their container

The component stays responsive wherever you drop it, not just relative to the whole page.

Read the case

Data and Auth

Session verification

Non-negotiable

Always verify with auth.getUser() on the server through one cached Data Access Layer function; never trust getSession()

getUser() cryptographically re-verifies the JWT while getSession() just reads the cookie a forged value could fake.

Read the case

Service role key

Non-negotiable

Never prefix the service role key with NEXT_PUBLIC_ or reference it from any client file; server-only, for trusted webhooks and admin work

It bypasses RLS entirely, and once inlined into the bundle a value is public forever with rotation the only recovery.

Read the case

Row Level Security

Non-negotiable

Enable RLS on every table the anon key can reach and open access one minimal policy at a time

The default posture is deny and access is opened deliberately, which is what makes the anon key safe to expose.

Read the case

Supabase clients

Non-negotiable

Split the browser and server clients into separate files (lib/supabase/client.ts and server.ts), both on the anon key

The separation is structural, not a matter of remembering which import is safe where.

Read the case

Auth input validation

Non-negotiable

Validate every auth form with a Zod schema inside a server action before calling Supabase, with a real password policy, accept-terms, and honeypot

Validation belongs at the boundary in a mutation, and the policy, terms, and honeypot stop weak passwords and bots.

Read the case

Open-redirect protection

Non-negotiable

Allowlist any next/returnTo value to a path starting with '/', else fall back to /dashboard

A user-controlled redirect target is an open redirect used for phishing and to bounce sessions off-site.

Read the case

User-specific caching

Non-negotiable

Embed relations in one query to avoid N+1, and never cache a user-dependent query unless the cache key includes that user

N+1 multiplies round trips and caching user-specific results without a per-user key leaks data.

Read the case

Database

Strong default

Supabase Postgres through the Supavisor transaction-mode pooler on port 6543; reserve direct port 5432 for migrations

Transient functions talking to Postgres directly exhaust the connection slot limit during traffic spikes.

Read the case

Auth stack

Strong default

Supabase Auth with @supabase/ssr in the App Router, email+password plus Google OAuth

It is the auth stack the whole page is built on for a side project on Supabase.

Read the case

Indexing

Strong default

Index every column you filter, join, or order by (notably the tenant FK and composites), confirmed by explain analyze

Postgres does not auto-index foreign keys and unindexed WHERE columns turn into fatal sequential scans at scale.

Read the case

Pagination

Strong default

Use keyset (cursor) pagination instead of OFFSET

OFFSET makes Postgres read and discard rows so deep pages get slower, while keyset rides the index at constant cost.

Read the case

Session refresh location

Strong default

Refresh expiring sessions in proxy (the Next.js 16 rename of middleware), never enforce access there

The docs are explicit that proxy is for optimistic checks only and real authorization belongs in the data layer.

Read the case

Security

Zero-trust entry points

Non-negotiable

Treat every Server Action and route handler as a public endpoint reachable by direct POST; never treat the UI as access control

An exported Server Action is reachable even when no component renders its button.

Read the case

Mutation check order

Non-negotiable

Three checks in order inside every handler: validate input with Zod, authenticate, then authorize by ownership

Skipping the ownership check is the IDOR bug, the most common way a logged-in user reads or deletes another user's data.

Read the case

Secrets handling

Non-negotiable

Read process.env only in server code, add import 'server-only' to every secret-touching module, and never give a secret a NEXT_PUBLIC_ prefix

server-only turns an accidental client import into a build error and NEXT_PUBLIC_ inlines the value into shipped JavaScript forever.

Read the case

NEXT_PUBLIC_ discipline

Non-negotiable

Treat NEXT_PUBLIC_ as publishing to the world; never add it to silence a missing-env error, move the read to the server instead

The error means a secret is being read on the client and the correct fix is server-side reading, not exposure.

Read the case

Env files in git

Non-negotiable

Never commit .env.local or any file with a real credential; commit .env.example with blank values

A secret in git history is leaked even after deletion, so real values must never reach the repository.

Read the case

Data Access Layer

Non-negotiable

One server-only DAL owns every DB call and process.env read, runs authorization, and is the single audit surface

Auditing one server-only module beats grepping the whole app for stray queries, secrets, and missing checks.

Read the case

SQL queries

Non-negotiable

Parameterize via tagged template or query builder; never interpolate user input into a query string

String interpolation in a query is SQL injection waiting to happen.

Read the case

Server-to-client data

Non-negotiable

Map to a minimal DTO with only the fields the UI renders; never pass a raw DB row or full User object across the boundary

Every field on a passed object is serialized into the client payload, so the unrendered fields leak.

Read the case

Content Security Policy

Non-negotiable

Nonce-based CSP minted in proxy.ts, fresh per request; never use 'unsafe-inline' in script-src, allow 'unsafe-eval' only in dev

Only scripts tagged with the exact per-request token run, which is exactly what an injected XSS payload lacks.

Read the case

Rate-limit backend

Non-negotiable

Shared Redis-backed limiter (Upstash, sliding window) in production; never a per-process Map, with an in-memory dev fallback

A module-level Map resets per serverless cold start, so the counter must be visible to every instance to limit anything.

Read the case

Error exposure

Non-negotiable

Model expected errors as return values; throw only unexpected ones to an error boundary, never render error.message or a raw DB error

A leaked stack trace or raw DB error is a reconnaissance gift to an attacker.

Read the case

Shipping verification

Strong default

Run next build and grep .next/static for known secret strings to confirm nothing leaked

A secret string appearing under .next/static proves it leaked into the browser bundle.

Read the case

Forms, Email, and Errors

Form submission

Non-negotiable

Submit forms through a server action that validates every field with Zod first; never fetch a third party or expose a provider key from the client

It keeps API keys and provider calls on the server and treats all client input as untrusted before any field is used.

Read the case

Email provider

Non-negotiable

Resend behind one uncached POST handler that validates first, builds the client second, sends last

Email is a side effect on untrusted input that needs one trusted choke point and mutations must never be cached.

Read the case

Sender address

Non-negotiable

Keep from on a verified domain and set replyTo to the visitor; never spoof from with the visitor's address

Spoofing from gets you marked as spam and breaks SPF and DKIM.

Read the case

Untrusted content in email

Non-negotiable

HTML-escape every user-supplied value before substituting it, and build action URLs from trusted server state only

Raw form input can break layout or smuggle a tracking pixel and an attacker-controlled value must never become the link target.

Read the case

Email layout

Non-negotiable

Build with HTML tables, never flexbox or grid, and always send a plain-text fallback

Outlook renders with Word's engine so tables are the only layout that renders the same across clients, and plain text aids deliverability.

Read the case

Destructive actions

Non-negotiable

Use a controlled dialog that names the target and disables while pending, never window.confirm, and re-check ownership server-side

window.confirm cannot show context and the modal is UX, not authorization, since a fetch client skips it entirely.

Read the case

Root 404

Non-negotiable

Always ship app/not-found.tsx as the app-wide catch-all for unmatched URLs

The root not-found doubles as the catch-all for any unmatched URL across the app.

Read the case

Spam defense

Strong default

Add a CSS-hidden honeypot rejected when filled plus a per-IP rate limit, instead of a captcha

Together they stop the bulk of automated spam without forcing a captcha on real users.

Read the case

Feedback UI

Strong default

Drive UI from the server action's returned state via useActionState, tie each error to its input with aria-describedby, disable submit while pending

Typed returned state plus ARIA wiring gives accessible inline errors and clear pending feedback.

Read the case

Growth: SEO, Analytics, Monetization

metadataBase and canonicals

Non-negotiable

Set metadataBase once in the root layout and an explicit alternates.canonical on every indexable page

Relative OG and canonical URLs resolve against it and the canonical is the strongest signal against duplicate-content dilution.

Read the case

robots.txt is not access control

Non-negotiable

Never rely on robots.txt disallow to protect admin, account, or API paths; gate sensitive routes with real authorization

robots.txt is a public, polite request that tells everyone exactly where to look.

Read the case

generateMetadata as a trust boundary

Non-negotiable

Validate params shape before fetching and never echo unsanitized user input into title or description

A crafted slug reaching an internal service is request forgery, not an SEO bug.

Read the case

Analytics load and placement

Non-negotiable

Load GA4 via next/script at afterInteractive, mounted once in app/layout.tsx referencing NEXT_PUBLIC_GA_ID literally, rendering nothing when unset

afterInteractive keeps the tag off the critical path, per-page mounting double-counts, and a computed env key silently no-ops.

Read the case

AdSense rendering and policy

Non-negotiable

Load the loader once via next/script afterInteractive, render units through a client component with a silent try/catch; place units only in finished content, never click your own ads

A missing ad must never break the page, and self-clicks or empty pages are policy violations that get accounts permanently banned.

Read the case

Consent default

Strong default

Load the CMP beforeInteractive and default analytics_storage to denied until the visitor opts in

Collecting nothing until consent is the secure-by-default posture that keeps you on the right side of GDPR.

Read the case

Title and OG strategy

Strong default

Use title.template plus title.default in the root layout and the opengraph-image.tsx file convention via ImageResponse

Child pages inherit the brand suffix for free and the file convention auto-wires og:image at higher priority than config.

Read the case

Affiliates and Referrals

Referral identifier

Non-negotiable

Sign the ref value (HMAC or signed JWT) in an httpOnly cookie and verify the signature before trusting it

A raw ?ref= value is attacker-editable and lets anyone credit themselves or burn a competitor.

Read the case

Commission source of truth

Non-negotiable

Never credit a commission from the browser; compute amounts server-side from the verified gross payment and your own tier table

There must be no step where editing a URL or replaying a request can manufacture a credit.

Read the case

Attribution and conversion

Non-negotiable

Claim attribution once, idempotently keyed on the new user id, and record the conversion inside the payment webhook after the row commits

An atomic RPC keyed on the user prevents double-attribution and ties the commission to real settled money.

Read the case

Refunds and chargebacks

Non-negotiable

Reverse or void the conversion when a commission-earning payment is refunded or charged back

A paid-out commission on refunded revenue is a direct loss, so commission must track real settled money.

Read the case

Auth-callback side effects

Strong default

Make attribution best-effort and never block the redirect; swallow errors and continue

A failed attribution is a bookkeeping problem but a blocked sign-in is a lost customer.

Read the case

Payments and Cost

Source of truth for access

Non-negotiable

Grant access only from the signed provider webhook, never from the browser checkout redirect

The browser can be closed, replayed, or forged, while the webhook is a signed server-to-server call.

Read the case

Webhook signature verification

Non-negotiable

Verify the signature against the raw req.text() body before parsing, never req.json()

The signature is computed over the exact bytes, so parsing first invalidates verification.

Read the case

Trusting webhook amounts

Non-negotiable

Re-fetch the payment from the provider API by id and trust that for amount, currency, and status

The webhook body is only a spoofable hint while the provider API is authoritative.

Read the case

Idempotency

Non-negotiable

Insert under a unique constraint keyed on payment id (and event id) and treat a 23505 violation as an already-processed no-op

Providers retry and fire sibling events per payment, so a non-idempotent grant can grant twice.

Read the case

Checkout pricing

Non-negotiable

Set amounts and SKU on the server from my own pricing table, never from a client-sent price

A client-supplied amount is a free-product exploit.

Read the case

Domain registrar

Non-negotiable

Register at an at-cost registrar like Cloudflare (or Porkbun, Namecheap) and check the renewal price, never just the first-year promo

At-cost registrars charge wholesale while others quietly gouge on renewals.

Read the case

Domain upsells

Non-negotiable

Never buy domain privacy, premium DNS, or SSL add-ons

WHOIS privacy is free at decent registrars and Vercel issues and renews TLS for free.

Read the case

Commercial licensing

Non-negotiable

Move Vercel to Pro the same day a project becomes commercial, never run commercial work on Hobby

Vercel Hobby forbids commercial use, so this is a licensing rule independent of traffic.

Read the case

Access checks

Strong default

Decide access by querying a live entitlement (active and not expired), not a boolean flag flipped once

Expiry, refunds, and downgrades need a queryable source of truth, not a stale flag.

Read the case

Launch stack and upgrades

Strong default

Launch on free tiers (Vercel Hobby, Supabase, Resend, Sentry, GA4) and upgrade one line at a time only when its specific trigger fires

Every tool has a production-usable free tier, so pre-buying Pro pays for headroom you have not earned.

Read the case

Ship: Deploy and CI

Secrets handling

Non-negotiable

Set every production env var in Vercel settings scoped to Production, Preview, or Development, never ship them in the repo

Committed secrets get published to GitHub and separate scopes stop a preview from writing production data.

Read the case

Linting in Next.js 16

Non-negotiable

Run eslint directly as its own script step in both the ship gate and CI, never next lint

next lint is removed and next build no longer runs the linter, so skipping this means lint never runs.

Read the case

CI gate

Non-negotiable

Run one GitHub Actions verify job of five steps (typecheck, lint, check:content, test, build) on every PR and push to main

A green Vercel build only proves it compiled, so types, lint, content, and tests need their own gate.

Read the case

Test command

Non-negotiable

Use vitest run, never bare vitest, for the CI test step

Bare vitest stays in watch mode and CI hangs forever.

Read the case

Branch protection

Non-negotiable

Require the verify check and up-to-date branches, require a PR with one approval, and disallow bypass for everyone including admins

A gate admins can bypass does not exist, because the one midnight bypass is when the broken build ships.

Read the case

DNS records

Non-negotiable

Point the domain at Vercel with exactly an A record on the apex and a CNAME on www, deleting stale records first and keeping Cloudflare DNS-only

The apex cannot be a CNAME, and stale or orange-cloud-proxied records are the top reasons a domain will not connect.

Read the case

Definition of shipped

Non-negotiable

Never treat implemented as shipped; shipping includes brand, edge cases, empty states, the phone view, and verifying the rendered live output

A passing build only confirms it compiled, nothing about whether the page looks right or works.

Read the case

Hosting and environments

Strong default

Deploy to Vercel via GitHub push with main as production, feature branches as previews, and run npm run ship locally first

Previews build from the exact commit that ships and the local gate makes CI a backstop, not a surprise.

Read the case

Node version

Strong default

Pin the same Node major (20.x or 22.x) locally, in CI, and on Vercel

Matching versions means a green build locally and in CI almost certainly means Vercel is green too.

Read the case

Pre-ship manual checks

Strong default

Open the live page, click every button, check both themes, resize to phone, verify the share card, and ship a real favicon with no dead links

Each check catches a class of bug the build never will, and a default icon or 404 is the loudest unfinished tell.

Read the case

Operate: Performance and Observability

Per-user caching

Non-negotiable

Cache only what is identical for everyone and tag it; never cache a per-user response under a key that omits the user

A user-specific response under a shared key leaks one user's data to another.

Read the case

Images

Non-negotiable

Use next/image with width, height, and sizes, never a raw full-resolution img tag

Next serves a fitted modern-format image and reserves layout space to avoid CLS.

Read the case

Logging failures

Non-negotiable

Every logging path must swallow its own errors and never rethrow

Logging is a side effect on the hot path and must never take down the request it only observes.

Read the case

PII in logs

Non-negotiable

Keep PII out of log fields and the msg string by default, with key-based redaction in the logger as a backstop

Doing redaction in the logger stops any call site leaking personal data, but it cannot catch concatenated values.

Read the case

Sentry DSN and source maps

Non-negotiable

Use NEXT_PUBLIC_SENTRY_DSN in the browser, keep SENTRY_AUTH_TOKEN secret, and set deleteSourcemapsAfterUpload so maps strip from the public bundle

A server-only DSN captures nothing client-side while a leaked token or shipped source map exposes your source.

Read the case

Sentry client instrumentation

Non-negotiable

Run client instrumentation only from instrumentation-client.ts, not the legacy sentry.client.config.ts

Next 16 only executes client instrumentation from instrumentation-client.ts.

Read the case

Policy honesty

Non-negotiable

Never paste a generic policy template you never read; treat any data-flow change (analytics, processor, OAuth scope) as shipping with a policy edit

A policy describing data flows you do not have is a promise you are silently breaking.

Read the case

Caching and invalidation

Strong default

Cache rarely-changing shared reads with use cache plus cacheLife, invalidate by tag, and wrap request-time parts in Suspense

Recomputing identical data per request wastes work while tags keep precise control over staleness.

Read the case

Pagination

Strong default

Default to cursor (keyset) pagination for feeds and paginate every list that can grow, using offset only for small fixed page-number UIs

An unbounded findMany stalls the page as the table grows, while keyset seeks straight to the cursor row at constant cost.

Read the case

Structured logging

Strong default

Emit one structured JSON line per event with a namespaced name (domain.action.outcome), forward warnings and errors to Sentry from the log helper, and await log.flush() before a serverless handler returns

Queryable events beat free text, helper-level forwarding reports every error with no call-site changes, and flushing stops a freezing instance dropping sends.

Read the case

Legal pages

Strong default

Ship privacy, terms, disclaimer, and data-deletion pages under one indexable /legal prefix, linked from the footer and signup with stored consent

These are the gates Google sign-in, AdSense, and app stores check, and a page no one can find does not count.

Read the case

What to log

Opinion

Log decisions worth an incident review (payment received, webhook rejected, rate limit, auth failure, job done or failed), not entered-function chatter

A handful of well-named events per flow beats a hundred console.logs that bury the signal.

Read the case
NextGetting Started