The problem
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.
The module
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
Provenance
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 earlier implementation this module was audited against was a temporary site-wide PIN gate on a multi-locale App Router marketing site, kept private ahead of its launch: a self-contained block in proxy.ts, run before locale routing, with a SITE_PIN env var, a cookie holding a SHA-256 of the PIN, and an inline HTML form posting to /__unlock. The architecture here is that block's. The templates are not a transcription of it, and the ledger below is why.
The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.
Fixed in the templates
- The unlock form redirected off-site (verified live) The return path was accepted if it started with
/.//evil.example/xstarts with/. Posting the correct PIN with that asnextproducedlocation: http://evil.example/x;/\evil.exampleand///evil.examplebehave the same in the URL parser. A page anywhere could auto-submit a wrong PIN with a hostilenext, 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 markedinputmode="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.redirectdefaults to307, which preserves method and body. The dev server log showedPOST /pl/robot 200after the unlock: the page rendered from a POST carryingpin=1234in 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 aTypeErrorfor any body that is notmultipart/form-dataorapplication/x-www-form-urlencoded. It was not caught; aPOSTwith 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 moreShow fewer
- 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
/apiand every path with a dot. Without a cookie,/sitemap.xmlreturned 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.
- The unlock form redirected off-site (verified live) The return path was accepted if it started with
Non-negotiables
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.
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, so the sanitiser resolves the value and compares origins. Three off-origin cases are in the suite.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.
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.
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.
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.The matcher is chosen on purpose, and the unlock path sits outside every locale prefix and app route.
Whatever the matcher excludes is public, including the sitemap and OG images unless you say otherwise. Build assets stay outside it: gating
_next/statichides nothing from a crawler, which never holds the cookie. A colliding unlock path would never render, because the gate answers it before routing.
Fit
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) ormiddleware.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 VercelInsteadVercel Deployment Protection, which does it with no code. 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
Build it yourself
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-gateClaude 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 directoryreferences/adaptation.mdThe seam contract with the host app: proxy versus middleware, the matcher, strings, styling, cookie name, a shared attempt storereferences/module.mdConfig, token derivation, constant-time compare, the return-path sanitiser, the attempt storereferences/handler.mdThe behaviour contract, the gate page, the request handler, the proxy wiringreferences/operations.mdEnv vars per environment, a PIN given at invocation, smoke checks, rotation, kill switch, uninstalling, what stays public, extensionsreferences/testing.mdThe two test files, 36 tests, and how to run them under vitest or bunreferences/provenance.mdThe engineering ledger: what the audit of the earlier implementation changed and how the templates verify it, what was kept on purpose, and what is new in the skillevals/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
- v0.3.4September 27, 2026
Fix release, from reading the prompt-1 agent eval runs against 0.3.3.
- v0.3.3September 27, 2026
Fix release, from reading the prompt-1 agent eval runs against 0.3.2.
- v0.3.2September 27, 2026
Documentation release. The skill content is unchanged from 0.3.1.
After installing
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.
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.
Lock our staging deployment for a client review with a shared PIN, and slow down anyone who keeps guessing wrong.
No data storeAudit the password gate in our middleware.ts.
Tested
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.
- Built, checks pass
Gemini CLI0.61.0
gemini-3.8-flash
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
- 3 min
- Changed
- 13 files, +1,949 lines
- Stack
- No data store
- Skill
- v0.3.4
- Run
- Sep 27, 2026
- Built, checks pass
Codex CLIcodex-cli 0.157.1
gpt-6-sol
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
- 3 min
- Changed
- 13 files, +881 lines
- Stack
- No data store
- Skill
- v0.3.4
- Run
- Sep 27, 2026
- Built, checks pass
Claude Code2.1.283
claude-opus-5-5
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
- 1 min
- Changed
- 14 files, +1,963 lines
- Stack
- No data store
- Skill
- v0.3.4
- Run
- Sep 27, 2026
Build it with Timerise
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:
Step 1
Brief
Tell us what the module must connect to. Takes minutes, in a chat.
Step 2
Prototype in 48 hours
A clickable prototype of your system and a quote, at no cost.
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-gateBuild 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
Generated from the skill's own files at commit 636bf69. Every rule above links to where the repository says it. All skills