Deploying development
Your project gains its Runtime Contexts in stages, and each stage ends somewhere you can stop: a
working laptop, a working pipeline, an optional rehearsal, a working product. This page puts
development on Cloudflare.
flowchart LR
L["1. Local setup<br/>Both apps on your machine"] --> D["**2. Deploying development**<br/>A push deploys"]
L --> R["3. Adding pull request previews<br/>Independent of development"]
D --> S["4. Adding staging<br/>Optional"]
S --> P["5. Going to production<br/>Your domain, real users"]
D -. "without staging" .-> P
By the end of this page, a push to development deploys both applications. Pull request previews
are a stage of their own that doesn't need this one:
Adding pull request previews works before this
page, after it, or without it. Adding staging is optional
and can wait, and Going to production comes last, once
the application and its name are final.
Everything here starts on the development branch that
Local setup had you create. Nothing here touches main.
PROJECT_SLUG that Local setup had you set,
and the first run that deploys a context creates that context's resources.Prerequisites
Everything in this section happens in a console you sign in to yourself. It's gathered here rather than spread through the steps so you open each console once.
What you need before you start
| You need | How to get it | Required |
|---|---|---|
| Local setup finished | Local setup | Yes |
| A Cloudflare account, and its account id | pnpm exec wrangler whoami prints the id, 32 hexadecimal characters | Yes |
| The Super Administrator role on that account | Creating an Account API token needs it. Otherwise, ask someone who has it | Yes |
Your workers.dev subdomain | The Workers & Pages page, beside Your subdomain | Yes — every non-production origin carries it |
A Postgres database for development | Any managed Postgres reachable from the internet, never one another context uses | Yes |
| A DNS zone you control | The value you set as PROJECT_ZONE. Add it to Cloudflare, or keep DNS access at your host | Only for outgoing email |
apps/web and apps/site Infisical projects | Set up your secret store | Yes |
The GH PRs and GH Push identities | Machine identities | Yes |
| Google, GitHub, and Polar credentials per context | Set up sign-in and payments | Only for the feature that reads them |
| A GitHub repository you can set secrets on | The one you created in Local setup | Yes |
Command-line tools
| Tool | Why |
|---|---|
| Infisical CLI | Reads and writes your secret store |
gh | Sets the repository secret and variables in one go |
wrangler | Already a dependency — pnpm exec wrangler |
Run infisical login and pnpm exec wrangler login once after installing. Both sign you in as
yourself, so nothing on this page asks you to create a personal token.
The credentials involved
These are easy to confuse, so each name below is the one used everywhere else on this page.
Cloudflare, where a person and a runner sign in differently:
| Who signs in | With | Why that method |
|---|---|---|
| You, at a terminal | pnpm exec wrangler login, a browser sign-in as yourself | No token to create, store or leak |
| GitHub Actions | the <slug>-github-actions Account API token, as CLOUDFLARE_API_TOKEN | A runner cannot open a browser |
The project uses no User API token. A User API token belongs to one person and stops working when that person leaves the account, and the deploy stops with it. An Account API token belongs to the account.
Infisical, where each of these signs in its own way:
| Who signs in | As | Why that method |
|---|---|---|
| You, at a terminal | your own infisical login session | The commands on this page write secrets as you |
| The applications on your machine | your machine identity, Universal Auth | Local setup connected it |
| A pull request build | the pull request identity (GH PRs), OIDC | The runner mints a token per run |
| A push deploy | the push identity (GH Push), OIDC | The runner mints a token per run |
Connect the repository to Cloudflare
Every deployed context shares these settings, so you set them once — here, or from Adding pull request previews if you reach that stage first. They use the account, the role, the two identities and the command-line tools from Prerequisites.
The Cloudflare API token
This is the deploy's only Cloudflare credential. Create an Account API token, not a User one — the credentials involved says why.
Go to Manage Account > Account API Tokens > Create Token, name it
<slug>-github-actions, and give it these permissions, each scoped to the entire account:
| Permission group | Level | Why |
|---|---|---|
| Workers Scripts | Edit | Deploys each Worker and each pull request preview, and deletes a closed pull request's preview |
| Workers R2 Storage | Edit | Finds or creates apps/web's bucket |
| D1 | Edit | Finds or creates apps/site's content database |
| Hyperdrive | Edit | Finds, creates or updates apps/web's pool |
Each resource type the deploy creates needs its own Edit permission, and your bindings decide the
set: hub.blob and hub.db in apps/web/nuxt.config.ts, and d1_databases in
apps/site/nuxt.config.ts. A project without apps/site can leave out D1, and a binding of a new
resource type adds a row. Cloudflare's Edit Cloudflare Workers template grants far more, including
write on Workers Routes for every zone, so build the token from the rows above instead.
Authentication error [code: 10000], and apps/site deploys cleanly without R2, so
the fault looks intermittent.Leave the expiration empty, then Continue to summary > Create Token. Account owned tokens is the fallback if a screen has moved.
Cloudflare shows the value once, and it goes straight into the CLOUDFLARE_API_TOKEN secret below.
Lose it and you create another, then delete this one.
The secret and the variables
In your repository on GitHub, go to Settings > Secrets and variables > Actions. Add each row under the tab its Kind names:
| Kind | Name | Holds |
|---|---|---|
| Secret | CLOUDFLARE_API_TOKEN | The <slug>-github-actions token from the step above |
| Variable | CLOUDFLARE_ACCOUNT_ID | The account both Workers live in |
| Variable | INFISICAL_GH_PRS_IDENTITY_ID | The pull request identity, trusted for repo:<org>/<repo>:pull_request, granted preview |
| Variable | INFISICAL_GH_PUSH_IDENTITY_ID | The push identity, trusted for refs/heads/…, granted development, staging and production |
Or set every row with gh. Set REPO to the repository you mean, replace each placeholder, then
paste the block:
REPO=<owner>/<repo>
gh secret set CLOUDFLARE_API_TOKEN --repo "$REPO" --body '<the <slug>-github-actions token>'
gh variable set CLOUDFLARE_ACCOUNT_ID --repo "$REPO" --body '<your Cloudflare account id>'
gh variable set INFISICAL_GH_PRS_IDENTITY_ID --repo "$REPO" --body '<the pull request identity id>'
gh variable set INFISICAL_GH_PUSH_IDENTITY_ID --repo "$REPO" --body '<the push identity id>'
--repo, gh
writes to whatever clone is your working directory, and one set of values silently overwrites
another project's. A wrong Cloudflare token is the worst case: both projects can share one account,
so the deploy authenticates and the run goes green.Check the result with gh secret list --repo "$REPO" and gh variable list --repo "$REPO". Both
print names; only variables print values.
--body puts each value in your shell history. Drop the flag and gh reads the value from standard
input instead, so paste it at the prompt and end with Ctrl-D — or set the secret in the dashboard and
use gh for the variables, which are identifiers rather than credentials.An identity id opens nothing without a token only your runner can mint, which is why the two OIDC identities travel as variables. No Infisical client secret is stored in GitHub at all.
The secret turns on every deploy at once. From then on a push to development deploys
development, and every pull request deploys a preview. A context whose values are still empty
ends its run red, naming the first one, so a stage you haven't reached yet shows as a red run rather
than a skipped one.
Get development live
At the end of this part your product is on the internet.
1. Fill development's secret folder
development reads the development Secret Tier's /development folder in apps/web's
Infisical project — the project id is the projectId committed in apps/web/.env.infisical. Store
your Postgres connection string there:
infisical secrets set DATABASE_URL='<the development connection string>' \
--env=development --path=/development --projectId='<apps/web project id>'
Paste the connection string as your provider hands it out, query string included. The deploy builds the context's Hyperdrive configuration from it, and Resources says which parts it reads.
Then add development's own value for every key that
Set up your secret store put in
/local. A feature you're skipping takes a stand-in, as it does locally.
2. Write development's origins
Both applications ship with their origins empty — NUXT_PUBLIC_APP_BASE_URL in each, and
apps/web's NUXT_PUBLIC_APP_WEBSITE_BASE_URL — and no origin is derived. Each one is required, so
now that the repository holds your Cloudflare token, a push without them fails at the build, naming
the key. Both Worker names come from your slug, so the only part you supply is the workers.dev
subdomain from the prerequisites:
NUXT_PUBLIC_APP_BASE_URL=https://<slug>-web-development.<subdomain>.workers.dev
NUXT_PUBLIC_APP_WEBSITE_BASE_URL=https://<slug>-site-development.<subdomain>.workers.dev
NUXT_PUBLIC_APP_BASE_URL=https://<slug>-site-development.<subdomain>.workers.dev
NUXT_PUBLIC_APP_WEBSITE_BASE_URL is the site's origin, which apps/web links to for /docs and
/blog.
3. Push development
Commit the two files, then push:
git push origin development
Before it builds, the workflow finds each resource development binds, by name. This is the first
run into development, so it creates all of them: the bucket and pool <slug>-web-development, and
the content database <slug>-site-development. It points the pool at development's DATABASE_URL,
then builds both applications, migrates apps/web's database and deploys each Worker.
Resources has the table, and
Which event deploys what is the whole mapping.
You are live
The run is green and both Workers serve. Each application's URL is its NUXT_PUBLIC_APP_BASE_URL in
its own .env.development — a *.workers.dev origin until
Going to production attaches your domain.
A run that shipped nothing is also green, so read the run log if a URL returns 404. When a deploy is skipped lists the reasons.
Turn on dependency updates
Install the Renovate app and turn on Dependabot alerts, so each dependency release arrives as a
pull request. Turn it on has the steps.
Until then, .github/renovate.json5 does nothing.
Email will not send until you onboard a domain
Local setup had you set PROJECT_ZONE, and Mailpit caught the
mail without asking anything of it. A deployed context sends for real.
You write no sending domain anywhere. Each application builds one per context —
<slug>-<app>-<context>.<zone> in every context except production.
Project identity has the full table.
Read the hostname for a context off the resolved environment, rather than assembling it yourself:
cd apps/web && APP_RUNTIME_CONTEXT=development pnpm exec varlock load --filter NUXT_EMAIL_SENDER_DOMAIN
Onboard that exact hostname, then fetch the DNS records it needs:
pnpm exec wrangler email sending enable acme-web-development.acme.com
pnpm exec wrangler email sending dns get acme-web-development.acme.com
When PROJECT_ZONE is a zone in the same Cloudflare account, the records are added for you — confirm
with wrangler email sending list. Otherwise add each printed record at your DNS host.
Repeat for every application and every context you deploy. Two things bite:
- A
*.workers.devhostname cannot be a sending domain. Onboarding needs SPF, DKIM and MX records on a zone you control, which is whatPROJECT_ZONEhas to be. Your non-production origins are onworkers.devand your senders never can be. - Onboarding is per exact hostname, not per zone.
acme.comdoes not authoriseacme-web-development.acme.com.
When it does not work
A green run that shipped nothing means the deploy was skipped, and on a push to your own
repository that means the run found no Cloudflare credentials —
Connect the repository to Cloudflare. A red run naming
a key means that value is empty — in .env.project.schema, the context's value file, or its folder
in the secret store. One naming a CLOUDFLARE_* variable means a prerequisite is unfinished, and one that
stops at Upsert the Cloudflare resources points at that context's DATABASE_URL or at one of the
token's resource permissions. See
When a deploy is skipped.
Next steps
- Adding pull request previews — a preview of every pull request, on preview's own Worker and data
- Adding staging — optional, a pre-production context between
developmentand production - Going to production — the last stage, once the application and its name are final
- Deploy pipeline — what each event does, and what a deploy needs
- Resources — what each context creates, how to see it, and how to delete it
