Get started
Deployment
The app ships as one Cloudflare Worker. A GitHub Actions workflow deploys main
after CI passes, and pnpm deploy:cf does the same from your machine. You work
here once to create the Cloudflare resources and set the secrets, and again when
you add a binding or a domain.
How it works
deploy.yml(Licensees only) runs when the ci workflow
succeeds on main, or when you start it by hand on main.
- It checks out the commit CI tested. If
mainhas moved past that commit, it writes a line in the run summary and deploys nothing. scripts/db/deploy-preflight.mjs(Licensees only) stops the run whilewrangler.jsoncstill holds an all-zeros placeholder id orDATABASE_URLis empty, and while the Hyperdrive has query caching on.pnpm buildwrites the Worker and mergeswrangler.jsoncinto.output/server/wrangler.json.- The migrations run over
DATABASE_URL, straight to the database. wrangler deployuploads the Worker with the bindings from that file.
Any failed step ends the run. If the migration fails, the old Worker stays live on the old schema.
Create the Cloudflare resources
Create a Postgres database first, with any provider Hyperdrive can reach
(Database lists what it must allow). Then create everything
wrangler.jsonc names:
pnpm exec wrangler hyperdrive create nuxter --caching-disabled --connection-string="postgres://USER:PASSWORD@HOST:5432/DATABASE"
pnpm exec wrangler d1 create nuxter
pnpm exec wrangler kv namespace create CACHE
pnpm exec wrangler r2 bucket create nuxter-avatars
pnpm exec wrangler queues create nuxter-jobs
pnpm exec wrangler queues create nuxter-jobs-dlq
pnpm exec wrangler email sending enable <your-domain>
Paste the ids they print over the placeholders under hyperdrive,
d1_databases and kv_namespaces, then run pnpm cf-typegen. The D1 database
holds Nuxt Content's pages and nothing else. The last command onboards the
domain your mail is sent from, for the cloudflare mail provider
(Email).
The create commands append a new binding to wrangler.jsonc instead of
filling in the one already there, and some Wrangler versions reindent the file
with tabs. Read the diff, delete the stanza they added, and paste the id into
the existing one.
Type the connection string into the Hyperdrive command rather than exporting
DATABASE_URL. A DATABASE_URL exported in your shell outranks .env, so
pnpm dev and pnpm db:migrate run from that shell would write to production.
Set secrets and variables
In GitHub, under Settings > Secrets and variables > Actions:
| Name | Kind | Value |
|---|---|---|
CLOUDFLARE_API_TOKEN | secret | A token from the Edit Cloudflare Workers template, plus Account > Queues > Edit and Account > Hyperdrive > Read |
CLOUDFLARE_ACCOUNT_ID | secret | Your Cloudflare account id |
DATABASE_URL | secret | The database's own connection string, the one the Hyperdrive was created with. The migrations can't go through Hyperdrive |
NUXT_SITE_URL | variable | https://your-domain. Baked into the build and every mailed link |
NUXT_PUBLIC_TURNSTILE_SITE_KEY | variable | From a Turnstile widget created for your hostname |
The blog workflows use the same CLOUDFLARE_API_TOKEN, so if you run them the
token also needs their permissions (Blog).
Everything else goes on the Worker as a secret. Secrets survive deploys, so you set each one once:
pnpm exec wrangler secret put NUXT_SESSION_PASSWORD --name <worker>
--name is required because wrangler.jsonc has no name. Nitro derives one
from the origin git remote, or the package.json name without one, and
pnpm build prints it as "Using auto generated worker name".
| Variable | Needed when |
|---|---|
NUXT_SESSION_PASSWORD | Always. 32 characters or more |
NUXT_OG_IMAGE_SECRET | Always. Any random string. Unset, each build signs OG image URLs with its own, and older URLs answer 403 |
NUXT_TURNSTILE_SECRET_KEY | Auth or the contact page is on. Forms skip the check until both Turnstile keys are set, and /contact answers 503 |
NUXT_EMAIL_FROM, NUXT_EMAIL_FROM_NAME | Email is on. Without a sender every mail is refused with a 501, so nobody can verify an address |
NUXT_PAYMENTS_STRIPE_SECRET_KEY, NUXT_PAYMENTS_STRIPE_WEBHOOK_SECRET, NUXT_PAYMENTS_STRIPE_REFS_* | Payments are on, as shipped. Checkout answers 500 until they are set (Payments) |
NUXT_OAUTH_<PROVIDER>_CLIENT_ID, NUXT_OAUTH_<PROVIDER>_CLIENT_SECRET | One pair per provider in auth.oauth, which ships empty |
NUXT_MONITORING_<PROVIDER>_DSN | monitoring.enabled is on (Observability) |
NUXT_CONTACT_ADDRESS | The contact page is on, as shipped. /contact answers 503 until it is set (Contact) |
NUXT_BLOG_AUTHOR_EMAIL | Optional. Read per request, so changing it needs no rebuild |
.env.example(Licensees only) lists every variable the kit reads and where
each value comes from. After a cold start the Worker logs one [env] line
naming everything starter.config.ts turns on that this deploy has no
credentials for. Read it in wrangler tail after the first deploy.
Add your domain
The kit ships no routes, so the Worker answers on its *.workers.dev address.
Add your domain at the top level of wrangler.jsonc:
"routes": [{ "pattern": "example.com", "custom_domain": true }],
wrangler deploy creates the DNS record and the certificate, but only for a
zone in the same Cloudflare account. Anywhere else the deploy fails. www needs
a second entry or a redirect rule, or it won't resolve.
Before the first deploy on that zone, turn off two settings Cloudflare enables by default:
- Speed Brain (Speed > Settings > Content Optimization). Cloudflare answers its
prefetches only from cache and never from a Worker, so every prefetched page
and
/_nuxt/chunk gets a 503. - Web Analytics automatic setup (Web Analytics > your site > Manage site). It injects a beacon script that the kit's Content-Security-Policy blocks, so every page logs a CSP violation. The kit's own analytics run on the server (Analytics).
Point NUXT_SITE_URL at the domain and deploy again.
Deploy from your machine
cp .env.production.example .env.production
pnpm deploy:cf
It runs the same steps as the workflow: preflight, build, migrate, deploy.
.env.production holds DATABASE_URL, NUXT_SITE_URL and
NUXT_PUBLIC_TURNSTILE_SITE_KEY, and the build reads it instead of .env, so
the http://localhost:3000 there never ends up in a mailed link. Wrangler
deploys with your wrangler login, or with CLOUDFLARE_API_TOKEN when it is
set. pnpm db:migrate:remote and pnpm db:studio:remote read the same file.
Ship a migration safely
Migrations run before the new Worker is uploaded, so for a short time the old code runs against the new schema. Adding tables, columns and indexes is safe. Dropping or narrowing something the live code still uses, such as a column it reads or the unique index an upsert names as its conflict target, breaks the old Worker until the new one lands, and for good if the deploy fails.
Split such a change across two releases: add the new shape and deploy, then drop the old one in the next pull request.
There is no rollback step. Revert the commit on main and let it deploy.
wrangler.jsonc
wrangler.jsonc(Licensees only) declares every binding the Worker gets.
Run pnpm cf-typegen after you add or rename one.
| Key | Binding | What it is for |
|---|---|---|
hyperdrive | HYPERDRIVE | Postgres. localConnectionString is the database pnpm preview uses |
d1_databases | DB | Nuxt Content's pages. No app table lives there |
kv_namespaces | CACHE | Nitro's cache, behind the swr route rules such as /blog |
r2_buckets | AVATARS | Uploaded avatars. The build checks that every storage.purposes entry names a bucket declared here |
queues | QUEUE | Background jobs and their dead letter queue (Background jobs) |
durable_objects, migrations | AUTH_THROTTLE | The counters behind requireRateLimit and sign-in lockout (Rate limiting) |
ratelimits | AUTH_RATE_LIMIT, ANALYTICS_RATE_LIMIT | Coarse limits in front of those counters and the analytics ingest. period takes only 10 or 60, and namespace_id is any number you pick |
analytics_engine_datasets | ANALYTICS | The cloudflare analytics sink. The dataset is created on its first write |
ai | AI | Workers AI. Always remote, so calls from pnpm dev are billed too (AI) |
send_email | EMAIL | The cloudflare mail provider |
triggers.crons | none | When scheduled tasks run. The build fails without one while auth or activity retention is on |
name, main and assets are left out because nitro sets them.
compatibility_flags is empty, but the build adds nodejs_compat, which the
kit needs: the pg driver and runtime config both depend on it.
observability.enabled turns on Workers Logs, and keep_vars keeps plain-text
variables you set in the dashboard across deploys.
Configuration
| Key or variable | Where | Default | What it does |
|---|---|---|---|
name | wrangler.jsonc | derived by nitro | Pins the Worker's name |
routes | wrangler.jsonc | none | Custom domains. Without one the Worker is on *.workers.dev |
NUXT_SITE_URL | GitHub variable, .env.production | none | The site's URL, read at build |
NUXT_PUBLIC_SITE_URL | Worker variable | none | Overrides NUXT_SITE_URL without a rebuild |
NUXT_IMAGE_PROVIDER | build environment | none | Turns on image optimization with a @nuxt/image provider, such as cloudflare |
DATABASE_URL | GitHub secret, .env.production | none | Where the deploy migrates. The Worker itself never reads it |
Good to know
- Two shipped defaults need the Workers Paid plan. On Free, the
cloudflaremail provider delivers only to addresses verified in your account, so listresendinemail.providersinstead. Password hashing takes about 17 ms of CPU against Free's 10 ms limit, so steady password sign-ins end in Error 1102. - Images ship unoptimized: a 4 MB file in
public/reaches every visitor at 4 MB.NUXT_IMAGE_PROVIDER=cloudflareat build time needs Image Transformations on the zone and does nothing on*.workers.dev. - The first deploy starts with an empty database. The first account to sign up and verify its address becomes superadmin, so register yours right away.
- The preflight's caching check needs Account > Hyperdrive > Read on the token.
Without it the run logs a warning and deploys anyway, so check the setting
once with
pnpm exec wrangler hyperdrive get <id>. pnpm devruns on Node, not on workerd. Try Worker-only behaviour withpnpm preview, which builds the Worker and runs it underwrangler devagainst a real Postgres (Database).
A link with a lock opens the kit’s private repository, which every licensee can read.