Skip to content

Deploy

Charcha deploys to your own Cloudflare account. Cloudflare clones the repository into your GitHub or GitLab account, creates the D1 database, asks you for the secrets below, builds, and deploys. What you end up with is a Worker on your-worker.your-subdomain.workers.dev and a repository of your own that you can keep developing from. Later pushes to it redeploy.

The button lives in the README, and points at https://deploy.workers.cloudflare.com/?url=https://github.com/withsetu/charcha.

Both want a real value, and any value you invent for either is safe.

Secret What it is
CHARCHA_DASHBOARD_PASSWORD The only credential for the moderation dashboard at /admin. No account, no reset, no second user. Generate it with openssl rand -base64 24. Unset, the dashboard refuses every request including its own login.
IP_HASH_SECRET The HMAC key that turns a commenter’s IP into the identifier the per-IP spam rate limit counts. Any long random string will do, say openssl rand -hex 32. Unset, no IP is stored at all and the per-IP half of the rate limit abstains.

Nothing else is on that form, and the reason is worth thirty seconds of your time. Cloudflare’s deploy form requires a value in every field it shows, with no way to mark one optional. For a Turnstile secret that would be a trap rather than an inconvenience: the only thing you could do without a real value is invent one, an invented Turnstile secret matches no widget, and every comment on your site would then be held or refused, quietly, until you thought to look at that field again. So the optional secrets are not collected during deploy. You set them afterwards, when you actually want them.

Six names, added one at a time in the Cloudflare dashboard under your Worker’s Settings, then Variables and Secrets. Setting a secret has the click path and the terminal equivalent for anyone who would rather type. Each takes effect on the next request, and there is nothing to redeploy.

Name Turns on
TURNSTILE_SECRET_KEY The bot check
RESEND_API_KEY Email when a comment arrives
CHARCHA_NOTIFY_FROM Email, with the two either side of it
CHARCHA_NOTIFY_TO Email, with the two above
AKISMET_API_KEY Third-party spam checking, with the one below it
CHARCHA_SITE_URL Third-party spam checking, with the one above it

Turnstile is the invisible bot check. Its secret key goes here, its sitekey goes on your own page as data-turnstile-sitekey, and you want both or neither. There is a longer version of that warning, and it is worth reading before you set one half.

The three email secrets work together or the feature stays off. A key with no recipient has nowhere to send, and Charcha holds no owner address anywhere in its schema to guess one from. CHARCHA_NOTIFY_FROM has to be on a domain verified in Resend under the same account as the key. Turning this on sends the commenter’s display name and an excerpt of their comment to Resend, who are a third party. It does not send their email address or anything derived from their IP.

The two Akismet values also work together or not at all, and they are the one thing here that changes what your readers are told about. Setting them sends every comment that gets that far to Automattic, IP address included. Read Third-party spam checking before you set either, since it lists every field that goes and has the paragraph you owe your privacy notice.

Your dashboard’s Setup tab tells you which of the first four the Worker currently has, plus IP_HASH_SECRET, as plain yes or no. The two Akismet values are not on it.

Allowed origins is a setting, not a secret

Section titled “Allowed origins is a setting, not a secret”

There is no ALLOWED_ORIGINS environment variable to set. The list lives in the database and you edit it at /admin, under Allowed origins.

It is the step a new deployment most often misses, and when it is missing the failure is total:

  • On POST /comments, a page on an unlisted origin gets HTTP 403 That origin is not allowed to comment on this site.
  • On GET /comments, the Worker still answers, but without the Access-Control-Allow-Origin header, so the browser throws the response away and the comments never render.

Add your site scheme first, with no trailing path: https://example.com. One per line. The list holds at most 20 origins and 2,048 characters in total.

Your Worker’s own address is always allowed, without being listed and without anything being written to the database, which is what makes a fresh deployment usable before you have touched a setting. Your site is a different address, so it has to be added.

This is not Turnstile’s Hostname Management screen, which governs where the Turnstile widget may render and has no bearing here. The two are easy to confuse.

Cloudflare creates the D1 database and does not migrate it, so the repository’s deploy script does:

Terminal window
wrangler d1 migrations apply DB --remote && wrangler deploy

That order and that && matter. A failed migration gives you a red build rather than a live Worker querying an empty database. The target is the binding DB rather than the database name, because the deploy form lets you rename the database.

Open / and you get a short page saying Charcha is running. That is the whole of what it says. The address is public and nobody has to sign in to reach it, so the page reports no database state, no configuration, no comment counts, and not even where the dashboard is.

Then:

  1. Sign in at /admin. The password is CHARCHA_DASHBOARD_PASSWORD.
  2. Add your site under Allowed origins.
  3. Paste the two lines into your page.

GET /health is the machine-readable check. See Health and monitoring.

Terminal window
pnpm install
pnpm wrangler d1 create charcha # then put the returned id in wrangler.jsonc
pnpm run deploy # applies migrations, then deploys
pnpm wrangler secret put CHARCHA_DASHBOARD_PASSWORD

pnpm run deploy, not pnpm deploy. The latter is pnpm’s own built-in workspace-deploy command, which shadows the script and does something else entirely. Cloudflare pre-populates a build’s deploy command from package.json, so this is also the first thing to check if a Workers Build fails while printing pnpm’s usage text. Set the deploy command to pnpm run deploy under Settings, Build.

Both are worth knowing about before you read one as something you did.

The build token and D1. wrangler d1 migrations apply exits non-zero with no useful output when its token lacks D1 permissions, which is a known wrangler issue. The token Workers Builds documents creating for itself carries Account Settings (read), Workers Scripts (edit), Workers KV (edit) and Workers R2 (edit), and D1 is not on that list. If the migration step is what failed, add D1 (edit) to the token, or apply the migrations once from a terminal with pnpm db:migrate:remote.

The ratelimits binding on the Workers Free plan. Charcha uses the Workers Rate Limiting binding to bound brute-force attempts against the dashboard login. Whether that binding is available on Free is documented nowhere: not the binding reference, not the bindings index, not the pricing page’s Free-against-Paid table, not the GA changelog. All four were checked on 2026-07-25 and the silence is real rather than unread. If the deploy fails mentioning it, deleting the ratelimits block from wrangler.jsonc makes the deploy succeed, and makes the login throttle refuse every attempt, so your dashboard stays shut. Comments carry on being accepted and queued throughout.

The package manager, if a build goes strange

Section titled “The package manager, if a build goes strange”

The Cloudflare build image ships pnpm 10.x, and the repository pins pnpm 10.x to match. Keep those on the same major. pnpm does not fail on a lockfile it considers incompatible, it prints a warning and resolves fresh, which gives you a green build that installed a dependency tree nobody locked or tested.

Workers Builds takes its pnpm version from the build image or from a PNPM_VERSION build variable in the Cloudflare dashboard. It does not read packageManager, and pnpm has no equivalent of .nvmrc, so that dashboard variable is your only lever. Node is genuinely pinned everywhere, because Workers Builds does read .nvmrc.