cloudflare-pages (new; prepared for Spotter Round 2.0.0 and moved here before any release)
Your pages.dev address is kept out of search, and your real domain is not. Every Cloudflare Pages project serves the whole site at <subdomain>.pages.dev as well as at its custom domain, and Cloudflare marks only preview deployments noindex. Measured 2026-09-24: a per-deployment URL and a branch alias answered X-Robots-Tag: noindex, and the production alias answered with no header until a host rule was added. The same day a search console reported five pages of one site as "Duplicate, Google chose different canonical than user" while every page carried a correct self canonical. Two host rules in _headers fix it, and a checker now proves them both ways.
- §1 deploying: git-connected or direct upload (a one-way choice per project name), push by SHA,
wrangler pages deploy --commit-hash so the deployment list names the commit, a real 404.html from day one, and "what is live" as a hash comparison. - §2
_headers and _redirects: host rules with a :placeholder subdomain (measured), the documented limits (100 header rules; 2,000 static and 100 dynamic redirects, every rule after the first wildcard counted as dynamic), exact paths first, extension-less rewrite targets, and wrangler pages dev as the only local server that applies them. - §3 every
pages.dev host is noindex: what Cloudflare sends on each host, graded; the two host rules; the subdomain taken from the project listing, never the name; no host-less noindex; the checker; and a throwaway-branch test with its cleanup. - §4 cache skew and the module MIME error, and the zone settings that cause the same error; §5 www to apex as a zone Redirect Rule (Pages ignores a domain-level
_redirects line), http to https, and the 308 to a trailing slash; §6 edge banning through a scoped token with an IP filter, and its proof of function; §7 presigned R2 uploads that must sign content-type; §8 prerendering a client-rendered app; §9 why preload and Early Hints are judged live; then gotchas 1–21, each graded measured, observed, or documented and not measured. scripts/pages-noindex-check.mjs: --headers reads a _headers file with no request and refuses a missing alias or branch rule, a rule for a different subdomain, a host-less /* noindex, a noindex on a named custom domain and the documented limits. Live mode fetches each host once, cache-busted, and fails an alias that is indexable, a custom domain with a noindex header or meta tag, and any host that did not answer 2xx; a branch alias is checked only when named, and the output says so when it is not. --from-wrangler takes the hosts from wrangler pages project list --json, run through npx, which downloads wrangler from the npm registry without asking when neither the project nor your global npm has it; the README and the skill say so. --selftest runs 32 cases, the live half through a fake fetcher, and opens no socket.
stripe-on-supabase (new)
Billing fails quietly. A price id written into a migration works for months in test mode and fails on every plan the day the functions get a live key; a webhook left behind Supabase's default JWT check answers 401 to every delivery; a subscription status the table does not accept fails its upsert, and Stripe retries the same event for three days while the customer's row keeps its old status. None of these fails a build.
- §1 prices as lookup keys, never price ids, so going live is data and not a migration; §2 test and live mode, sandboxes, and proving which account a key belongs to before writing; §3 secrets only in the Edge Functions (a
VITE_* or NEXT_PUBLIC_* variable ships to the browser); §4 the browser names a plan, never a price or an amount; §5 the webhook: signed raw body, constructEventAsync, JWT verification off for that function alone, an idempotent upsert on a unique subscription id, every Stripe status mapped, 200 for problems a retry cannot fix, the period end that moved onto items, and a pinned API version; §6 access granted from the table by an allowlist of statuses; §7 the chosen plan carried through a magic-link sign-in; §8 live-mode settings, legal links and the hosted pages before launch; §9 type-checking the functions beside an app; then 20 gotchas, each graded measured, observed or documented. scripts/billing-check.mjs reads a project and refuses 18 defects: a price id in SQL, code or JSON; a secret in a public variable, in source, or in an env file git tracks; a checkout that takes a price or an amount from the request; a webhook with no signature check, a synchronous one, a parsed body, a path that skips the check, or JWT verification still on; an unpinned SDK or API version; an upsert with no or the wrong conflict target, or one with no unique index behind it; a status the table rejects or the webhook never maps; the period end read from the wrong object; functions that type-check against the app's node_modules. It names a secret by kind and length, never its value; says what it cannot see (the Stripe account, the deployment, webhooks outside supabase/functions); refuses a folder that is not a Supabase project before reading anything; and --selftest runs 63 cases without a request.
supabase-passkeys (new)
A passkey unlocked with a fingerprint, face or PIN is two factors by NIST's measure, and Supabase Auth still issues its session at the first assurance level. So the users who turned on two-factor sign-in are the ones sent back to a code screen after using a passkey, and every check that requires the second level locks them out unless it is taught otherwise.
- §1 the rule, as a decision you make; §2 the order of operations: enable it with a permanent relying-party id, the client's passkey flag, a hand-run ceremony that requires user verification and checks the flag, skipping the code step after that sign-in, every second-level check in SQL, Edge Functions and the app that also accepts a passkey session (and step-up windows that count it), the
userHandle check before a step-up verify, insufficient_aal when adding or removing a passkey, the session-length setting to leave off, and the interface copy; §3 the known limit (the server cannot see the verification flag) and the exception you decide per action; §4 API facts the types do not show; §5 testing with a virtual authenticator; then 14 gotchas, each graded. scripts/passkey-check.mjs reads a project and refuses 16 defects, following helpers and their callers, and prints how many places each rule examined: the missing client flag, a ceremony without required verification or the flag check, a verify call given the wrong challenge field, a step-up without the userHandle check, second-level checks in SQL, functions and the app that reject a passkey session, step-up windows that omit it, passkey.update() or delete() without passkeyId, uncaught passkey calls, missing insufficient_aal handling, sign_count used as a signal, and a committed rp_id = "localhost". Live mode reads the project's auth settings and relying-party id with the anon key, and fails when the id is not the domain you expect. A folder that is not a project is refused before anything is read. --selftest runs 64 cases, the live half through a fake fetcher.