










My blog has always been hosted on Cloudflare — what the community affectionately calls the “cyber Bodhisattva” — where I freeload its edge nodes and various free services (I even wrote a post about deploying website analytics on Cloudflare). As a developer constantly harassed by bots yet annoyed by traditional CAPTCHAs, I had long wanted an “invisible” solution — one that neither makes users click traffic lights and pick fire hydrants, nor fails to stop scripts.
Lately I’ve been tinkering with and maintaining a project called Lap, which is exactly such a lightweight, open-source CAPTCHA alternative. Under the hood it relies on Proof-of-Work and browser instrumentation. No images, no puzzles, no tracking — the user is verified in the background while their browser does “a little maths.”
Waiting for api.github.com...
Below I’ll walk you through, from a first-person perspective, what Lap is and how to run it on Cloudflare at zero cost.
At its heart, Lap is a CAPTCHA replacement: when someone submits your form, their browser quietly performs a series of hash computations to prove “I am not a low-cost script,” and only then is it allowed through. A few things I really like about it:
/widget.js, so it depends on no third-party CDN or npm package — no supply-chain risk.Fun fact: Lap is actually a rebrand fork I made of Cap (by tiago, under Apache-2.0), porting its originally Node + Redis/Valkey architecture into a pure Cloudflare Serverless setup and renaming the components (
<cap-widget>→<lap-widget>). All the cryptographic design and frontend interaction come from Cap’s work, so please also go give the original project a Star.
1. Browser requests a challenge POST /:siteKey/challenge
│
▼
Lap Worker issues a challenge { challenge, token } (with c/s/d params)
│
▼
2. Browser brute-forces a nonce so that sha256(salt + nonce) starts with target (this is the PoW)
│
▼
3. Browser submits the solution POST /:siteKey/redeem → Worker verifies and issues a one-time redeem token
│
▼
4. Your backend exchanges the token at POST /siteverify for { success: true }
In short: a bot would have to complete massive amounts of hashing in a very short time — extremely expensive — while a real browser spends just a moment and is barely aware of it. Every token is single-use and is consumed on first verification.
Lap offers three deployment paths: Workers + D1 (recommended), Pages Functions, and GitHub Actions for CI/CD. Below I’ll take the most common, CI-used Workers + D1 route.
What you’ll need:
git clone https://github.com/lenmei233/Lap.git lap
cd lap/cloudflare
npm install
npx wrangler login # opens a browser to authorize your Cloudflare account
npx wrangler d1 create lap-serverless
Wrangler prints a TOML block. Copy the database_id into cloudflare/wrangler.toml, replacing REPLACE_WITH_YOUR_D1_ID:
[[d1_databases]]
binding = "DB"
database_name = "lap-serverless"
database_id = "0f9c1a2b-3c4d-5e6f-7a8b-9c0d1e2f3a4b" # <- yours
binding = "DB"is the name the code reads (env.DB). Leave it unless you also changesrc/worker.js.
npx wrangler d1 migrations apply lap-serverless --remote
This creates six tables: site_keys, tokens, nonces, blocklist, ratelimit, and meta. The migration is idempotent, so re-running it is safe.
The admin key protects the /admin/* endpoints that mint site keys. It is a Worker Secret — never commit it:
npx wrangler secret put ADMIN_KEY
# paste a long random string, e.g. openssl rand -hex 32
npm run deploy # runs sync-widget, then wrangler deploy
Your Worker URL will look like https://lap-serverless.<your-subdomain>.workers.dev. Verify it:
curl https://lap-serverless.<your-subdomain>.workers.dev/health
# {"ok":true,"service":"lap-serverless","version":"1.0.0"}
If you prefer Pages, the repo’s cloudflare/functions/[[path]].js is a catch-all route that forwards requests to the same Worker logic, so both targets behave identically. The gist:
cd cloudflare
node scripts/sync-widget.mjs # generates public/ + src/assets/
npx wrangler pages project create lap-serverless --production-branch main
npx wrangler pages deploy public --project-name lap-serverless
Pages doesn’t read wrangler.toml bindings, so set them in the dashboard under Workers & Pages → your Pages project → Settings:
DB, bound to lap-serverless, added to both Production and Preview.ADMIN_KEY, value your random string, then click Encrypt..github/workflows/deploy-serverless.yml runs tests and deploys on every push to main that touches cloudflare/ or widget/. You only need to create a Cloudflare API Token with Workers Scripts: Edit and D1: Edit permissions, then add these repo secrets under Settings → Secrets:
CLOUDFLARE_API_TOKEN: the token from aboveCLOUDFLARE_ACCOUNT_ID: Account ID from Workers & Pages → Account detailsADMIN_KEY is not a GitHub Secret — it lives in the Worker, so set it once with wrangler secret put ADMIN_KEY; subsequent deploys preserve it.
After deployment you can’t use it directly yet — you first need to mint a site key for your website. Its secret is shown only once, so store it safely.
ORIGIN=https://lap-serverless.<your-subdomain>.workers.dev
ADMIN_KEY=<the key you set earlier>
curl -X POST "$ORIGIN/admin/keys" \
-H "x-admin-key: $ADMIN_KEY" \
-H "content-type: application/json" \
-d '{}'
Sample response:
{
"id": "a1b2c3d4e5f60718293a4b5c",
"secret": "9f8e7d6c5b4a39281706f5e4d3c2b1a0",
"config": { "difficulty": 4, "challengeCount": 80, "saltSize": 32 }
}
The Lap Worker hosts the widget itself, so no CDN or npm package is needed:
<script src="https://lap-serverless.<your-subdomain>.workers.dev/widget.js"></script>
<form method="POST" action="/signup">
<input name="email" type="email" required />
<lap-widget
data-lap-api-endpoint="https://lap-serverless.<your-subdomain>.workers.dev/<SITE_KEY>/">
</lap-widget>
<button type="submit">Sign up</button>
</form>
Note the trailing slash on data-lap-api-endpoint and that the path must include your site key — the widget appends challenge and redeem to it. On success the widget writes a hidden input named lap-token into the surrounding form, so your backend just reads req.body["lap-token"].
For more custom interaction you can also use the JS API:
<script src=".../widget.js"></script>
<script>
const lap = new Lap({ apiEndpoint: "https://.../<SITE_KEY>/" });
const { token } = await lap.solve();
// send the token to your backend yourself
</script>
For styling, every visual property is a --lap- prefixed CSS variable you can tweak:
lap-widget {
--lap-background: #11111b;
--lap-border-color: #313244;
--lap-border-radius: 12px;
--lap-color: #cdd6f4;
--lap-spinner-color: #89b4fa;
}
A token from the browser means nothing until verified server-side at /siteverify, and each token is consumed on first use.
curl -X POST "$ORIGIN/siteverify" \
-H "content-type: application/json" \
-d '{"secret":"<SITE_SECRET>","response":"<SITEKEY>:<ID>:<TOKEN>"}'
# {"success":true}
Node.js / Express example:
app.post("/signup", async (req, res) => {
const token = req.body["lap-token"];
const r = await fetch(`${process.env.LAP_ORIGIN}/siteverify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ secret: process.env.LAP_SECRET, response: token }),
});
const { success } = await r.json();
if (!success) return res.status(403).send("CAPTCHA failed");
// ...continue your business logic
});
Python / Flask version:
import os, requests
from flask import request, abort
@app.post("/signup")
def signup():
r = requests.post(
f"{os.environ['LAP_ORIGIN']}/siteverify",
json={"secret": os.environ["LAP_SECRET"], "response": request.form.get("lap-token")},
timeout=10,
)
if not r.json().get("success"):
abort(403, "CAPTCHA failed")
# ...continue
A site key’s config can be set at creation (POST /admin/keys) or later (PUT /admin/keys/:id). Common knobs:
difficulty (default 4): target prefix length; each +1 is ~16× the work, exponential.challengeCount (default 80): number of sub-puzzles, linear cost.instrumentation (default false): also run a browser-behaviour challenge.blockAutomatedBrowsers (default true): reject headless/automated browsers (needs instrumentation).ratelimitMax / ratelimitDuration: per-IP rate limit, default 30 / 5s.Tuning tip: the default
difficulty:4+challengeCount:80takes about a second on a modern laptop. When you want it stricter, raisechallengeCount(linear cost) beforedifficulty(exponential cost) — it gives smoother, more predictable timing.
Local debugging needs no Cloudflare account:
cd cloudflare
npm install
echo "ADMIN_KEY=dev-secret" > .dev.vars
npx wrangler d1 migrations apply lap-serverless --local
npx wrangler dev --local
The Worker runs at http://127.0.0.1:8787 against a local SQLite file, ready to use out of the box.
Common pitfalls:
{"error":"Admin key not configured"}: ADMIN_KEY isn’t set — run wrangler secret put ADMIN_KEY.no such table: site_keys: migrations weren’t applied — run the d1 migrations apply step (add --remote for production).D1_ERROR: your wrangler.toml database_id is probably still the placeholder./widget.js, then confirm <lap-widget> is spelled correctly.data-lap-api-endpoint must end in / and include the site key.What impresses me most about Lap is its “quietness” — no puzzle popups, no face scans, no cross-site tracking; it just lets the browser silently finish one computation in the background, and everything goes on as usual. Running it on Cloudflare means zero servers, zero ops, within the free tier, which is almost the perfect CAPTCHA replacement for personal blogs, forms, and small tools.
If you’re also tired of traditional CAPTCHAs and want to own your data, fork Lap and follow the steps above — ten minutes is all it takes to have your own invisible verification service. And if you could spare a Star, that would be even better (.
Waiting for api.github.com...
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。