Secure deploy to Cloudflare

An API token for one account, kept out of the code - step by step for Next.js and Express, including old chunks after a deploy

A guide you can follow even without deep Cloudflare experience: how to create a token with the right permissions, keep it out of the deployed code, lock deploys to one account, check that it worked and deploy without breaking pages that visitors have open.

Vladimír NeporAuthorVladimír Nepor
Published
Secure deploy: the API token lives in .env.deploy, a bash wrapper loads it only for one command, and wrangler.jsonc locks the deploy to one Cloudflare account. The build never sees the token..env.deployCLOUDFLARE_API_TOKEN=•••••••token for this projectchmod 600 · .gitignore✕ build never reads itnpm run deploybash -c 'set -a; . …'exec wrangler deploytoken only forthis one commandCloudflare"account_id": "a1b2…"one account onlywrangler.jsonc03 · TOKENA token for one accountminimal permissions + IP filterchapter →04 · PROJECTToken outside the buildaccount_id + .env.deploy + scriptschapter →05 · VERIFYCheck before deployingwhoami + scan of the buildchapter →06 · DEPLOYDeploy without broken pageskeep the old chunkschapter →
Guide contents

00 · Overview

Secure deploy at a glance

This guide sets up deploys to Cloudflare Workers so that the API token never ends up in the deployed code, every deploy goes to exactly one account, and visitors do not get broken pages after a deploy. Every command can be copied with one click.

Choose your technology

Most steps are the same for every project. Where Next.js and Express differ, the guide shows tabs like these. Your choice applies to the whole page and is remembered.

  • The risk: the OpenNext build copies every value from .env into the Worker, so a token there runs in production.
  • Follow: all chapters, including old chunks in chapter 06.
  • Static export (output: "export") on Workers Static Assets: chapters 03 to 05 like a Worker, plus the old chunks.

The whole setup in short

  1. One token per project - a token for one account with only the permissions the project uses, limited to your IP address
  2. Lock the account - "account_id" in wrangler.jsonc, so a deploy cannot land in another account
  3. Keep the token out of the code - the token lives in .env.deploy, which no framework and no wrangler dev reads; never in .env
  4. Load it only for one command - npm scripts load the file in their own bash; npm run -s wr -- replaces npx wrangler
  5. Verify, then deploy - check the account and the build output, deploy, and (Next.js) keep the old JS chunks so open pages keep working

What are you trying to do?

QuestionWhere to find the answer
Why is a token in .env or wrangler login a problem?Why →
What do I need before I start?Before you start →
Which permissions should the token have?Create the token →
How do I set up the project files?Set up the project →
How do I check that the token is not in the build?Verify →
After a deploy, open pages fail with ChunkLoadError.Old chunks →
We already have a token in .env. What now?Moving from the old setup →
I get a 403, an OAuth window or "not authenticated".Troubleshooting →
Scope

State as of October 2026: Wrangler 4, @opennextjs/cloudflare 1.x, account-owned API tokens. The setup runs in production on this website (Next.js on OpenNext, D1, R2, Workers Cache) and on our Express Workers. The commands need bash: Linux, macOS, or WSL on Windows.

01 · Why

What is wrong with wrangler login and a token in .env

The common setup, wrangler login or a token in .env, works, but it has weak spots. It is easy to deploy into the wrong account, and the token can end up where it does not belong: in production code, or in reach of your own code and its dependencies.

wrangler login opens every account

npx wrangler login signs you in through OAuth as a person. Wrangler then has access to every account you are a member of: your own and all your clients'. One command run in the wrong folder, or a project without a fixed account, and the deploy, a D1 query or a deleted bucket lands in the wrong place. A token created for one account cannot do that.

Where a token in .env ends up

Wrangler reads .env automatically, so putting the token there looks natural. But other tools read the file too, and what happens next depends on the technology:

Top: the token in .env is read by the OpenNext build, written into next-env.mjs and ends up in the production Worker. Bottom: the token in .env.deploy is loaded only by the wrapper for the wrangler CLI, the build does not read it and the Worker contains no token.✕ Token in .env.envCLOUDFLARE_API_TOKEN=…OpenNext buildreads every .env*Production Workertoken in process.env✓ Token in .env.deploy.env.deployCLOUDFLARE_API_TOKEN=…bash wrapperonly for wranglerCloudflare APIdeploy to one accountThe build reads only .env, .env.local, .env.<mode>: the Worker contains no token
The same token, two files. Only the bottom path keeps it out of the deployed Worker.
WhereHow the token gets into the deployed code
OpenNext buildwrites all values from .env, .env.local, .env.<mode> and .env.<mode>.local into .open-next/cloudflare/next-env.mjs and loads them into process.env at runtime
Next.js standalone.next/standalone contains a copy of .env
DockerCOPY . . without .env* in .dockerignore copies the file into the image
Browservariables prefixed NEXT_PUBLIC_ and values in next.config → env end up in the public JavaScript

