Universal installation
This guide works for any site. Framework-specific notes live in Integrations.
Open the hosted installer
It opens in a new tab. Your Cloudflare password is never seen by StaticLayer — the wizard only asks for a one-time OAuth consent or an API token.
Prefer the terminal? The same wizard also runs locally via npm run dev:installer (→ http://localhost:8788).
Three ways to install
- Web installer (recommended) — a guided browser wizard that creates the Worker, the D1 database and the secrets for you. It supports two connections: one-click OAuth or a pasted API token. No tokens stored on disk; the OAuth token is revoked after the deploy. Start it with one command and follow the browser flow:
npm run dev:installer(→http://localhost:8788). - CLI —
npx staticlayer initwith an API token. More control, still fully scripted. - Manual dashboard — for tinkerers who prefer clicking through the Cloudflare dashboard. See below.
Web installer, step by step
- Start it: one command (
npm run dev:installer) launches a small local app athttp://localhost:8788. There is no email step — just press Start →. From here on, everything happens in the browser. - Connect Cloudflare — choose a method: (A) one-click OAuth: the app redirects you to the official Cloudflare consent screen with the minimal scopes (Workers deploy, D1, account read). (B) paste an API token (permissions: Workers Scripts: Edit, Cloudflare D1: Edit, Account Settings: Read). Your Cloudflare password is never seen by StaticLayer; pasted tokens are kept only in memory.
- Pick your account: choose the Cloudflare account to deploy into.
- Review the plan (optional but recommended): enable password-less admin sign-in. GitHub OAuth (free, no credit card, no Zero Trust plan): create a GitHub OAuth App (guide in your admin → Settings → Admin access & sign-in), then enter its Client ID, the Client Secret and your GitHub user id in the wizard — the Worker is pre-configured with
GITHUB_CLIENT_ID/GITHUB_ADMIN_IDSandGITHUB_CLIENT_SECRETis stored as a secret. The admin login then shows "Sign in with GitHub". (Cloudflare Access — "Sign in with Cloudflare" — is also supported.) - Deploy: the app generates the three secrets locally, creates the D1 database, deploys the Worker and binds the secrets via the Bulk Secrets API, then verifies the deployed state.
- Cleanup: the OAuth token is revoked immediately; a pasted token is cleared from memory. Nothing is stored on disk — no token, no secrets.
A hosted, zero-terminal installer is available as a Cloudflare Worker (apps/installer-worker): deploy it once and anyone can install from the browser with no terminal. It needs a registered public Cloudflare OAuth app — see apps/installer-worker/README.md.
client_secret_post exchange needs a confidential server holding the OAuth app secret; a static GitHub Pages page cannot hold that secret safely. Second, api.cloudflare.com does not allow browser cross-origin calls (no CORS), so even the API-token path cannot run purely client-side. The local app is the confidential server — it runs on your machine, secrets never leave it, and the Cloudflare token is revoked after deploy. A hosted installer on a Cloudflare Worker removes the local step — it ships in apps/installer-worker/ and deploys in a few commands (see its README).
Alternative — manual install in the Cloudflare dashboard
No terminal, no local app, no installer. You do everything in the Cloudflare dashboard (about 10 minutes the first time):
- D1: Dashboard → D1 → Create database → name it
staticlayer→ copy its ID. - Worker: Dashboard → Workers & Pages → Create → Worker → paste the runtime source (or upload the built bundle) → set compatibility date to the release's date.
- Bindings: in the Worker → Settings → Variables → add D1 binding
DB, the rate-limit binding, and the vars (POW_DIFFICULTY,ALLOWED_ORIGINS, …). - Secrets: add the three secrets
ADMIN_SECRET,SESSION_SECRET,POW_SECRETwith strong random values (e.g.openssl rand -hex 32). - Migrations: run the two migration files against the D1 database (Dashboard → D1 → Console, or the CLI).
- Route: add a custom domain or route (e.g.
comments.yourdomain.com).
Then follow steps 5–9 below to add the widget, choose article IDs and moderate. The automated installer and CLI exist to replace this manual work with a verified, repeatable process.
Both paths produce the same desired state: Worker + D1 + three secrets (ADMIN_SECRET, SESSION_SECRET, POW_SECRET) + rate-limit binding. Secrets are generated locally and sent straight to Cloudflare via the Bulk Secrets API — they are never shown to you and never stored on disk.
1 · Deploy the Worker
Run npx staticlayer init (see Quick start). The CLI creates the D1 database, deploys the Worker and binds the three secrets via the Bulk Secrets API. It then verifies the desired state.
2 · Create & bind D1
Handled automatically by the CLI. Apply the schema migrations to the remote database:
npx wrangler d1 migrations apply staticlayer --remote -c wrangler.jsonc
3 · Configure secrets
Exactly three secrets: ADMIN_SECRET, SESSION_SECRET, POW_SECRET. Values are generated locally and sent to Cloudflare; they are never stored on disk by StaticLayer.
4 · Configure the allowed origin (CORS)
When your site and the Worker are on different origins (e.g. GitHub Pages → a *.workers.dev endpoint), the Worker must allow your site origin. Configure the allowlist in your deployment (see Security) and reject unexpected origins. No wildcard for authenticated routes.
Reactions are configured through plain vars in the same file when you want to change the defaults: REACTION_OPTIONS (e.g. "👍,❤️,🎉"), REACTION_DIFFICULTY_BASE/CEILING, REACTION_ESCALATION_VOTES, REACTION_MIN_INTERVAL_SECONDS. See Integrations → Reactions.
5 · Add the widget
<div data-staticlayer
data-api="https://comments.example.com"
data-article-id="/blog/my-post"></div>
<script src="https://comments.example.com/widget.js" defer></script>
data-article-id defaults to window.location.pathname when omitted.
6 · Choose the article ID
- Automatic:
articleId = window.location.pathname— one comment thread per URL. - Explicit:
data-article-id="post-123"— useful for client-side routing, translated sites, URL migrations and aliases.
Each unique article ID gets its own moderated thread. Put the same snippet on every page where you want comments, with the matching data-article-id (or rely on the URL). Example across two pages:
<!-- /blog/hello-world.html -->
<div data-staticlayer
data-api="https://comments.example.com"
data-article-id="/blog/hello-world"></div>
<script src="https://comments.example.com/widget.js" defer></script>
<!-- /blog/another-post.html -->
<div data-staticlayer
data-api="https://comments.example.com"
data-article-id="/blog/another-post"></div>
<script src="https://comments.example.com/widget.js" defer></script>
7 · Open the admin
Visit https://comments.example.com/admin.html and sign in with your admin password. If you deployed with the wizard it showed the password exactly once at the end (with a copy button); the CLI lets you set your own. The console shows one moderation queue for all articles: approve or delete each pending comment. Approved comments appear on their article immediately; published comments are also listed for reference. Deleted comments are removed from the database.
8 · Test a comment
Post from your site: the browser solves a proof-of-work, the comment enters moderation, and you approve it from the admin. Comments are plain text + emoji only — no HTML, no Markdown, no clickable links.
9 · Deploy the website
Deploy your static site as usual. No build-time integration is required — the widget loads at runtime.
10 · Telegram alerts (optional)
Get a private notification on Telegram whenever a comment enters the moderation queue. Privacy-first: the alert contains no comment content — only "a new comment awaits moderation" plus a link to your admin console. Your bot token and chat id are stored only in your own private D1 database; nothing is sent to us.
- Create your bot: in Telegram, search @BotFather → /newbot → choose a name and a username → copy the token it sends you (format
123456789:AA…). - Find your chat id: message your bot once (e.g. "hi"), then open
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdatesin a browser and copy the number in"chat":{"id":123456789,…}(channels/groups have a leading minus). - Enable alerts: open your admin → Settings → Telegram alerts → set Alerts = On, paste the bot token and chat id → Save settings.
- Test: post a comment on your site — you'll get the notification with a link to the queue.
Alerts are off by default and never expose comment data. You can disable them anytime from the same settings.
11 · Polls (optional)
StrawPoll-style polls for any page. In the admin open the Polls tab, enter the article path, the question and 2–10 options, optionally enable one vote per browser and/or Multi-select, then copy the poll snippet into your page:
<div data-staticlayer
data-endpoint="https://comments.example.com"
data-poll-id="<poll id from the admin>"></div>
<script src="https://comments.example.com/widget.js" defer></script>
Votes are anonymous (no IP, no cookies) and protected by the same Proof-of-Work + anti-replay as comments. Results are ranked (highest first) with the leader highlighted and a total-votes chip. With Multi-select a visitor picks several options and casts one vote (single PoW) for the whole set. The optional one vote per browser guard stores an anonymous token only in the visitor's browser (no personal data) and, on multi polls, offers Change your votes (revoke + vote again). Honest limitation: a visitor who clears browser storage can vote again — same trade-off as every anonymous poll (StrawPoll included).
Global poll: leave the article path empty in the admin to make a poll appear on any page where you embed its snippet. Optional attributes: data-poll-style="bars|percent|counts|minimal" (result display) and data-poll-results="after|always" (when to reveal results).
12 · Nested replies
Comments support replies up to 3 levels. Click Reply on any comment — the same Proof-of-Work and moderation pipeline applies. A reply is public only when it and its parent are approved; if a parent is deleted, its replies stay visible with a "comment removed" placeholder. The admin queue shows a ↳ reply to badge so you can moderate threads in context.
Owner replies: from the admin (Queue or Published) press Reply to answer as the site owner — the reply appears immediately with an Author badge. Set your nickname in Settings → Owner nickname (default "Site owner").