# CLAUDE.md

> Project rules for a production Next.js app, distilled from Selwyn Uy's Next.js Handbook (https://selwynuy.dev/docs).
> Source of truth: https://selwynuy.dev/claude.md (re-fetch to refresh). Lines marked (draft) are strong defaults, not gospel.

## How to use this file

- Treat every rule below as a default for this codebase. When a rule's trigger matches what you are about to do, apply it.
- Prefer React Server Components; add "use client" only for interactivity or browser APIs.
- Never expose secrets to the client; validate and authorize every mutation and sensitive read on the server.
- When unsure why a rule exists, read the linked page before overriding it.

## Rules by area

### Start Here

#### Is Next.js the Right Tool?  (https://selwynuy.dev/docs/nextjs-fit-check)

- Otherwise: It is a web app that renders HTML and needs server logic. Use Next.js. Reaching this line means it is the right call.

#### Environment and Secrets  (https://selwynuy.dev/docs/environment-and-secrets)

- Do: Read secrets in Server Components, route handlers, and server actions only. Pass results, not credentials, to the client.
- Don't: Never commit `.env.local` or any file containing a real credential. Commit `.env.example` with the keys and blank values instead.

### Architecture

#### System Architecture  (https://selwynuy.dev/docs/system-architecture)

- Do: Keep `app/` files thin. A page or route handler orchestrates; it never implements. If a function in `app/` is longer than the render it produces, the logic belongs in `lib/`.
- Don't: Never import a database client or a vendor SDK inside a component or route handler. That couples your UI to your infrastructure and makes the data layer impossible to reuse or test.

#### Database Scalability  (https://selwynuy.dev/docs/database-scalability)

- Do: Connect serverless and edge functions through the transaction-mode pooler on port 6543. Reserve the direct connection (port 5432) for long-lived clients and for migrations.
- Don't: Do not add an index to every column reflexively. Each index slows writes and costs storage. Index the columns your actual queries touch, confirmed by `explain analyze`, and stop there.

### Design

#### Responsive and Mobile-First  (https://selwynuy.dev/docs/responsive-design)

- WHEN you write any layout: style the smallest screen first (unprefixed), then add sm: / md: / lg: to widen. Never start desktop and patch downward.
- WHEN you set a font size for body or headings: use a fluid scale with clamp() so type grows with the viewport instead of jumping at breakpoints.
- WHEN you make anything tappable (button, link, icon): give it at least a 44x44px hit area. Fingers are not cursors; tiny targets are unusable on a phone.
- WHEN your primary navigation has more than two or three items: collapse it below lg into a static hamburger that opens a sidebar drawer sliding in from the side. The hamburger icon stays a hamburger; do not morph it into an X. The close (X) lives inside the open drawer. Keep one constant orientation cue in the bar; move the rest into the drawer.
- WHEN you render an image: use next/image with sizes so the browser downloads a width that fits the slot, and constrain it to max-w-full so it never overflows.
- WHEN content can be wider than the screen (tables, code, long strings): contain the overflow: wrap where you can, scroll the element (not the page) where you cannot. A horizontal page scrollbar on mobile is a bug.
- WHEN a layout adapts to its container, not the whole page (cards, sidebars, widgets): prefer container queries (@container) over viewport breakpoints so the component is responsive wherever you drop it.

### Build

#### Authentication and Google Sign-In  (https://selwynuy.dev/docs/authentication)

- WHEN you read the current user anywhere on the server: call auth.getUser(), never getSession(). Funnel it through one cached DAL function so a forged cookie can never stand in for a verified user.
- WHEN you accept signup, login, reset, or any auth form: validate with a zod schema in a server action before calling Supabase. Enforce a real password policy and require an explicit accept-terms checkbox.
- WHEN an auth endpoint can be hit repeatedly (signup, login, password reset): rate-limit by IP and by hashed email before touching Supabase. Hash the email so the limiter key never stores a raw address.
- WHEN you add Google or any social sign-in: gate the button behind a public env flag so it only renders where the provider is configured, force account selection, and request only the scopes you use.
- WHEN any auth flow redirects to a next or returnTo value from the URL: allowlist it to a path that starts with /. A user-controlled redirect target is an open-redirect, used for phishing and to bounce sessions off-site.
- WHEN you write the OAuth / email-confirmation callback route: handle every branch: provider error, code present (exchange it), and no code at all. Map Supabase error codes to stable tokens the login page can explain, and never let a best-effort side effect block the redirect.
- WHEN you protect a page, action, or API behind login: re-check the session at the data layer with requireUser(), not just in proxy.ts. The proxy is a coarse gate for UI; server actions are independent POST endpoints that must verify the caller themselves.

#### Contact Forms and Form Handling  (https://selwynuy.dev/docs/contact-and-forms)