With OpenNext a token in .env is therefore in the production Worker, readable through process.env by your code and every dependency.

Why it matters

Whoever gets hold of a deploy token (through the Worker's code, its environment, its logs, a compromised dependency) can deploy, read databases and delete storage in your account. The token stays valid until you delete it in the dashboard, even after you remove it from .env.

02 · Before you start

What you need

Ten minutes and these few things. If something is missing, sort it out first, the later steps rely on it.

  • A project with a Wrangler config: wrangler.jsonc, wrangler.json or wrangler.toml in the project root.
  • bash: Linux and macOS have it. On Windows, work in WSL (Ubuntu), Git Bash is enough for the commands too.
  • Access to the Cloudflare dashboard of the target account, with the right to create API tokens.
  • The project in git, so you can see exactly what changed.

Which kind of project do you have?

You see in the projectProject typeTab and what to follow
@opennextjs/cloudflare in package.jsonNext.js on OpenNextNext.js, all chapters including the old chunks
wrangler.jsonc with "main", no Next.jsa Worker (Express, Hono…)Workers, all chapters, old chunks only with a hashed frontend build
"assets" without "main", a static build (Next.js output: "export", Vite)static siteWorkers. A token can reach the output only through NEXT_PUBLIC_ / VITE_, so the lock, .env.deploy and its own token are enough. Old chunks apply too (CHUNKS_DIR).
Dockerfile + containers in the configCloudflare ContainersWorkers, plus .env* in .dockerignore
Express on your own server (no Wrangler)not a Workerthe server needs no Cloudflare token at all, keep it only on the machine you deploy from

03 · Token

Create a token for one project

Create a separate token for every project. If you ever suspect a leak, you delete one token and the other projects keep running. Cloudflare shows the token value only once, so have the project open and ready to paste it.

Step 1: Open the token form in the right account

In the dashboard, switch to the project's account and open Manage Account → Account API Tokens → Create Token → Custom token. An account token belongs to the account and can never reach another one. A user token (My Profile → API Tokens) works too, but you have to limit it to one account yourself (step 3).

Step 2: Name the token and add only the permissions the project uses

Name: e.g. my-app deploy. Then click Add more for every row you need. Look at the bindings in your wrangler.jsonc: d1_databases means D1, r2_buckets means R2, kv_namespaces means KV.

Workers Scripts covers deploys, secrets, Durable Objects and logs. If a deploy later fails with an error about a missing permission (Queues, Containers, Images), add just that one.

Permission (type → item → level)When
Account → Workers Scripts → Editalways
Account → Account Settings → Readalways
Account → D1 → Editd1_databases in the config
Account → Workers R2 Storage → Editr2_buckets (also the OpenNext incremental cache)
Account → Workers KV Storage → Editkv_namespaces
Zone → Workers Routes → Editonly with routes in the config, and limit Zone Resources to that one zone
Do not start from the "Edit Cloudflare Workers" template

The template is convenient, but it grants much more than a deploy needs: besides Workers Scripts also KV, R2, Tail, user details and Workers Routes on all your zones. Start from Custom token and add only the rows from the table above.

Step 3: Limit the token to one account

Only in the user token form: under Account Resources keep Include and replace All accounts with the project's account. This is the most important field of the whole form. The account token form does not have it, because the token is bound to the account.

Step 4: Allow the token only from your IP address

Under Client IP Address Filtering keep Allow and enter your public address. A leaked token is then useless anywhere else. Find your addresses with:

Terminal
# your public IPv4 address
curl -4 -s https://ifconfig.me; echo

# your public IPv6 address (no output = you have none, skip it)
curl -6 -s --max-time 5 https://ifconfig.me; echo

If you also have IPv6, add it with Add more (the address, or its /64 range). Otherwise Wrangler may connect over IPv6 and get a 403. You can edit the list later, e.g. when your provider changes your address.

Step 5: Set the expiry, create the token and copy it

TTL is optional; a year is a good default, then you roll the token. Click Continue to summary → Create Token and copy the value. Do not send it in chat or email. It goes straight into the project in the next chapter. Copy the Account ID too: on Account home click the dots next to the account name and choose Copy account ID. The Account ID is not a secret.

04 · Project

Set up the project

Five small changes in the project. All of them are files you commit, except .env.deploy, which stays only on your computer.

Step 1: Write the Account ID into wrangler.jsonc

Add "account_id" right below "name". From now on every deploy and wrangler command of this project targets this account only. If the project has more Wrangler configs (e.g. a separate static Worker), add it to each of them.

wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-app",
  // Locked to one account: a deploy or any wrangler command cannot land in another account.
  // The API token lives in .env.deploy (see package.json scripts "deploy" and "wr").
  "account_id": "<ACCOUNT_ID>",
  // ... the rest of your config stays as it is
}

