Skip to content

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:

bash
tamer dev          # == tamer dev --env local
tamer migrate      # == tamer migrate --env local

dev 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:

FileUse
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:

ts
// 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.

bash
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): api8787, spa8788, … 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):

  1. Start the API: tamer dev --worker apihttp://127.0.0.1:8787.
  2. Run the SPA's own dev server (Tamer does not run vite dev — see SPA Build). Point it at the local API via your base vars:
ts
// 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:

bash
tamer migrate                              # local SQLite
tamer migrate --env dev                    # remote dev D1

After squashing migration files locally, reset the local database without touching Cloudflare:

bash
tamer reset --env local --target d1:app-db

See 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.

bash
# 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-reset
  • env.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.

ts
// 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 — gitignored
STRIPE_API_KEY=sk_test_...
JWT_SECRET=local-dev-secret

See Secrets for the vault workflow that takes over for non-local envs.

What tamer dev does not do

  • No resource provisioning — local resources are miniflare/local SQLite, not created via the Cloudflare API.
  • No state DB writes — there is no tamer-state row for local.
  • No route expansion — local envs get no routes (no *.workers.dev preview, no custom domains). Reach the worker via http://127.0.0.1:<port>.

Released under the Tamer Evaluation License.