- WHEN you handle any form that sends mail, writes data, or calls a paid API: do it in a server action, not a client fetch to a third party. Keys and provider calls stay on the server; the client only sends field values.
- WHEN you accept form input: validate it with a zod schema before using any field, and return field-level errors the form can show inline. Never trust lengths, formats, or presence from the client.
- WHEN a form is publicly reachable: add a honeypot field hidden from humans and rejected when filled, plus a rate limit by IP. Together they stop the bulk of automated spam without a captcha.
- WHEN the email send can fail (provider down, bad address, quota): check the send result and surface a real error instead of pretending success. A form that says 'sent' but dropped the message is worse than one that says 'try again'.
- WHEN you render form errors and status: drive the UI from the server action's returned state via useActionState, tie each error to its input with aria-describedby, and disable the submit button while pending.
- WHEN a user submits a contact message you will reply to: set the email's reply-to to their address (not the from), so hitting reply in your inbox reaches the user. Keep the from on your own verified domain.

### Security

#### Security by Default  (https://selwynuy.dev/docs/security)

- WHEN you build a SQL query from any user-supplied value: use parameterized queries or the ORM's binding. Never string-concatenate or interpolate input into SQL.
- WHEN you accept an email address: validate it with a schema, normalize the case, and treat it as data, never as a command or a file path.
- WHEN you render user-supplied content as HTML: let React escape it by default. Only reach for dangerouslySetInnerHTML after sanitizing with a vetted library.
- WHEN you accept a file upload: check the size and the real content type, store outside the web root with a generated name, and never trust the client-sent filename or MIME type.
- WHEN you put a user value into a redirect, a shell command, or an outbound URL: allowlist it. Compare against known-safe values instead of trusting the input to be safe.
- WHEN you expose an endpoint that sends email, costs money, or mutates data: rate-limit per user or IP. An unthrottled action is a spam relay and a billing-attack surface.

#### Security by Design  (https://selwynuy.dev/docs/security-by-design)

- Do: Keep every query inside the DAL. Auditing one `server-only` module beats grepping the whole app for stray `sql` calls.
- Don't: Do not run mutations as a side-effect of rendering. Next.js blocks setting cookies and revalidating during render for exactly this reason. A `GET` that mutates is both a CSRF hole and a caching bug. Put every mutation behind a Server Action POST.

#### Data Access Security  (https://selwynuy.dev/docs/data-security)

- Do: Read secrets, tokens, and database rows in server code only. The browser should receive rendered output and narrow data, never the keys used to produce it.
- Don't: Never pass a raw database row or a full `User` to a Client Component. Map it to a small Data Transfer Object first, even when the component renders only one field today.

#### Content Security Policy  (https://selwynuy.dev/docs/content-security-policy)

- Do: Call `await connection()` at the top of every page that relies on a nonce. It is the explicit, readable signal that the page is request-bound.
- Don't: Do not reach for SRI to escape the dynamic-rendering cost when you genuinely need a strict per-request policy. It is build-time only and cannot cover scripts generated dynamically at request time.

#### Rate Limiting  (https://selwynuy.dev/docs/rate-limiting)