Step 2: Put the token into .env.deploy

Create the file in the project root and paste the token. Next.js, OpenNext and Vite load only .env, .env.local, .env.<mode> and .env.<mode>.local, and wrangler dev only .dev.vars or .env. Nothing reads .env.deploy on its own.

.env.deploy
# Only for deploys and the wrangler CLI. Next.js, OpenNext and Vite never read this file.
CLOUDFLARE_API_TOKEN="<API_TOKEN>"
Terminal
# only you can read the file
chmod 600 .env.deploy

# must print a .gitignore rule; if it prints nothing, add one
git check-ignore -v .env.deploy || echo '.env*' >> .gitignore
Terminal
# Docker only: keep all env files out of the image
echo '.env*' >> .dockerignore

Step 3: Remove Cloudflare variables from all other env files

Delete the lines CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID from .env, .env.local, .env.production, .dev.vars and similar. The first command below must print nothing.

Terminal
# Which env files still contain Cloudflare variables? (prints names, never values)
grep -l 'CLOUDFLARE_' .env .env.* .dev.vars 2>/dev/null | grep -v '^.env.deploy$'

# Show the variable names in .env without their values
sed 's/=.*/=<redacted>/' .env

Why the Account ID too, when it is no secret? For Workers commands the account_id in wrangler.jsonc wins (tested with Wrangler 4.98 and 4.149), but wrangler pages … commands prefer the variable, and wrangler dev hands it to your code as a binding. One place for the account avoids both.

Step 4: Let the npm scripts load the token

Each script starts its own bash, loads .env.deploy into it and runs one command. The token does not stay in your terminal. Replace the matching scripts in package.json:

package.json
"scripts": {
  "dev":     "bash -c 'set -a; . ./.env.deploy; set +a; exec next dev \"$@\"' dev",
  "cfbuild": "bash -c 'set -a; . ./.env.deploy; set +a; exec opennextjs-cloudflare build'",
  "preview": "bash -c 'set -a; . ./.env.deploy; set +a; opennextjs-cloudflare build && exec opennextjs-cloudflare preview'",
  "deploy":  "bash -c 'set -a; . ./.env.deploy; set +a; opennextjs-cloudflare build && scripts/keep-old-chunks.sh merge && opennextjs-cloudflare deploy && scripts/keep-old-chunks.sh save'",
  "wr":      "bash -c 'set -a; . ./.env.deploy; set +a; exec wrangler \"$@\"' wr"
}
  • OpenNext does not write values from the shell environment into the build, so loading the token for cfbuild and deploy is safe.
  • dev needs the token only when a binding has "remote": true (it then reads production data). Without the wrapper it would open the OAuth window.
  • scripts/keep-old-chunks.sh used by deploy is in chapter 06.

Step 5: Use npm run -s wr -- instead of npx wrangler

Everything after -- goes to Wrangler. Replace npx wrangler in your README, docs and scripts as well, so nobody runs a command without the token.

Terminal
# instead of:  npx wrangler <command>
npm run -s wr -- whoami
npm run -s wr -- tail
npm run -s wr -- d1 execute <DB_NAME> --remote --command "SELECT 1"
npm run -s wr -- r2 object get <BUCKET>/<KEY> --remote

Why not Wrangler's own --env-file? It works only after the command name (wrangler whoami --env-file=…), in front of it Wrangler prints just the help. The wrapper is simpler and the same for every command.

Secrets the app needs at runtime

API keys your server code reads while it runs (payments, e-mail, maps on the server) do not belong in .env either. In production they are Worker secrets. Note that secret put creates a new version of the Worker in production.

Terminal
# Runtime secrets of the app (third-party API keys etc.), never in .env.
# The value is read from .env and piped in, so it never shows up on screen or in the shell history.
grep '^MY_SECRET=' .env | cut -d= -f2- | npm run -s wr -- secret put MY_SECRET

