→ Template

Knock Codes Template

A complete, single-file restricted-access screen — dark backdrop, centered card, segmented code entry, support link, footer help text.

Client previewStandardtemplatesegmented

Best used for A polished, on-brand restricted-access page you can ship as-is, no assembly required.

The fastest way to a fully-branded, polished restricted-access page — copy one file, wire a hash and a logo, done. Paste a code across any box and it distributes automatically across the group.

Loading preview…
Hash your code

Generate a hash

Fully client-side — the plaintext code never leaves this page, and never touches a file. Only the hash below gets pasted into your project.

Installation

Add this template to your project

CLI

npx shadcn@latest add http://localhost:3000/r/react/knock-codes-template.json

Running this site somewhere other than localhost? Set NEXT_PUBLIC_SITE_URL and this command switches to that host automatically.

Also installs

  • Knock Codes Core
  • Knock Codes Types
  • useKnockCodes
  • cx (classname helper)

Files created (8)

  • components/knock-codes/core/hash.ts
  • components/knock-codes/core/verify.ts
  • components/knock-codes/core/session.ts
  • components/knock-codes/core/storage.ts
  • components/knock-codes/react/types.ts
  • components/knock-codes/react/useKnockCodes.ts
  • components/knock-codes/react/cx.ts
  • components/knock-codes/react/KnockCodesTemplate.tsx

No CLI? Copy the files by hand

  1. Open the Code tab in the preview above.
  2. Create each path listed below in your project and paste its contents in.
  3. Do the same for anything listed under “Also installs”, if present.

Installing via an AI agent?

Drop AGENTS.md into your project root — it instructs any coding agent to hash the code locally, write only the hash, and confirm the plaintext never touched a file. Thin pointer files exist for tools that read a different filename.

Props

API reference

PropTypeDefaultDescription
expectedHashstringSHA-256 hex hash to verify against, for local mode.
verifyVerifyFnCustom async verification function, for server mode.
children *ReactNodeRendered once unlocked.
logoReactNodeRendered above the heading — your own logo/wordmark. Omitted entirely if not passed.
codeLengthnumber8Total number of code boxes.
groupSizenumber4Boxes per group — a dash separates groups.
supportHrefstringRenders "Contact Support" as a link to this URL.
onContactSupport() => voidRenders "Contact Support" as a button instead of a link — e.g. to open a chat widget.
labelsKnockCodesTemplateLabelsOverrides heading, description, access-code label, support label, footer text, and every KnockCodesLabels string.
fullPagebooleantrueSet false to embed this somewhere other than a real page root (a demo, a docs preview) — drops the full-viewport (100dvh) sizing.
theme"light" | "dark"Forces light or dark presentation on its own, independent of any ancestor ".dark" class. Omit to follow the nearest ".dark" ancestor if one happens to exist.
classNamestringExtra classes on the full-page backdrop.
Notes

Accessibility

Each code box has its own aria-label ("Access code character N of 8"), the group has role="group", and Backspace/Arrow-key navigation between boxes works without a mouse. Errors and the submitting state announce through a shared aria-live="polite" region, same contract as every other block.

Customization

Everything is one file — background, card, heading, description, code entry, button, support link, and footer are all inline, not composed from Gate Wrapper/PIN Input. Pass `logo` for your own wordmark, `codeLength`/`groupSize` to change the code shape, and `labels` to override every string. For a single masked field instead of segmented boxes, use Knock Codes + PIN Input instead.

Threat model

The honest version

Knock Codes stops casual visitors, search engines, and forwarded links. Local mode does not stop anyone who opens DevTools — the hash ships in your client bundle by design. Server mode (swap one prop) stops determined visitors. This is a velvet rope, with an optional real lock — never marketed as more than that.

Server mode

Need real protection?

Local mode is deterrence — the hash ships in your client bundle by design. Swap the expectedHash prop for a verify function pointing at one of these, and the code is checked server-side instead — same markup, same component, one prop different. Each template rate-limits attempts and returns a short-lived signed token on success.

nextjs-route-handler.js
// @ts-check
/**
 * Knock Codes — Next.js route handler server verification template.
 * Drop at app/api/verify-access/route.js (App Router).
 *
 * Set two env vars: KNOCK_CODES_SERVER_HASH, KNOCK_CODES_TOKEN_SECRET.
 *
 * Wire contract identical across all three server templates — see the
 * Cloudflare Worker template in this same folder for the full contract.
 */
import { createHash, createHmac } from "node:crypto";

const RATE_LIMIT_MAX_ATTEMPTS = 5; // adjust per deployment
const RATE_LIMIT_WINDOW_SECONDS = 60;
const TOKEN_TTL_MS = 5 * 60_000; // 5 minutes

// In-memory counter — acceptable ONLY for a single-instance/dev deployment.
// Vercel and most production hosts run multiple instances, so this counter
// would not be shared across them; swap in Redis or Vercel KV (a single
// shared store, same key scheme below) before deploying for real.
/** @type {Map<string, number>} */
const attemptsByKey = new Map();

/** @param {string} input */
function sha256Hex(input) {
  return createHash("sha256").update(input, "utf8").digest("hex");
}

/** @param {string} secret */
function signToken(secret) {
  const payload = Buffer.from(JSON.stringify({ exp: Date.now() + TOKEN_TTL_MS })).toString("base64url");
  const signature = createHmac("sha256", secret).update(payload).digest("base64url");
  return `${payload}.${signature}`;
}

/** @param {string} identifier */
function checkRateLimit(identifier) {
  const windowStart = Math.floor(Date.now() / 1000 / RATE_LIMIT_WINDOW_SECONDS);
  const key = `${identifier}:${windowStart}`;
  const attempts = attemptsByKey.get(key) ?? 0;
  if (attempts >= RATE_LIMIT_MAX_ATTEMPTS) return false;
  attemptsByKey.set(key, attempts + 1);
  return true;
}

/** @param {Request} request */
export async function POST(request) {
  const identifier = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
  if (!checkRateLimit(identifier)) {
    return Response.json({ ok: false, reason: "network" }, { status: 429 });
  }

  /** @type {unknown} */
  let code;
  try {
    ({ code } = /** @type {{ code?: unknown }} */ (await request.json()));
  } catch {
    code = undefined;
  }
  if (typeof code !== "string") {
    return Response.json({ ok: false, reason: "network" }, { status: 500 });
  }

  if (sha256Hex(code) !== process.env.KNOCK_CODES_SERVER_HASH) {
    return Response.json({ ok: false, reason: "invalid" });
  }

  return Response.json({ ok: true, token: signToken(/** @type {string} */ (process.env.KNOCK_CODES_TOKEN_SECRET)) });
}

Compose with

Blocks this template is built from

Drop down to these directly once you need more control than the single-file template gives you.