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-negotiableTypeScript, scaffolded with --typescript
Types pay for themselves the first time a refactor would silently break call sites.
Read the caseRouter and rendering
Non-negotiableApp 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 caseClient boundary
Non-negotiablePush "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 caseRoute files
Non-negotiableKeep 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 caseApp layering
Non-negotiableFour 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 caseVendor SDKs
Non-negotiableNever 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 caseNext.js 16 async APIs
Non-negotiableTreat 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 caseData at the boundary
Non-negotiableValidate 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 caseMutations
Strong defaultWrites 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 caseLinting
Strong defaultESLint 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 caseDependency versioning
Strong defaultPin 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 caseWhen to Reach Elsewhere
Default framework
Non-negotiableReach 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 caseMismatched workloads
Non-negotiableNever 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 caseMany concurrent websockets
Strong defaultUse 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 caseHeavy ML or batch jobs
Strong defaultUse 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 caseArchitecture granularity
Strong defaultDecide 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 caseDesign and Branding
Type roles
Non-negotiableExactly 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 caseFont loaders
Non-negotiableCall 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 caseAccent color
Non-negotiablePick 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 caseAccent on prose
Non-negotiableNever 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 caseBase colors
Non-negotiableNear-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 caseBody contrast
Non-negotiableBody 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 caseToken architecture
Non-negotiableTwo 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 caseComponents from tokens
Non-negotiableComponents 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 caseRe-theming
Non-negotiableA 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 caseTailwind v4 wiring
Strong defaultExpose 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 caseReadable measure
Strong defaultCap 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 caseCard depth
Strong defaultUse 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 caseResponsive and Theming
Authoring order
Non-negotiableStyle 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 caseTap targets and overflow
Non-negotiableGive 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 caseTheme default and flash
Non-negotiablePut 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 caseInline script safety
Non-negotiableKeep 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 caseMobile navigation
Strong defaultBelow 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 caseComponent responsiveness
Strong defaultPrefer 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 caseData and Auth
Session verification
Non-negotiableAlways 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 caseService role key
Non-negotiableNever 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 caseRow Level Security
Non-negotiableEnable 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 caseSupabase clients
Non-negotiableSplit 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 caseAuth input validation
Non-negotiableValidate 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 caseOpen-redirect protection
Non-negotiableAllowlist 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 caseUser-specific caching
Non-negotiableEmbed 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 caseDatabase
Strong defaultSupabase 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 caseAuth stack
Strong defaultSupabase 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 caseIndexing
Strong defaultIndex 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 casePagination
Strong defaultUse 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 caseSession refresh location
Strong defaultRefresh 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 caseSecurity
Zero-trust entry points
Non-negotiableTreat 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 caseMutation check order
Non-negotiableThree 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 caseSecrets handling
Non-negotiableRead 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 caseNEXT_PUBLIC_ discipline
Non-negotiableTreat 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 caseEnv files in git
Non-negotiableNever 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 caseData Access Layer
Non-negotiableOne 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 caseSQL queries
Non-negotiableParameterize 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 caseServer-to-client data
Non-negotiableMap 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 caseContent Security Policy
Non-negotiableNonce-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 caseRate-limit backend
Non-negotiableShared 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 caseError exposure
Non-negotiableModel 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 caseShipping verification
Strong defaultRun 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 caseForms, Email, and Errors
Form submission
Non-negotiableSubmit 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 caseEmail provider
Non-negotiableResend 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 caseSender address
Non-negotiableKeep 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 caseUntrusted content in email
Non-negotiableHTML-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 caseEmail layout
Non-negotiableBuild 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 caseDestructive actions
Non-negotiableUse 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 caseRoot 404
Non-negotiableAlways 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 caseSpam defense
Strong defaultAdd 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 caseFeedback UI
Strong defaultDrive 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 caseGrowth: SEO, Analytics, Monetization
metadataBase and canonicals
Non-negotiableSet 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 caserobots.txt is not access control
Non-negotiableNever 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 casegenerateMetadata as a trust boundary
Non-negotiableValidate 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 caseAnalytics load and placement
Non-negotiableLoad 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 caseAdSense rendering and policy
Non-negotiableLoad 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 caseConsent default
Strong defaultLoad 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 caseTitle and OG strategy
Strong defaultUse 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 caseAffiliates and Referrals
Referral identifier
Non-negotiableSign 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 caseCommission source of truth
Non-negotiableNever 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 caseAttribution and conversion
Non-negotiableClaim 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 caseRefunds and chargebacks
Non-negotiableReverse 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 caseAuth-callback side effects
Strong defaultMake 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 casePayments and Cost
Source of truth for access
Non-negotiableGrant 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 caseWebhook signature verification
Non-negotiableVerify 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 caseTrusting webhook amounts
Non-negotiableRe-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 caseIdempotency
Non-negotiableInsert 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 caseCheckout pricing
Non-negotiableSet 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 caseDomain registrar
Non-negotiableRegister 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 caseDomain upsells
Non-negotiableNever 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 caseCommercial licensing
Non-negotiableMove 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 caseAccess checks
Strong defaultDecide 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 caseLaunch stack and upgrades
Strong defaultLaunch 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 caseShip: Deploy and CI
Secrets handling
Non-negotiableSet 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 caseLinting in Next.js 16
Non-negotiableRun 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 caseCI gate
Non-negotiableRun 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 caseTest command
Non-negotiableUse vitest run, never bare vitest, for the CI test step
Bare vitest stays in watch mode and CI hangs forever.
Read the caseBranch protection
Non-negotiableRequire 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 caseDNS records
Non-negotiablePoint 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 caseDefinition of shipped
Non-negotiableNever 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 caseHosting and environments
Strong defaultDeploy 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 caseNode version
Strong defaultPin 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 casePre-ship manual checks
Strong defaultOpen 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 caseOperate: Performance and Observability
Per-user caching
Non-negotiableCache 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 caseImages
Non-negotiableUse 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 caseLogging failures
Non-negotiableEvery 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 casePII in logs
Non-negotiableKeep 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 caseSentry DSN and source maps
Non-negotiableUse 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 caseSentry client instrumentation
Non-negotiableRun 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 casePolicy honesty
Non-negotiableNever 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 caseCaching and invalidation
Strong defaultCache 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 casePagination
Strong defaultDefault 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 caseStructured logging
Strong defaultEmit 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 caseLegal pages
Strong defaultShip 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 caseWhat to log
OpinionLog 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