[ Getting started ]
Deploy your app
Create the Cloudflare resources a new app binds, set its secrets, keep .env.example, .dev.vars and cloudflare.config.ts in step, and deploy from your machine or from CI.
Last updated 2026-10-19
After reading this guide, you will know
- Which Cloudflare resources a new app needs before its first deploy, and where each id goes
- How a secret moves from
cloudflare.config.tsto.env.example,.dev.varsand production - How to deploy with
sdxc deployfrom your machine and from CI
A project from sdxc create runs locally with nothing set up: bun dev simulates its database,
sessions and queue, and reads its secrets from .dev.vars. Production needs each of those to
exist in your Cloudflare account first. You do this once per app; after that, every deploy is
sdxc deploy.
The commands below use cf, the Cloudflare command
line the project installs, run through bunx so you get the project's version. They are the few
you need here; cf's own documentation covers each one in full.
Sign in
bunx cf auth login
On your machine, cf uses that login. In CI, it reads CLOUDFLARE_API_TOKEN instead, which
In CI sets up.
Create the resources the app binds
Every binding the Worker reads is declared in cloudflare.config.ts. Three of them need a
resource in your account, and two of those need its id written back into the file.
The database, which sdxc run db migrate --remote and sdxc deploy migrate:
bunx cf d1 create --name links
Put the id it prints in the DB binding:
DB: bindings.d1({ name: manifest.name, id: "<the id cf printed>" }),
The KV namespace that holds visitors' sessions:
bunx cf kv namespaces create --title links-sessions
SESSIONS: bindings.kv({ id: "<the id cf printed>" }),
The queue the job dispatcher sends to and the Worker consumes, named like the Worker:
bunx cf queues create --queue-name links
Run sdxc run typegen after editing the file, so env type checks against what the Worker
receives.
Set the site's own settings
Two values in config/constants.ts name the outside world, and both ship as placeholders:
SITE_URLis the address the sitemap,robots.txtand canonical links are built from. Set it to the domain the app answers on.MAIL_FROMis the address the app's email comes from. Cloudflare sends only from a domain with Email Routing enabled, so use an address on one.
Secrets
A secret is a value the Worker reads from env that never goes into the repository: a signing
key, an API key. Four places name each one, and each has its own job:
| Where | What it holds | Committed |
|---|---|---|
cloudflare.config.ts | bindings.secret(), the declaration | yes |
.env.example | the name, with a development value | yes |
.dev.vars | the values bun dev reads | no |
| your Cloudflare account | the production value | — |
cloudflare.config.ts is the source of truth: sdxc run typegen types env from it, and
cf deploy uploads the Worker with exactly the secrets it declares. .env.example is what a
fresh clone and CI copy to .dev.vars, which is why it holds working development values rather
than blanks.
A new app declares four. Set each one for production, with a long random value for the two
signing secrets, such as one from openssl rand -base64 32:
bunx cf workers secrets update SESSION_SECRET
bunx cf workers secrets update HONEYPOT_SECRET
bunx cf workers secrets update TURNSTILE_SITE_KEY
bunx cf workers secrets update TURNSTILE_SECRET_KEY
The two TURNSTILE_* values in .env.example are Cloudflare's test keys, which always pass.
Create a Turnstile widget for the
production pair.
Add a secret
When your code needs a new secret, such as an API key:
Declare it in
cloudflare.config.ts:STRIPE_KEY: bindings.secret(),.Add it to
.env.examplewith a development value, and to your own.dev.vars.Run
sdxc run typegen, soenv.STRIPE_KEYtype checks.Set the production value with
bunx cf workers secrets update STRIPE_KEY.
sdxc doctor fails when step 2 is missing, since CI builds .dev.vars from .env.example and
would type check without the key. In a fresh clone, copy the file yourself:
cp .env.example .dev.vars
Deploy
sdxc deploy --dry-run
sdxc deploy
The dry run builds, lists the migrations production lacks and checks the build with cf,
changing nothing. sdxc deploy does the same, asks, then applies the migrations and uploads the
build. The first deploy answers on a workers.dev address; a custom domain is a
route in cloudflare.config.ts.
Deploy covers the plan and what each failing step leaves in production.
In CI
A deploy from CI runs the same commands, with an API token in place of your login and --yes
in place of the question. Create an API token
that can deploy Workers and edit D1 databases and queues, and store it as the repository secret
CLOUDFLARE_API_TOKEN.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: cp .env.example .dev.vars
- run: bunx sdxc run typegen
- run: bun check
- run: bun run test
- run: bunx sdxc doctor
- run: bunx sdxc deploy --yes
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
sdxc runs through bunx because the project lists @sdxc/kit in its devDependencies, so
CI uses the release the project pins. sdxc doctor runs before the deploy so a secret missing
from .env.example, or a migration an empty database cannot replay, stops the run before
production changes.
Where to go next
Deploy — the plan, the flags, and what each failing step leaves behind.
Check a project — every check
sdxc doctorruns in that workflow.Configure the sdxc command — every key in
.config/sdxc.jsonc.