Site PIN gate

A shared-PIN gate in front of a whole Next.js site, from the proxy layer: one environment variable arms it, an unlock page sets an HMAC cookie, and the app behind it stays untouched.

Released
October 15, 2026
npx skills add timerise-ai/site-pin-gate
Current release
v0.5.0
Reference docs
6
Non-negotiables
6
License
MIT

What problem does Site PIN gate solve?

A site that is not ready for the public still has to be seen: by the client, by investors, by a reviewer. User accounts are too much for that. A shared PIN is enough, as long as the gate itself is safe.

The dangerous parts of a PIN gate are small and easy to get wrong: where the unlock page redirects, and how the cookie is made. An open redirect or a guessable cookie turns a locked preview into a public one.

This skill puts the gate in front of the whole site from the proxy layer. One environment variable arms it. The unlock page sets a cookie derived from a server-side secret. Comparisons run in constant time, failed attempts get a budget, and the app behind it never knows the gate exists.

What does the skill build?

An agent with this skill builds one module for a Next.js App Router app, on your stack. For Site PIN gate that module consists of:

  • 1. One env var arms it
  • 2. An unlock page sets an HMAC cookie
  • 3. Origin-resolved return path
  • 4. Attempt budget answering 429
  • 5. A bypass list that keeps webhooks and cron working
  • 6. The host's own locale resolver with right-to-left layout

Where do the rules come from?

This skill was written by the engineer who has shipped this module. The earlier implementation it was audited against was a temporary gate on a marketing site kept private ahead of its launch. The templates hold the properties a gate has to hold: an unlock that can only return to the site's own origin, a redirect the browser follows as a GET so the PIN is never sent twice, a cookie that is opaque to anyone without the server's secret, comparisons that take the same time whatever the input, a request body that is answered rather than thrown, and a failed-attempt budget that answers 429. The behaviour contract and the handler suite state each one; `references/provenance.md` has the record.

The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.

  1. Fixed in the templates

    • The unlock form redirected off-site (verified live) The return path was accepted if it started with /. //evil.example/x starts with /. Posting the correct PIN with that as next produced location: http://evil.example/x; /\evil.example and ///evil.example behave the same in the URL parser. A page anywhere could auto-submit a wrong PIN with a hostile next, let the victim see the real gate on the real domain, and collect them on the other side.
    • The cookie was the PIN in disguise (verified live) The cookie held SHA-256("<constant>:" + PIN), with the constant in the code. Numeric PINs are what a field marked inputmode="numeric" invites. A ten-thousand-iteration loop recovered the PIN from a captured cookie in milliseconds. The comment above the code said the raw PIN "never sits in the browser cookie", which was true and beside the point.
    • A successful unlock re-POSTed the PIN to the landing page (verified live) NextResponse.redirect defaults to 307, which preserves method and body. The dev server log showed POST /pl/robot 200 after the unlock: the page rendered from a POST carrying pin=1234 in its body, and with a locale redirect chain the body travels once more. Reloading the landing page in a browser prompts to resubmit the form.
    • A non-form body crashed the proxy (verified live) request.formData() throws a TypeError for any body that is not multipart/form-data or application/x-www-form-urlencoded. It was not caught; a POST with a JSON body returned a 500 page.
    • No attempt budget Nothing counted failures. A four-digit PIN is ten thousand requests to a handler that costs a hash each; a laptop does that in minutes. The host app had a rate-limit helper used by its API routes; the gate did not call it.
    Show 7 more
    • Plain === on the PIN and on the cookie Both comparisons short-circuit on the first differing character.
    • The pages-only matcher published the route list (verified live) The matcher excluded /api and every path with a dot. Without a cookie, /sitemap.xml returned 200 and listed every URL of the unlaunched site; the OG image and the API routes were reachable as well.
    • The digest was recomputed on every request gateToken(SITE_PIN) ran once per request for a value that never changes.
    • One language, brand hardcoded, on a multi-locale site The gate page carried the site's brand as a literal and spoke one language to every visitor, whose locale cookie was right there in the request.
    • A whitespace PIN armed a gate nobody could open SITE_PIN=" " passed the truthiness check, but submitted PINs were trimmed, so nothing ever matched.
    • Return path escaped for " only The hidden field escaped double quotes and nothing else. In a double-quoted attribute that is enough to stay inside the attribute, so no exploit was found; it is still one function away from correct.
    • status: error ? 401 : 401 Cosmetic; both branches were 401. Recorded because it is the kind of line that gets "fixed" into a wrong status. 401 on both is right.
