wrangler.toml explained
wrangler.toml tells Cloudflare what your Worker is called, where it lives, and which database, file storage and settings it uses. This page goes through the example file that comes in the download, wrangler.toml.example, part by part. It is for whoever deploys to Cloudflare. If you have not yet deployed, start at Deploy to Cloudflare Workers.
Your wrangler.toml is yours. The download contains only wrangler.toml.example, so unpacking an update never overwrites your copy. Keep your copy out of any public repository: it holds your account and resource ids.
The top of the file
name = "choir-manager"
compatibility_date = "2025-01-01"
main = "server/entry/cloudflare.js"
# account_id = "YOUR_ACCOUNT_ID"
| Line | What it does |
|---|---|
name | The Worker's name in your Cloudflare account. choir-manager is the product's older name. If you change the name later, Cloudflare treats it as a different Worker. |
compatibility_date | Pins the version of the Workers runtime's behaviour that the code was written for. Leave it as it is. |
main | The server's entry file in the download. Wrangler bundles it, with what it imports, when you deploy. Leave it. |
account_id | Your Cloudflare account, so Wrangler never has to guess which one you mean. Get it with npx wrangler whoami. |
Keeping it off workers.dev
workers_dev = false
preview_urls = false
Without these two lines, a deploy also publishes your site at an address under workers.dev, and for preview versions, backed by your real data. Keep both false, so the site is reached only at your own domain.
Your domain
routes = [{ pattern = "choir.example.org", custom_domain = true }]
pattern is the bare domain, and custom_domain = true makes Cloudflare serve the Worker for that whole name and set up its DNS record. The DNS zone must be on the same Cloudflare account. More in Cloudflare: your domain and public files.
The website files
[assets]
directory = "./dist"
binding = "ASSETS"
not_found_handling = "single-page-application"
run_worker_first = ["/*", "!/assets/*", "!/images/*", "!/favicon.svg"]
The built website in dist/ is served as the Worker's static assets, so the site and its API share one address.
| Line | What it does |
|---|---|
directory | Where the built website is. It is already built in the download. |
binding = "ASSETS" | Lets the server code read the built page, so that it can write your choir's name into it. The name must be exactly ASSETS. New in 1.6.12. |
not_found_handling = "single-page-application" | An address that is not a file gets index.html, which lets the site's own pages (such as /admin/login) work. |
run_worker_first | Which requests go to the server code before being looked up as files. "/*" sends everything; each entry starting with ! is an exception, so the build's own files (/assets/*, /images/* and /favicon.svg) are still served by Cloudflare without the Worker running. Changed in 1.6.12: it used to be ["/api/*", "/files/*"]. |
Your choir's name in the page
A link to your site shown in Facebook, Messages, WhatsApp or Slack, and what some search engines read, is made from the page as it is sent, before any of its script has run. So from version 1.6.12 the Worker writes your choir's name, description, favicon and link-preview tags into the page on its way out. What they are made from is in How your site looks when a link is shared.
That needs both of the lines marked above: binding lets the Worker read the built page, and run_worker_first sends pages to it. Change the two together.
A wrangler.toml made before 1.6.12 has run_worker_first = ["/api/*", "/files/*"] and no binding. Your wrangler.toml is never changed by an update, so a site that was installed earlier still has the old lines until you edit them. It goes on working exactly as it did.
Your [assets] section has | What happens |
|---|---|
| Neither change (the old lines) | The site works as before. Pages are served without the Worker, titled "Choir" until the page has loaded in a browser, so link previews and some search engines show "Choir". The Worker's log says once for each running copy: [config] wrangler.toml has no ASSETS binding: link previews and search engines are shown "Choir" in place of the site's name. Add the two [assets] lines in docs/DEPLOY_CLOUDFLARE.md |
binding added, run_worker_first left as it was | Nothing changes. Pages still never reach the Worker, so they are still titled "Choir". The log line above no longer appears, so nothing tells you it is half done. |
The new run_worker_first, but no binding | Every page answers {"success":false,"error":"Not Found"}, because pages now reach a Worker that has nothing to answer them with. The API still works. Add binding = "ASSETS" and deploy again. |
| Both | Each page is sent with your choir's own title, description, icon and preview tags. |
After changing the lines, run npx wrangler deploy.
What it costs
Each page viewed is then one request to your Worker and one read of your site's settings from D1 (a single SELECT on the settings table), where before a page was neither: only its API calls were. A page view already makes several API calls, so this adds little to the free plan's daily allowance of 100,000 requests and 5 million rows read. Nothing is kept between requests, so a change of name shows on the next page.
The page never depends on it
- If the settings cannot be read, or have not come back after a second and a half, the page is sent as it was built, titled "Choir", and your name appears once the page has loaded in the browser. The log has a line starting
The page was served without the site's own title:. - If the Worker cannot start at all (a binding is missing, or
EMAIL_PROVIDERis set without its secrets), pages are still served as they were built: the API is down, andnpx wrangler tailshowsThe site could not be started, so pages are served as built and the API is down:followed by the reason, once for each running copy of the Worker. The site is not blank, but nobody can log in and no content loads until the setting is put right.
The page's own address (<link rel="canonical"> and og:url) and a preview picture kept on the site are written from SITE_URL, using its scheme and host only, and are left out when it is not set. The address a request arrived on is never used.
Bindings
A binding gives the Worker a name for one of your Cloudflare resources.
[[d1_databases]]
binding = "DB"
database_name = "choir-db"
database_id = "YOUR_D1_DATABASE_ID"
[[r2_buckets]]
binding = "PUBLIC_BUCKET"
bucket_name = "choir-public"
[[r2_buckets]]
binding = "PRIVATE_BUCKET"
bucket_name = "choir-private"
[[kv_namespaces]]
binding = "SESSIONS"
id = "YOUR_KV_NAMESPACE_ID"
| Binding | Resource | Notes |
|---|---|---|
DB | The D1 database | Required. database_name is what you passed to d1 create; database_id is what it printed. |
PUBLIC_BUCKET | An R2 bucket for public files | Required unless you use STORAGE_PROVIDER = "s3". |
PRIVATE_BUCKET | An R2 bucket for member files and rehearsal tracks | Required unless you use STORAGE_PROVIDER = "s3". Never give this bucket a public address. The Worker serves its files itself, after checking who is asking. |
SESSIONS | A KV namespace for login sessions | Optional. |
The binding names (DB, PUBLIC_BUCKET, PRIVATE_BUCKET, SESSIONS) are what the code looks for. Only the values on the other lines are yours to change.
Sessions
By default sessions are kept in KV. They are kept in D1 instead when SESSION_STORE is database, or when there is no SESSIONS binding. To skip KV, delete the [[kv_namespaces]] block and set SESSION_STORE = "database". See Sessions and how long people stay logged in.
Storage
If you set STORAGE_PROVIDER = "s3", the site stores files in the S3-compatible service you name, and the two R2 buckets are not needed. See S3-compatible storage and R2 on Cloudflare.
Settings: [vars]
[vars]
NODE_ENV = "production"
SITE_URL = "https://choir.example.org"
# PUBLIC_FILES_URL = "https://files.choir.example.org"
EMAIL_PROVIDER = "ses"
EMAIL_FROM = "Harmony Community Choir <noreply@example.org>"
# EMAIL_REPLY_TO = "board@example.org"
# CONTACT_FORM_TO = "board@example.org"
AWS_REGION = "us-east-1"
# ADMIN_AUTH = "table"
# UPDATE_MODE = "off"
# CAPTCHA_PROVIDER = "turnstile"
# CAPTCHA_SITE_KEY = "..."
[vars] takes every setting that is not a secret: ordinary text that can be seen in the file and in the Cloudflare dashboard. Passwords and keys do not go here; they are secrets.
| Setting | Meaning |
|---|---|
NODE_ENV | production, which is also the default. |
SITE_URL | The public address, with https://. Every emailed link is built from it, and so is the address each public page gives as its own. |
PUBLIC_FILES_URL | Where browsers fetch public files, if not from the Worker. See Cloudflare: your domain and public files. |
EMAIL_PROVIDER | ses, resend, sendgrid, mailgun, postmark, log or none. smtp is not available on Workers. |
EMAIL_FROM, EMAIL_REPLY_TO, CONTACT_FORM_TO | The sender, reply-to and contact-form destination. |
AWS_REGION | For Amazon SES. |
ADMIN_AUTH | env (the default) or table. See Admin sign-in. |
UPDATE_MODE | A Worker cannot update itself, so the mode is always "notify". Set off to remove the update notice. See How updates work. |
CAPTCHA_PROVIDER, CAPTCHA_SITE_KEY | The bot check. The secret key is a secret. See Stop spam with a bot check. |
Secrets
Set with npx wrangler secret put NAME, never in this file:
| Secret | For |
|---|---|
ADMIN_USERNAME, ADMIN_PASSWORD | The admin login (optional with ADMIN_AUTH = "table"). |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY | Amazon SES. Other providers use RESEND_API_KEY, SENDGRID_API_KEY, MAILGUN_API_KEY, POSTMARK_SERVER_TOKEN (Mailgun's MAILGUN_DOMAIN and MAILGUN_REGION are ordinary settings for [vars]). |
CAPTCHA_SECRET_KEY | The bot check. |
LICENSE_KEY | To be told about new versions. |
ANTHROPIC_API_KEY | The optional writing assistant. |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | Only with STORAGE_PROVIDER = "s3". |
All other settings in Environment variables can be set the same way, as a variable or as a secret.
The [env.dev] block
The last part of the file, [env.dev] and the blocks under it, is for people who work on the product's source and run the site locally as a Worker. You do not need it, and can delete it.