Local Development
The daily inner loop: run your workers and SPA against local resources without touching Cloudflare. Tamer gets you to a deployed env (see Greenfield Setup); this page covers tamer dev and the local env.
The local env
local is special: it makes no Cloudflare API calls, writes no state rows, and uses wrangler's local resources (local D1 SQLite, local KV, miniflare). --env defaults to local for most commands, so:
tamer dev # == tamer dev --env local
tamer migrate # == tamer migrate --env localdev and migrate in local mode need no CLOUDFLARE_* credentials.
Durable local wrangler pair
Any durable --env local emit (tamer apply / migrate / seed / dev / types, and WFP wfp tenant wrangler) writes two gitignored files:
| File | Use |
|---|---|
wrangler.json (or wranglerOutFile) | wrangler dev, import, local D1 migrate/seed |
wrangler.vitest.json (sibling) | @cloudflare/vitest-pool-workers only |
The primary may include remote: true (e.g. shared corpus R2 under local). The Vitest sibling keeps the same bindings but strips remote so unit tests stay offline against Miniflare. Point the pool at the sibling:
// vitest config
wranglerConfigPath: "./wrangler.vitest.json"tamer dev
tamer dev generates that pair (resolved against local state — empty for a fresh stack, so resource IDs stay as placeholders) and spawns wrangler dev against the primary file in the worker's project directory.
tamer dev --worker api # one worker
tamer dev --all # every worker, each on its own port--all starts one wrangler dev per worker on incrementing ports, beginning at TAMER_DEV_BASE_PORT (default 8787): api → 8787, spa → 8788, … Ctrl-C stops them all.
For a single worker, tamer dev runs one wrangler dev in the foreground (default port 8787).
Pointing an SPA at the local API
For a stack with an API worker and a SPA worker (see SPA with API):
- Start the API:
tamer dev --worker api→http://127.0.0.1:8787. - Run the SPA's own dev server (Tamer does not run
vite dev— see SPA Build). Point it at the local API via your basevars:
// tamer/workers/spa/base.ts
vars: {
VITE_API_CLIENT_URL: "http://127.0.0.1:8787/v1", // base = local values
},vite dev reads VITE_* from process.env or your SPA's .env/.env.local — that's Vite's responsibility, not Tamer's. Tamer only bakes vars when it spawns the build (tamer deploy), not during vite dev.
Local D1 migrations
tamer migrate (no --env, or --env local) targets wrangler's local SQLite cache — no --remote flag. Iterate on schema locally, then run tamer migrate --env dev to apply the same files to the real dev D1:
tamer migrate # local SQLite
tamer migrate --env dev # remote dev D1After squashing migration files locally, reset the local database without touching Cloudflare:
tamer reset --env local --target d1:app-dbSee Migrations.
WFP tenant local D1 + generated wrangler
Stack tamer migrate / tamer reset --env local only cover D1s on workers.*. Per-tenant product Workers live on WFP templates (wfp.namespaces.*.workers.*).
Tamer’s job locally is to generate the gitignored wrangler pair from that template. Your repo runs bun run dev / Vitest / wrangler — Tamer does not wrap tenant dev.
# Generate (or refresh) wrangler.json + wrangler.vitest.json
tamer wfp tenant wrangler --env local --workspace demo
# Schema / data (also regenerates the pair first; D1 uses the primary)
tamer wfp tenant migrate --env local --workspace demo
tamer wfp tenant seed --env local --workspace demo
tamer wfp tenant reset --env local --workspace demo --kind d1
# Then your scripts:
bun run dev # wrangler.json (may remote-proxy corpus R2)
bun test # wrangler.vitest.json (always offline Miniflare)Wipe / migrate / seed use cwd = the template worker directory so Miniflare SQLite matches what wrangler dev uses:
{workerDir}/.wrangler/state/v3/d1/...Template local / env overlays
WFP templates accept the same style of overlays as stack workers:
local— merged when generating local wrangler / local migrate-seed-resetenv.dev/env.production— merged on remote provision/reupload
Put local-only bindings under local (e.g. shared corpus R2 with remote: true for real caa-data during wrangler dev). Do not put that on the base template used for remote deploy. Vitest uses the generated wrangler.vitest.json sibling (same binding names, no remote). Secrets stay in {workerDir}/.dev.vars, not local.vars.
// tamer/wfp/base.ts (sketch)
{
main: "apps/tenant/src/index.ts",
path: "apps/tenant",
d1: [/* … */],
shardGroups: [/* … */],
local: {
vars: { ENVIRONMENT: "local" }, // non-secrets only
r2_buckets: [
{ binding: "MIGRATION_SOURCE", bucket_name: "caa-data", remote: true },
],
},
}If a hand-written wrangler.jsonc still exists, Tamer warns and writes the wrangler.json + wrangler.vitest.json pair (preferred). Delete the jsonc when you cut over.
Local has no WFP dispatch — SPA → tenant Worker directly. wfp tenant provision / destroy / add-shard stay remote-only.
Local secrets (.dev.vars)
Local worker secrets live in a plain .dev.vars file in the worker's project directory — not in the Tamer vault. Wrangler reads it automatically during wrangler dev. This is intentionally separate from the encrypted vault (.dev.vars is local-only; the vault is for dev/prod deploys).
workers/api/.dev.vars # local only — gitignoredSTRIPE_API_KEY=sk_test_...
JWT_SECRET=local-dev-secretSee Secrets for the vault workflow that takes over for non-local envs.
What tamer dev does not do
- No resource provisioning —
localresources are miniflare/local SQLite, not created via the Cloudflare API. - No state DB writes — there is no
tamer-staterow forlocal. - No route expansion —
localenvs get no routes (no*.workers.devpreview, no custom domains). Reach the worker viahttp://127.0.0.1:<port>.
Related docs
- Environments — the
localenv, ephemeral PR envs - Migrations — local SQLite vs remote D1
- SPA Build — why
vite devis outside Tamer's scope - Secrets —
.dev.vars(local) vs vault (deployed)