Read the full record in provenance.md ›

What are the 6 rules the module never breaks?

Every module built from this skill holds these, whoever builds it. The same list is in the skill's README and SKILL.md, so the agent reads it before it writes a line.

  1. The return path is resolved against the request origin

    , never checked with startsWith('/'). A path that begins with a slash can still leave the site, even after one resolve (/a/..//evil.example normalises to //evil.example), so the sanitiser resolves the value, compares origins and checks the result again. Five off-origin path cases are in the suite.

  2. The unlock redirect is a 303.

    A 307 tells the browser to repeat the request with its method and body, so the PIN would be posted again to the landing page, and once more through any locale redirect. The status is asserted on the success path.

  3. The cookie is an HMAC keyed by a server-side secret

    , not the PIN and not an unkeyed hash of it. A cookie an operator can read is a cookie an operator's laptop can leak. A token derived without the secret is rejected in the suite.

  4. Both comparisons are constant-time

    over equal-length digests, so neither the PIN check nor the cookie check leaks its answer through how long it took.

  5. A body that is not a form is answered, never thrown.

    request.formData() rejects anything that is not form-encoded, and an unhandled rejection in the proxy is a stack trace instead of a rejected attempt. The 400 path is in the suite.

  6. The matcher is chosen on purpose, and the unlock path sits outside every locale prefix and app route.

    Whatever the matcher excludes is public, and so is whatever a bypass list beside a shared matcher names, including the sitemap and OG images unless you say otherwise. Build assets stay outside it: gating _next/static hides nothing from a crawler, which never holds the cookie. A colliding unlock path would never render, because the gate answers it before routing.

When should you use it, and when not?

Use it for

  • A public-by-default Next.js site that must be hidden for a while: before launch, during a client or investor review, on a staging domain. Everyone who should see it can be told one PIN. The gate sits in proxy.ts (Next 16) or middleware.ts (Next 13 to 15) and is removed by unsetting one env var.

Not for

  • Real users with real accountsInsteadThe host's auth: Clerk, NextAuth, Supabase Auth. A shared PIN cannot be revoked for one person
  • Per-route or per-tenant authorizationInsteadThe host's authorization layer; this gate is all-or-nothing by path matcher
  • Locking preview deployments for team members on Vercel, with no webhooks to receiveInsteadVercel Deployment Protection, which does it with no code. It refuses webhooks and other machine callers; the comparison is in references/operations.md
  • Protecting an API consumed by machinesInsteadAn API key; a cookie and an HTML form are the wrong shape

How do I install it?

One command. The skills.sh CLI installs the skill into every skills-compatible agent it finds.

$ npx skills add timerise-ai/site-pin-gate

Claude Code

Invoke with /site-pin-gate

Codex CLI

Invoke with $site-pin-gate

Gemini CLI

Invoke with /skills

Name the agents instead with -a, for example npx skills add timerise-ai/site-pin-gate -a claude-code -a codex. Or clone the repository into your agent's skills folder. Nothing in it is agent-specific.

What is inside the repository (8 entries)
  • SKILL.mdEntry point: architecture diagram, critical facts, hard rules, quick start, and the reference directory
  • references/adaptation.mdThe seam contract with the host app: proxy versus middleware, the machine callers, the matcher or a bypass list beside a shared one, strings, a host locale resolver and RTL, styling, cookie name and lifetime (a session cookie), a shared attempt store
  • references/module.mdConfig, token derivation, constant-time compare, the return-path sanitiser, the attempt store
  • references/handler.mdThe behaviour contract, the gate page, the request handler, the proxy wiring
  • references/operations.mdEnv vars per environment, a PIN given at invocation, smoke checks, rotation, kill switch, uninstalling, what stays public, why platform protection breaks webhooks, browser test runs, apps built from an older copy, extensions
  • references/testing.mdThe two test files, 41 tests, and how to run them under vitest or bun
  • references/provenance.mdThe engineering ledger, in two sources: what the audit of the earlier implementation changed and how the templates verify it, what was kept on purpose, what is new in the skill, and what applying it to a second host taught
  • evals/The prompts an operator types after installing (prompts.md) and one file per agent eval: the skill installed into an empty Next.js app, one prompt, no help, then type-checked, built and tested

Recent releases

  1. v0.5.0October 15, 2026

    cookieMaxAgeSeconds: null issues a session cookie: no Max-Age, so the browser drops it on close and the PIN is asked again on the next browser session. The 30-day default is unchanged. adaptation.md carries the variant and what a session cookie does not do (open windows, session restore, cookies already issued); one handler test, and the unlock test now asserts the default Max-Age. The suite is 41 tests.

  2. v0.4.0October 5, 2026

    From applying the skill to a second host: the staging site of a multi-vendor marketplace, with an existing proxy, nine UI locales including Arabic, and inbound webhooks from six kinds of provider. provenance.md records it as source B.

  3. v0.3.7September 27, 2026

    Fix release, from scoring the prompt-1 agent eval runs against 0.3.6.

What do I tell my agent?

Say what you need in your own words; the skill supplies the how. These are starting points, and the ones we tested say how it went.

  1. Hide this whole site behind a PIN until launch. One environment variable turns it on, and search engines must not index anything while it is on.

  2. Lock our staging deployment for a client review with a shared PIN, and slow down anyone who keeps guessing wrong.

    No data store
  3. Audit the password gate in our middleware.ts.

How does it do in each agent?

We install the skill into an empty Next.js app, give the agent one of the prompts above and no further help, then type-check, build and run the tests it left behind. Nothing is fixed by hand before the checks, and a failing run is published like a passing one. The procedure and every result are public, and the first prompt runs again before each release.

  • Gemini CLI0.64.0

    gemini-3.8-flash

    Built, checks pass
    Hide this whole site behind a PIN until launch. One environment variable turns it on, and search engines must not index anything while it is on.
    Typecheck: passBuild: passTests: pass
    Time
    5 min
    Changed
    13 files, +2,017 lines
    Stack
    No data store
    Skill
    v0.5.0
    Run
    Oct 15, 2026

    Result file ›Agent log ›

  • Codex CLIcodex-cli 0.162.1

    gpt-6.1-sol

    Built, checks pass
    Hide this whole site behind a PIN until launch. One environment variable turns it on, and search engines must not index anything while it is on.
    Typecheck: passBuild: passTests: pass
    Time
    2 min
    Changed
    15 files, +2,082 lines
    Stack
    No data store
    Skill
    v0.5.0
    Run
    Oct 15, 2026

    Result file ›Agent log ›

  • Claude Code2.1.300

    claude-opus-5-5

    Built, checks pass
    Hide this whole site behind a PIN until launch. One environment variable turns it on, and search engines must not index anything while it is on.
    Typecheck: passBuild: passTests: pass
    Time
    2 min
    Changed
    13 files, +2,015 lines
    Stack
    No data store
    Skill
    v0.5.0
    Run
    Oct 15, 2026

    Result file ›Agent log ›

How long does it take, and what does it cost?

We quote this module per project. The price depends on what it has to connect to. The path to a number is short and free:

  1. Step 1

    Brief

    Tell us what the module must connect to. Takes minutes, in a chat.

  2. Step 2

    Prototype in 48 hours

    A clickable prototype of your system and a quote, at no cost.

  3. Step 3

    Build and handoff

    One project price. Source code, documentation and IP are yours.

Two ways to get Site PIN gate

Build it yourself

Install the skill. Your own agent builds the module.

  • MIT licensed, no strings
  • Runs in Claude Code, Codex CLI and Gemini CLI
  • The same rules our engineers build by
$ npx skills add timerise-ai/site-pin-gate

Build it with Timerise

Send a brief. We build Site PIN gate into a system you own.

  • Clickable prototype and a quote within 48 hours, free
  • One project price, no subscription, no commission
  • Source code, documentation and IP handed over
3 years of support included.

Generated from the skill's own files at commit a22a50d. Every rule above links to where the repository says it. All skills