# check (prints names only), then delete the line from .env
npm run -s wr -- secret list

OpenNext copies Worker secrets into process.env at runtime, and they take precedence over values from the build. The code stays the same: process.env.MY_SECRET.

05 · Verify

Check before the first deploy

Three checks before the first deploy. Each one takes a few seconds and tells you exactly what is wrong if it fails.

Step 1: Wrangler sees the token and the right account

Terminal
npm run -s wr -- whoami
# → "You are logged in with an Account API Token" (or "User API Token")
#   and the account you expect

If you see a different account or an error, check the token value in .env.deploy and that you run the command in the project root. A user token also prints Unable to retrieve email… User Details Read: that is only information, no permission needs to be added.

Step 2: Without the wrapper, Wrangler is not signed in

Terminal
npx wrangler whoami
# → "You are not authenticated. Please run `wrangler login`."
#   That is correct: without the wrapper there is no token. Do NOT run wrangler login.

If it says you are logged in, either an old OAuth session is active (run npx wrangler logout) or the token is still in .env or in your shell (open a new terminal and go back to step 3 of chapter 04).

Step 3: The build output contains no token

Build locally (this deploys nothing) and search the output. The checks print only names, never the token itself.

Terminal
npm run cfbuild

# OpenNext: no Cloudflare variable in the Worker bundle (must print 0)
grep -c CLOUDFLARE_ .open-next/cloudflare/next-env.mjs

# Any project: no value from .env.deploy anywhere in the build output.
# Prints file names only, never the values. Empty output = OK.
while IFS='=' read -r k v; do v=${v//\"/}; [ -n "$v" ] && [ "${k#\#}" = "$k" ] && \
  grep -rlF -- "$v" .open-next .next dist build 2>/dev/null | sed "s|^|$k found in: |"; done < .env.deploy

Something found? A Cloudflare variable is still in one of the .env files the build reads. Remove it and build again.

06 · Deploy

Deploy, rollback, CI and old chunks

With the scripts from chapter 04 a deploy is a single command. This chapter also covers rollback, CI and, for Next.js and static sites, how to keep pages that visitors have open working after a deploy.

Deploy

Terminal
npm run deploy
# 1. build  2. add old chunks  3. deploy  4. archive the deployed chunks
# Step 4 runs only when the deploy succeeded.

The first deploy prints no archive yet, which is fine. From the second one on you see added N old files. How the archive works is described below.

Rollback

Static files are part of each Worker version, so a rollback brings back that version's files too. No rebuild is needed.

Terminal
npm run -s wr -- deployments list   # recent versions
npm run -s wr -- rollback           # back to the previous version

Deploying from CI

In GitHub Actions or GitLab, store the token as a CI secret and pass it only to the steps that need it. Never write a .env file from CI secrets before the build, that brings back the original problem. GitHub-hosted runners have changing IP addresses, so the IP filter does not work there: use a separate CI token with a short TTL, or a self-hosted runner with a fixed address.

.github/workflows/deploy.yml (steps)
# GitHub Actions: the token goes only to the steps that need it, never into a .env file
- run: npm ci
- run: npx opennextjs-cloudflare build
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}   # only if bindings use "remote": true
- run: scripts/keep-old-chunks.sh merge
  env:
    OLD_ASSETS_DIR: ${{ runner.temp }}/old-assets             # restore it with actions/cache
- run: npx opennextjs-cloudflare deploy
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- run: scripts/keep-old-chunks.sh save
  env:
    OLD_ASSETS_DIR: ${{ runner.temp }}/old-assets

The chunk archive has to survive between runs, e.g. with actions/cache on OLD_ASSETS_DIR.

Old chunks (Next.js and static sites)

Every Next.js build gives its JS and CSS files new names. Pages that visitors already have open, or have in the browser cache, still ask for the old names. If the new deploy does not contain them, the page breaks. The fix: keep an archive of old chunks and add it to every deploy. An Express Worker without a hashed frontend does not need this.

A page opened before the deploy (version 1) asks for its own chunk a1.js. After a plain deploy of version 2 the file is gone: 404 and ChunkLoadError. With the archive merged in, version 2 also contains the old chunks and the page keeps working.Open tabpage loaded from v1GET a1.jsDeploy v2 without the archivea2.jsb2.jsa1.js✕ 404 → ChunkLoadErrorDeploy v2 + archived chunksa2.jsb2.jsa1.jsb1.jsfrom the archive ↓✓ 200 → page keeps working
A tab opened before the deploy asks for its own chunks. Only the deploy with the archive still has them.
  • a visitor has the page open and clicks a link (client-side navigation loads more chunks of the old version),
  • the browser shows a cached copy of the HTML (on this site up to 10 minutes),
  • an edge or CDN cache keeps HTML across deploys (Workers Cache with cross_version_cache).