- WHEN you add rate limiting to a multi-instance or serverless deploy: use a shared store (Upstash Redis or equivalent), never a per-process Map. The counter must be visible to every instance or it does not limit anything.
- WHEN Redis env vars are missing (local dev, tests, a fresh clone): fall back to an in-memory limiter so dev still works, but make it explicit that it is single-process only and never the production backend.
- WHEN you choose what to key the limit on: key by the most specific stable identity available, and often combine two: per-IP AND per-account/email. IP alone punishes shared networks; identity alone is bypassed by new accounts.
- WHEN the rate-limit key includes an email or other PII: hash it before using it as a key (and before logging). The limiter store should never hold raw personal data.
- WHEN you read the client IP to key a limit: read it from the forwarded headers your platform sets, with a fallback, since the socket address behind a proxy is the proxy, not the user.
- WHEN a request exceeds the limit: return 429 with a clear retry hint and a friendly message, and log the event. Do not leak whether the underlying resource exists (e.g. don't say 'too many attempts for this account' on login).

### Grow

#### SEO  (https://selwynuy.dev/docs/seo)

- WHEN you set up a new site and want Google to track its indexing: add the property in Search Console and verify ownership. The DNS TXT method verifies the whole domain at once; the HTML meta tag verifies a single origin and is one line in your root metadata.
- WHEN your property is verified: submit your sitemap URL in Search Console (Sitemaps -> add /sitemap.xml). Do not wait for Google to discover it; submitting tells Google your full URL set immediately.
- WHEN a page must never appear in search (auth shells, dashboards, thank-you pages): set robots index:false on the page itself, and keep it out of the sitemap and the robots allow-list. Crawl budget should go to content, not login forms.
- WHEN you write robots.ts with both an allow and a disallow on overlapping paths: remember Google applies the longest matching rule. Disallow: /library/ will silently drop an allowed /library listing. Block specific private subpaths, not a broad prefix that also catches public pages.
- WHEN after launch, or after a content or structure change: check Search Console for coverage errors and use URL Inspection to request indexing on important new pages. Treat 'Discovered but not indexed' as a signal the page is too thin or too similar to others.

#### Performance and Optimization  (https://selwynuy.dev/docs/performance)

- WHEN an endpoint or query returns a list that can grow unbounded: paginate it. Default to cursor (keyset) pagination for feeds and infinite scroll; use offset only for small, fixed page-number UIs.
- WHEN the same data is read on many requests and changes rarely: cache it with the use cache directive and set a cacheLife. Do not recompute per request what is identical across requests.
- WHEN a mutation changes data you have cached: invalidate by tag. Use updateTag in a Server Action when the user must see their own write immediately; use revalidateTag for background refresh where slight staleness is fine.
- WHEN a component reads request-time data (cookies, headers, searchParams) on an otherwise cacheable page: wrap just that component in a Suspense boundary so the static shell ships instantly and only the dynamic part streams. Do not make the whole page dynamic.
- WHEN you render an image: use next/image with width/height and sizes so Next serves a fitted, modern-format image and reserves layout space (no CLS). Never ship a raw full-resolution img tag.
- WHEN you query a list and then query again inside the loop for each row: that is an N+1. Fetch the relations in one query (join / include) or batch the lookups. One query for the page, not one per row.
- WHEN a query filters or sorts on a column: make sure that column is indexed. An unindexed WHERE or ORDER BY does a full table scan that gets slower with every row.
- WHEN a heavy or rarely-used client component sits below the fold or behind an interaction: code-split it with next/dynamic so it is not in the initial bundle. Keep the first load lean.

#### Monetization with AdSense  (https://selwynuy.dev/docs/monetization-adsense)

- WHEN you are about to apply for AdSense: ship the required pages first: a published privacy policy, an about page, and a reachable contact method. A site with no privacy policy is an automatic rejection.
- WHEN you want the crawl surface to read as content-dense (a quality signal): keep auth shells and thin pages out of the index. noindex /login and /signup and exclude them from the robots allow-list so the crawlable surface is mostly real content.
- WHEN you add the AdSense account association: declare the publisher ID with the google-adsense-account meta in your root metadata. This links the domain to your account for review, separate from the loader script.
- WHEN you load the AdSense script: load it once in the root layout with next/script at afterInteractive, gated behind an env flag. The same single script also delivers Google's certified CMP consent banner to EEA/UK/Swiss visitors; there is no separate consent tag to embed.
- WHEN you define ad slots before the units exist in the dashboard: keep slot IDs empty until you create the real unit, and make the ad component render nothing for an empty slot. A misconfigured deploy should show no ad, never a broken or blank unit.
- WHEN some users pay and some do not: show ads to free and anonymous visitors only; never render an ad unit for a paid user. Gate it in one viewer component so no ad can leak onto a Pro page.

#### Affiliates and Referrals  (https://selwynuy.dev/docs/affiliates-referrals)

- WHEN you store a referral identifier from a URL: sign it (HMAC or signed JWT) and set it as an httpOnly cookie; verify the signature before trusting it. A raw ?ref= value is forgeable and lets anyone credit themselves.
- WHEN a referred user signs up: claim the attribution once, idempotently, keyed on the new user id (first click wins). Do it in an atomic RPC so concurrent callbacks cannot double-attribute.
- WHEN attribution side effects run in an auth callback: make them best-effort and never block the redirect. A failed attribution is a bookkeeping problem; a blocked sign-in is a lost customer.
- WHEN a referred user makes a payment: record the conversion and compute the commission inside the payment webhook, AFTER the payment row commits, in the same idempotent path. Never credit a commission from the browser.
- WHEN you compute commission amounts or tiers: do it server-side from the verified gross amount and your own tier table, never from anything the client sent. The commission is derived from the trusted payment, not from a referral parameter.
- WHEN a payment that earned a commission is refunded or charged back: reverse or void the conversion. Commission must track the real, settled money; a paid-out commission on refunded revenue is a direct loss.
- WHEN you expose affiliate dashboards, payouts, or assets: gate every affiliate route behind auth and ownership, and keep them out of the search index. An affiliate sees only their own numbers; these are private app pages, not content.

#### Payments and Billing  (https://selwynuy.dev/docs/payments-billing)

- WHEN you receive a payment webhook: verify the signature against the raw request body before parsing or trusting anything. Read req.text(), not req.json(), because the signature is computed over the exact bytes.
- WHEN you read the amount, currency, or status from a webhook: re-fetch the payment from the provider's API by id and trust that, not the numbers in the webhook body. The body is a hint; the API is the truth.
- WHEN you write the webhook handler: make it idempotent. Insert the payment keyed by a unique constraint and treat a 23505 unique-violation as 'already processed, no-op'. Providers retry and often fire more than one event per payment.
- WHEN a provider fires multiple event types for one payment: key idempotency on the PAYMENT id, not just the event id. e.g. payment.paid and checkout.paid are two events with two event ids for the same money; the payment id collapses them to one grant.
- WHEN payment is confirmed and you grant access: grant from the webhook only, after the payment row commits, and only for status paid. Carry the user id in the checkout session metadata so the webhook knows who paid.
- WHEN you decide what a user can access: check a live entitlement (active and not expired), not a boolean you flipped once. Expiry, refunds, and downgrades all need a queryable source of truth, not a stale flag.
- WHEN you create the checkout session: set amounts and the SKU on the server from your own pricing table, never from a price the client sent. A client-supplied amount is a free-product exploit.

### Ship

#### Errors, Feedback, and Confirmations  (https://selwynuy.dev/docs/error-handling-ux)

- Do: Return expected failures as a typed state object. Inline errors live next to the field; transient successes fire a toast.
- Don't: Do not throw for validation, conflicts, or "not allowed". Those are normal flow, not exceptions.
- Do: Name the target in the dialog title, disable both buttons while pending, and confirm the outcome with a toast.
- Don't: Do not use `window.confirm` for destructive actions, and never delete on a single un-guarded click.

### Operate

#### Observability and Structured Logging  (https://selwynuy.dev/docs/observability-logging)

- WHEN you log anything in production code: emit one structured JSON line with a stable event name and typed fields, not a free-text string. Name events like namespaced keys (domain.action.outcome) so they group and search.
- WHEN a log's fields might contain personal or sensitive data: redact by key in the logger itself, so no call site can leak. Keep a deny-list (password, email, answers, tokens) and replace matched values with [redacted].
- WHEN you want errors and warnings in an aggregator (Sentry): forward them from inside the log helper, gated on the DSN being set, so every existing log.error call reports automatically with zero changes at the call sites.
- WHEN you log inside a serverless function (route handler, server action): await a flush before returning so the log/Sentry transport completes. A serverless function can freeze the instant it returns, dropping in-flight async sends.
- WHEN logging itself could fail (transport down, Sentry not initialized): make every logging path swallow its own errors. Logging is a side effect on the hot path; it must never throw and take down the request it was only meant to observe.
- WHEN you need errors caught before any request handler runs: use Next.js instrumentation hooks (register / onRequestError in instrumentation.ts) to initialize Sentry and capture server errors globally, rather than wrapping every route by hand.

#### Error Monitoring with Sentry  (https://selwynuy.dev/docs/sentry-setup)

- Don't: Never expose `SENTRY_AUTH_TOKEN` to the client or commit `.env.sentry-build-plugin`. A leaked token lets anyone push releases and read your source maps.
- Do: Sample performance traces, never errors. You want every exception and a representative slice of traces.

#### Legal Pages You Cannot Skip  (https://selwynuy.dev/docs/legal-pages)

- WHEN your app collects any user data, uses analytics, or offers accounts: publish a privacy policy at a stable URL. It must say who you are, what you collect, why, who you share it with (sub-processors), how long you keep it, and how a user exercises their rights.
- WHEN you offer Continue with Google or any OAuth provider: add an explicit Google user data section to the privacy policy. Name the exact scopes (openid, email, profile), the fields you actually read, and confirm Limited Use. Google verification checks for this.
- WHEN users sign in with a third party (Google, Apple, etc.): provide a data-deletion page with a clear path to delete an account and its data. Google and the app stores require a reachable deletion route, not just a support email buried in a FAQ.
- WHEN users can pay, subscribe, or rely on your content: publish terms of service: the agreement to use the app, payment and refund terms, acceptable use, liability limits, and governing law. Pair it with a disclaimer if your content could be read as professional advice.
- WHEN you set the accept-terms checkbox on signup: link the checkbox to the live /legal/terms and /legal/privacy routes, and store the consent (a boolean plus a timestamp) with the account. Acceptance you cannot prove is acceptance you do not have.
- WHEN you serve users in the EU/UK, California, or other privacy-law regions: cover the rights those laws grant (access, correction, deletion, objection) and name the law. State your cookie use plainly. If you run ads, a consent banner is required, not optional.

## Attribution

Bootstrapped with Selwyn Uy's Next.js Handbook (https://selwynuy.dev). Keep this CLAUDE.md in the project root so future sessions inherit the rules.