Skip to content

Greenfield Setup ​

Starting fresh with Tamer — no existing Cloudflare resources, no legacy naming. This is the happy path from empty repo to a live multi-tenant deployment.

1. Install Tamer ​

bash
npm install -D @dragonmastery/tamer wrangler

2. Create your config ​

Minimum viable config for a single-product multi-tenant app:

ts
// tamer/project.config.ts
import { cf, defineConfig } from "@dragonmastery/tamer";

export default defineConfig({
  stack: "myapp",
  account_id: "your-account-id",
  compatibility_date: "2025-05-19",

  // Workers for Platforms: dispatch namespace per product-env.
  // Default CF name is `{stack}-{logical}-{env}` (`myapp-myapp-dev`).
  // `namespace` overrides the base → `{namespace}-{env}` (`myapp-dev`).
  // Each service under `workers` gets one script per workspace
  // (`{service}-{workspace}-{env}`).
  wfp: {
    ephemeralEnvPattern: "^pr-",
    namespaces: {
      myapp: {
        namespace: "myapp",
        workers: {
          tenant: {
            main: "apps/tenant/src/index.ts",
            shardGroups: [
              { name: "primary", binding: "DB", migrationsDir: "db/primary/migrations" },
            ],
            d1: [
              { logicalName: "system", binding: "DB_SYSTEM", migrationsDir: "db/system/migrations" },
            ],
          },
        },
      },
    },
  },

  workers: {
    api: {
      path: "apps/api",
      main: "src/index.ts",
      scriptName: "myapp-api",
      resources: {
        d1: [{
          logicalName: "app-db",
          type: "single",
          binding: "APP_DB",
          migrationsDir: "apps/api/migrations",
        }],
      },
      vars: { ENVIRONMENT: "local" },
      env: {
        dev: { vars: { ENVIRONMENT: "${tamer:env}" } },
        prod: { vars: { ENVIRONMENT: "prod" } },
      },
      tamerRoutes: [
        { host: "api.myapp.com", customDomain: true },
      ],
    },
    spa: {
      path: "apps/spa",
      scriptName: "myapp-web",
      assets: { directory: "dist", not_found_handling: "single-page-application" },
      build: { command: "vite build" },
      vars: {
        VITE_API_URL: "http://127.0.0.1:8993/v1",
        ENVIRONMENT: "local",
      },
      env: {
        dev: { vars: { VITE_API_URL: "https://${tamer:env}.api.myapp.com/v1", ENVIRONMENT: "${tamer:env}" } },
        prod: { vars: { VITE_API_URL: "https://api.myapp.com/v1", ENVIRONMENT: "prod" } },
      },
      tamerRoutes: [
        { host: "myapp.com", customDomain: true },
      ],
    },
  },

  outputs: {
    api_worker_name: cf.worker("api").name,
  },
});

For the full split-config pattern (per-worker files with env overrides), see Single-Product Multi-Tenant.

3. Set up .env ​

bash
# .env (gitignored)
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_API_TOKEN=your-api-token

That's it — no app config values, no VITE_* URLs. Just Tamer's auth credentials for remote commands. Skip this file (and bootstrap) if you only want laptop Miniflare: Local Development (tamer wrangler --env local). See Values Lifecycle.

4. Bootstrap (once per account) ​

bash
bunx tamer bootstrap

Creates three shared account-scoped resources:

  • tamer-state (D1) — deployment state for all envs
  • tamer-secrets (D1) — encrypted secrets vault
  • tamer-artifacts (R2) — build bundles

Idempotent — safe to run multiple times.

5. Initialize secrets ​

bash
# Generate master key for dev (shown once — store in CI + password manager)
bunx tamer secrets init --env dev

# Set each required secret (pipe the value on stdin — interactive entry
# is not supported yet, so a bare `set` on a TTY errors)
echo -n "sk_live_..." | bunx tamer secrets set STRIPE_API_KEY --env dev
echo -n "..."        | bunx tamer secrets set JWT_SECRET --env dev

# Verify all declared secrets are in the vault
bunx tamer secrets verify --env dev

Store the master key as TAMER_SECRETS_KEY_dev in your CI secrets.

6. Apply resources ​

bash
bunx tamer apply --env dev

Creates everything declared in config:

  • Dispatch namespace (myapp-dev via namespace override; default without override is {stack}-{logical}-{env})
  • D1 databases (e.g. db_app-db_dev — db_{logical}_{env})
  • R2 buckets
  • KV namespaces
  • DNS records / routes
  • Generates wrangler.json per worker

7. Migrate databases ​

bash
bunx tamer migrate --env dev

Runs D1 migrations on each database declared with migrationsDir.

8. Deploy workers ​

bash
bunx tamer deploy --env dev

Per worker (topologically sorted by service bindings):

  1. Generate wrangler.json (if changed)
  2. Build SPA (vite build — values baked at compile time)
  3. wrangler types → wrangler deploy
  4. Push secrets from vault
  5. Register workflows

9. Provision your first tenant ​

bash
# `--main` is optional — defaults to the template's configured `main`.
bunx tamer wfp tenant provision --env dev \
  --workspace acme

Creates per-tenant D1 shards, runs migrations, bundles the tenant Worker via wrangler (TS compilation, WASM, polyfills), and deploys it to the dispatch namespace with D1 bindings. See Single-Product Multi-Tenant for the dispatch routing pattern.

10. Set up CI ​

Copy the workflow templates from CI Workflows into .github/workflows/. Set up GitHub secrets:

  • CLOUDFLARE_API_TOKEN
  • CLOUDFLARE_ACCOUNT_ID (variable)
  • TAMER_SECRETS_KEY_dev
  • TAMER_SECRETS_KEY_prod

Recap ​

# Laptop (no Cloudflare):
tamer wrangler     → gitignored wrangler.json + wrangler.vitest.json
tamer migrate      → Miniflare SQLite (stack D1s)
# then wrangler dev / bun test

# Remote:
bootstrap          → once per account
secrets init       → once per env (generates master key)
secrets set        → per secret
apply              → creates resources
migrate            → runs D1 migrations
deploy             → builds + deploys workers
wfp tenant provision → creates a tenant

Next: Single-Product Multi-Tenant for the full architecture, CI Workflows to automate everything, or Local Development for tamer wrangler + Wrangler / Vitest.

Released under the Tamer Evaluation License.