The visitor sees a blank or frozen page and the console shows ChunkLoadError or a 404 for /_next/static/…. Chunk names contain a content hash, so old and new files never collide and it is safe to keep both.

Step 1: Add the script that keeps old chunks

Save it as scripts/keep-old-chunks.sh. merge copies archived chunks into the fresh build (it never overwrites a new file), save adds the deployed chunks to the archive. The archive lives outside the project, in ~/next-deployments-old-assets/<project folder>, so a clean checkout or rm -rf .open-next cannot delete it.

scripts/keep-old-chunks.sh
#!/usr/bin/env bash
# Keeps the JS/CSS chunks of earlier builds available after a deploy.
#   merge  before the deploy: copy archived chunks into the fresh build (new files are never overwritten)
#   save   after a successful deploy: add the deployed chunks to the archive
# Archive: $OLD_ASSETS_DIR, default ~/next-deployments-old-assets/<project folder name>
# Chunk folder: $CHUNKS_DIR, default .open-next/assets/_next/static (OpenNext); static export: out/_next/static
set -euo pipefail

BUILD="${CHUNKS_DIR:-.open-next/assets/_next/static}"
ARCHIVE="${OLD_ASSETS_DIR:-$HOME/next-deployments-old-assets/$(basename "$PWD")}/$BUILD"

# Copy files from $1 to $2 that do not exist in $2 yet; the count goes to COPIED.
# Paths keep the leading "./" (build folders may start with "-").
COPIED=0
copy_missing() {
  local src=$1 dst=$2 f
  COPIED=0
  while IFS= read -r -d '' f; do
    if [ ! -e "$dst/$f" ]; then
      mkdir -p -- "$(dirname -- "$dst/$f")"
      cp -p -- "$src/$f" "$dst/$f"
      COPIED=$((COPIED + 1))
    fi
  done < <(cd "$src" && find . -type f -print0)
}

# Delete Windows "Zone.Identifier" files (created when files are copied through Windows Explorer into WSL).
remove_zone_ids() {
  local dir n
  for dir in "$@"; do
    [ -d "$dir" ] || continue
    n=$(find "$dir" -type f -name '*:Zone.Identifier' -print -delete | wc -l)
    [ "$n" -gt 0 ] && echo "keep-old-chunks: deleted $n Zone.Identifier files in $dir"
  done
  return 0
}

[ -d "$BUILD" ] || { echo "keep-old-chunks: $BUILD not found, run the build first" >&2; exit 1; }
remove_zone_ids "$ARCHIVE" "$BUILD"

case "${1:-}" in
  merge)
    if [ -d "$ARCHIVE" ]; then
      copy_missing "$ARCHIVE" "$BUILD"
      echo "keep-old-chunks: added $COPIED old files from $ARCHIVE"
    else
      echo "keep-old-chunks: no archive yet ($ARCHIVE), it starts with this deploy"
    fi
    ;;
  save)
    mkdir -p "$ARCHIVE"
    copy_missing "$BUILD" "$ARCHIVE"
    echo "keep-old-chunks: archived $COPIED new files to $ARCHIVE"
    ;;
  *)
    echo "usage: $0 merge|save" >&2
    exit 2
    ;;
esac
Terminal
chmod +x scripts/keep-old-chunks.sh

Another location? Set OLD_ASSETS_DIR. The script deletes Windows Zone.Identifier files from the archive and the build, they appear when you copy files through Windows Explorer into WSL.

Step 2: Call it around the deploy

merge before the deploy, save only after a successful one, so files that never went live do not get into the archive. The OpenNext deploy script from chapter 04 already does it. A static Next.js export only sets CHUNKS_DIR:

package.json (static export)
# Static Next.js export (output: "export") deployed as Workers Static Assets
"deploy": "next build && CHUNKS_DIR=out/_next/static scripts/keep-old-chunks.sh merge && bash -c 'set -a; . ./.env.deploy; set +a; wrangler deploy' && CHUNKS_DIR=out/_next/static scripts/keep-old-chunks.sh save"

The archive grows by a few hundred kilobytes per deploy. If it ever gets large, delete the oldest build folders in it (after a few weeks nobody needs them).

07 · Migration

Moving from a token in .env

Moving from a token in .env. Go in this order, so the site keeps running and the old token stops working only when nothing needs it anymore. Start while the old token is still in .env: the first two steps read it from there.

The old token is in production

The OpenNext build has written it into the Worker that runs right now. Delete it only after the new deploy (step 6), until then it is still valid and readable in production.

Step 1: Check whether other projects use the same token

A token copied from project to project is common. Deleting it would break the others, so each of them needs its own token first.

Terminal
# Do other projects use the same token? Prints file names only, never the token.
# Anything outside this project = that project must get its own token before you delete this one.
T=$(grep '^CLOUDFLARE_API_TOKEN=' .env | cut -d= -f2- | tr -d '"')
[ -n "$T" ] && grep -lsF -- "$T" ~/projects/*/.env* ~/projects/*/.dev.vars; unset T

Step 2: Find the old token in the dashboard

An account token is under Manage Account → Account API Tokens, a user token under My Profile → API Tokens. The call below prints the token's ID and sets its Last used to now, so you recognise it in the list. Do not be surprised by the fresh date.

Terminal
# Which token is it? Prints only its ID, never the token.
# The call also sets "Last used" of that token in the dashboard to now: that is how you spot it in the list.
T=$(grep '^CLOUDFLARE_API_TOKEN=' .env | cut -d= -f2- | tr -d '"')

# account token (Manage Account → Account API Tokens)
curl -s -H "Authorization: Bearer $T" "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/tokens/verify" | grep -o '"id":"[^"]*"'

# user token (My Profile → API Tokens)
curl -s -H "Authorization: Bearer $T" "https://api.cloudflare.com/client/v4/user/tokens/verify" | grep -o '"id":"[^"]*"'
unset T

Step 3: Create a new token

As in chapter 03, one per project.

Step 4: Set up the project

account_id, .env.deploy with the new token, clean .env, scripts (chapter 04).

Step 5: Verify the account and the build

Chapter 05.

Step 6: Deploy

npm run deploy. The Worker running now has no token inside.

Step 7: Delete the old token

In the dashboard, the token you found in step 2. With Next.js only now; with a plain Worker you could already after step 5.

Step 8: Revoke the OAuth access

If you ever used wrangler login:

Terminal
npx wrangler logout
# then in the dashboard: My Profile → Access Management → Connected Applications → revoke Wrangler
Was the token ever in git?

Check with git log --all --oneline -- .env. If it prints anything, the token is in the repository history. Deleting the token in the dashboard (step 7) is what protects you; rewriting the history is optional.

08 · Troubleshooting

When something does not work

The most common problems and what to do about them, roughly in the order you may meet them.

What you seeCauseWhat to do
An OAuth window: Wrangler wants to access your accounta command runs without the token (npx wrangler … directly)click Cancel, run it again through npm run -s wr --
npm run dev opens the OAuth windowa binding has "remote": true and dev is not wrappedwrap dev like the other scripts (chapter 04)
You are not authenticatedno token in the environmentuse the npm script; do not run wrangler login
Unable to retrieve email… User Details Reada user token without that permissiononly information, nothing to add
403 / Authentication erroryour IP address changed, you connect over IPv6, or a permission is missingcheck your IP (step 4) and the token's IP list, then the permissions
.env.deploy: No such file or directorythe command is not run in the project rootcd into the folder with package.json
Wrangler prints only its help--env-file placed before the command nameuse the wrapper, or put the flag after the command
Build fails reading D1 / R2a binding has "remote": true and the build runs without the tokenbuild through the npm script (npm run cfbuild), not with npx …
ChunkLoadError after a deploythe deploy did not contain the old chunkscheck the archive path in the deploy output (old chunks)
bash: command not found on Windowsnpm runs scripts in cmd or PowerShellwork in WSL (Ubuntu), or set npm's script-shell to Git Bash
Key takeaways
  • One project = one token, one account in account_id, the token in .env.deploy.
  • Never a Cloudflare token in .env (OpenNext bundles it, wrangler dev hands it to your code) and never wrangler login.
  • Check the build output before the first deploy. After switching, delete the old token only when nothing needs it, with Next.js after the new deploy.
  • Next.js and static sites: keep the old _next/static chunks in an archive outside the project and add them to every deploy.

Sources

Official documentation and further reading

Deploying to several Cloudflare accounts?

We set up tokens, deploys and the whole Cloudflare stack so that nothing secret ends up in production code.

Contact Us