npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dragonmastery/tamer

v0.57.1

Published

Tamer: Cloudflare Workers infra CLI (sync, apply, deploy, migrate, destroy) and Wrangler-oriented TypeScript types.

Readme

@dragonmastery/tamer

Tamer tooling monorepo — Cloudflare Workers infrastructure CLI (sync, apply, deploy, migrate, destroy), Wrangler-oriented TypeScript types (generated from the official Wrangler config schema), reference fixtures, and tests for the published @dragonmastery/tamer npm package.

Using Tamer in a downstream repo? Install from npm and follow Quickstart. The rest of this README is for contributors hacking Tamer and for CLI/config reference.

Installation

Contributors (this repo):

bun install

Requires Bun and Node.js 22+ (engines.node). Run the CLI with bun run tamer -- … or bun src/cli/index.ts … (see Developing).

Published package (downstream repos):

npm install -D @dragonmastery/tamer wrangler

Also requires the wrangler peer (>=4.0.0). Install, scripts, stack layout, and first deploy: Quickstart.

Consumers (downstream repos)

Install from npm and walk through bootstrap → apply → deploy in docs/guide/quickstart.md. Adopting existing D1/R2/workers without CF IDs in config: docs/guide/brownfield-adoption.md. Worker secrets (vault + push): docs/guide/secrets.md. Resource kinds, ${tamer:import:…}, outputs, and CLI flags remain documented below for reference.

Per-env config overlays: alongside tamer/project.config.ts you can add tamer/env/<env>.config.ts (or tamer.env.<env>.ts for the flat layout) exporting defineProjectOverlay({...}) — Tamer deep-merges it onto the project config for that --env. Overlays may only patch workers that already exist in the project config.

Types usage

import type {
  WranglerConfig,
  WranglerEnvironment,
  WranglerD1Database,
  WranglerR2Bucket,
  WranglerKVNamespace,
} from "@dragonmastery/tamer";

const config: WranglerConfig = {
  name: "my-worker",
  main: "src/index.ts",
  compatibility_date: "2025-01-01",
  d1_databases: [{ binding: "DB", database_id: "xxx" }],
};

| Type | Description | | --------------------- | ------------------------------------------- | | WranglerConfig | Full wrangler config (alias: RawConfig) | | WranglerEnvironment | Environment block (alias: RawEnvironment) | | WranglerD1Database | D1 binding entry | | WranglerR2Bucket | R2 binding entry | | WranglerKVNamespace | KV binding entry |

CLI

tamer bootstrap  Create account-scoped Tamer metadata: tamer-state (D1), tamer-artifacts (R2), tamer-secrets (D1). Run once per account
tamer sync       Sync local state from Cloudflare (no writes)
tamer apply      Provision missing D1 / R2 / KV / … / dispatch namespaces (creates shard 0 for each `resources.shardGroups[]` entry)
tamer migrate    Run D1 migrations per worker (includes stack-level shard groups)
tamer seed       Run D1 seed SQL per worker (`seedDir`; after migrate on reset/provision)
tamer types      Generate worker-configuration.d.ts per worker (wrangler types)
tamer shard add  Append the next physical D1 to a stack-level shard group; regenerates wrangler + `src/shard-bindings.ts`
tamer deploy     wrangler deploy per worker (after sync), then apply zone-name `tamerRoutes` via Workers Routes API
tamer dev        wrangler dev (use --all for every worker)
tamer status     Show config vs state (optional `--tenant product:workspace`)
tamer events     Print operation history timeline from state (--limit N, --json)
tamer drift      Compare state vs Cloudflare and report differences (read-only)
tamer plan       CloudFormation-style preview: what `apply`+`deploy` would create (read-only)
tamer import     Register an existing Cloudflare resource into state by logical name
tamer doctor     Verify `CLOUDFLARE_*` credentials against the account API (`--json` supported)
tamer destroy            Remove workers + storage + namespaces for an env
tamer reset              Delete + recreate D1 database(s), rerun migrations, redeploy bound workers (dev-loop; d1 only)
tamer env list           List all environments recorded in state (ages, resource counts)
tamer env gc             Destroy ephemeral envs older than --max-age (default 72h; --dry-run, --force, --json)
tamer wfp put            Upload a single-module Worker to a dispatch namespace
tamer wfp delete         Delete a Worker from a dispatch namespace
tamer wfp tenant provision  Runtime: create per-tenant shard groups + utility D1s + upload dispatch scripts (`--namespace`, `--main` optional)
tamer wfp tenant destroy    Runtime: remove tenant scripts + D1s + state (shared envs: `--confirm-tenant`)
tamer wfp tenant list       Runtime: list tenants (`product:workspace`) and their provisioning status (`--json`)
tamer wfp tenant status     Runtime: show one tenant's dispatch scripts, shard groups, and provisioning status
tamer wfp tenant migrate    Runtime: run D1 migrations on tenant shard groups + utility D1s
tamer wfp tenant seed       Runtime: run D1 seed SQL on tenant shard groups + utility D1s (`seedDir`)
tamer wfp tenant add-shard  Runtime: append the next physical shard to a tenant shard group and re-upload the dispatch script
tamer wfp tenant reset      Runtime: delete + recreate tenant D1(s), migrate, reupload dispatch scripts (greenfield squash loop)
tamer secrets    Encrypted vault: init, set, load, copy, get, list, rm, verify, push, migrate (see docs/guide/secrets.md)

Common flags: --env <name> (required for every command; tamer deploy --env <name> no longer silently defaults to prod), --worker <name>, --config <path>, --force, --confirm-env <name> (destroy: required for any env in the root-level protectedEnvs from tamer/project.config.ts — defaults to ["prod","production"] when unset; pass protectedEnvs: ["prod","production","production-eu","qa"] etc. to widen the gate, or [] to opt out — unless --force), --confirm-tenant <workspace> (wfp tenant destroy: same rule, sourced from root-level protectedEnvs), --skip-workers (destroy), --wipe-metadata (destroy: after resources, deletes the env's rows from the shared tamer-state D1 and the env's rows from the shared tamer-secrets D1 — neither database itself is dropped, both are account-scoped and shared across envs; the tamer-artifacts R2 bucket is not cleaned per-env. Use on the last stack in a multi-stack teardown), --dispatch-namespace <name> (deploy), --namespace <name> (wfp tenant: select dispatch namespace; defaults to the only configured namespace), --json (drift / plan: emit machine-readable JSON), --detailed-exitcode (plan: exit 2 instead of 0 when there are pending changes — Terraform-style CI gate), --destroy (plan: preview deletions instead of creates/updates — read-only tamer destroy dry-run). Flags take space-separated values (--env dev); the --flag=value form is not supported.

tamer import flags: --target <kind>:<logical> (<kind> ∈ d1 | r2 | kv | queue | hyperdrive | vectorize | ai_gateway | pipeline | workflow | secret_store | dns_record | dispatch_namespace | worker_route; <logical> from tamer/project.config.ts, or worker key for worker_route), --cf-id <id> (D1 uuid, KV id, R2 bucket name, queue id, hyperdrive config id, vectorize index name, AI gateway id, pipeline id, workflow id, secrets store id, DNS record id, or dispatch namespace name), --shard-date <YYYY-MM-DD> (sharded D1 only), --created-date <YYYY-MM-DD> (R2: optional, defaults to extracting from name), --route-id <id> and --zone-name <z> (worker_route).

Environment variables

Tamer uses the same env vars as Wrangler — see system environment variables:

  • CLOUDFLARE_ACCOUNT_ID
  • CLOUDFLARE_API_TOKEN
  • R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY (optional) — R2 S3 API credentials. When both are set, tamer destroy empties managed R2 buckets (delete all objects, abort incomplete multipart uploads) before removing the bucket. Not the same as CLOUDFLARE_API_TOKEN.
  • TAMER_DEV_BASE_PORT (default 8787, used by dev --all)

Bun loads .env from the current working directory automatically when you run bun src/cli/index.ts ..., so you can keep credentials in a per-project .env (gitignored) instead of exporting them.

API token scopes

Custom token, scoped to the account you target. Names match Cloudflare’s API token permissions reference:

| Permission | Scope | Used by | | ------------------------- | ------- | ------------------------------------------------------------------------ | | Workers Scripts: Edit | Account | deploy, destroy (worker delete), wfp put, dispatch-namespace ops | | Workers KV Storage: Edit | Account | KV apply / destroy / sync | | Workers R2 Storage: Edit | Account | R2 apply / destroy / sync | | D1: Edit | Account | D1 apply / destroy / migrate / sync | | Queues: Edit | Account | Queues apply / destroy / sync / drift / import | | Hyperdrive: Edit | Account | Hyperdrive apply / destroy / sync / drift / import | | Vectorize: Edit | Account | Vectorize apply / destroy / sync / drift / import | | AI Gateway: Edit | Account | AI Gateway apply / destroy / sync / drift / import | | Pipelines: Edit | Account | Pipelines apply / destroy / sync / drift / import | | Workflows: Edit | Account | Workflows apply / destroy / sync / drift / import | | Secrets Store: Edit | Account | Secrets Store apply / destroy / sync / drift / import | | DNS: Edit | Zone | DNS records apply / destroy / sync / drift / import (zones declared in dnsRecords[]) | | Logs: Edit (or Logpush: Edit) | Account | logpushJobs in tamer/project.config.ts: create/delete Workers trace Logpush jobs (Workers Logpush) | | Workers R2 Data Catalog: Edit | Account | pipelinesAuto (apply token): calls POST …/r2-catalog/{bucket}/enable and …/credential on your CLOUDFLARE_API_TOKENnot the minted “catalog+sink” sub-token. With Workers R2 Storage: Edit on the catalog bucket for Pipelines. | | API Tokens: Edit (or equivalent) | Account | pipelinesAuto (apply token): list permission groups and create / delete the two account sub-tokens only. | | Account Settings: Read | Account | Account-scoped reads (and required by Wrangler subprocesses) | | Workers Tail: Read | Account | Optional, only for wrangler tail | | Workers Routes: Edit | Zone | Zone-name tamerRoutes: list/create/delete routes (deploy, sync, destroy, drift). Skip if you only use wrangler-native routes or custom-domain tamerRoutes. | | Zone: Read | Zone | Resolving zone_name → zone id for those routes (same zones as above). |

The dashboard template “Edit Cloudflare Workers” covers most of these; add D1: Edit manually.

Workers trace → Pipelines → Iceberg (pipelinesAuto): On tamer apply, Tamer mints two account sub-tokens, in line with the dashboard: Workers R2 Data Catalog (Edit/Write) only for the stored catalog credential and the r2_data_catalog sink, and Workers Pipelines Send only for Logpush stream ingest. Values are stored in the logpush_pipelines state entry; treat that state as sensitive. Your CLOUDFLARE_API_TOKEN must also be allowed to call the R2 Data Catalog account HTTP API (/accounts/.../r2-catalog/... for enable + credential) on its own — that uses the apply token, not the sub-tokens. The same apply token also needs the account Token API, Logpush, Pipelines, etc. (table above, Account Token API). If sink creation fails with HTTP 422 / code 1012 (existing catalog table), change pipelinesAuto.tableName and/or namespace to a name that does not already exist in that catalog bucket.

For read-only flows (status, dry runs), use the Read variants instead.

Resource kinds

Tamer-managed resources are declared on a worker under resources. Each kind expands to a Wrangler binding plus a state row keyed by derived name.

defineWorker({
  resources: {
    d1: [{ logicalName: "settings", type: "single" }],
    r2: [{ logicalName: "assets" }],
    kv: [{ logicalName: "cache" }],
    queues: [
      { logicalName: "events" },               // producer binding emitted
      { logicalName: "audit", consumerOnly: true }, // queue created, no producer binding
    ],
    hyperdrive: [
      {
        logicalName: "primary",
        origin: {
          scheme: "postgres",
          host: "db.example.com",
          port: 5432,
          database: "app",
          user: "app",
          password: { fromEnv: "PG_PW" },        // never persisted in state
        },
        localConnectionString: "postgres://localhost/app",
      },
    ],
    vectorize: [
      { logicalName: "embeddings", dimensions: 768, metric: "cosine" },
    ],
    aiGateway: [
      {
        logicalName: "openai",
        cacheTtl: 60,           // seconds; 0 = caching off (default)
        authentication: true,   // require Authorization header on gateway endpoint
        rateLimitingInterval: 60,
        rateLimitingLimit: 100,
      },
    ],
    pipelines: [
      {
        logicalName: "events",
        // Arroyo SQL — `events_stream` and `events_sink` must already exist on Cloudflare.
        sql: "insert into events_sink select * from events_stream;",
      },
    ],
    workflows: [
      {
        logicalName: "billing",
        className: "BillingWorkflow",  // class exported from this worker's entrypoint
        // scriptName defaults to the owning worker's deployed script — pin it
        // here only to bind to a different worker's exported class.
        limits: { steps: 50 },         // optional — tracked in state for drift
      },
    ],
    secretsStores: [
      // Tamer manages the *store* (account-scoped container). Secret values
      // inside it are written out-of-band via wrangler / dashboard / CI.
      { logicalName: "apiKeys" },
    ],
    secretsStoreSecrets: [
      // Wire one previously-uploaded secret into this worker. The wrangler
      // generator resolves `store: "apiKeys"` to the live store_id from state.
      { binding: "STRIPE_KEY", store: "apiKeys", secretName: "stripe_key" },
    ],
  },
  vars: {
    // No wrangler binding for AI Gateway — pull the derived id into env vars instead.
    AI_GATEWAY_ID: "${tamer:ai_gateway:openai.name}",
  },
});

Notes:

  • Queues: Tamer creates the queue itself on apply and emits queues.producers[] in the generated wrangler config. Consumer subscriptions stay wrangler-side — set them on the worker config under queues.consumers (passed through verbatim).
  • Hyperdrive: the origin connection (including password and optional access_client_secret) is sent to Cloudflare on apply and discarded — Tamer's state only records the resulting config id, scheme, host and database name. Use { fromEnv: "VAR" } to source secrets from the shell environment so they never enter tamer/project.config.ts. localConnectionString is written to the generated wrangler config for wrangler dev only.
  • Vectorize: dimensions and metric (cosine | euclidean | dot-product) are immutable once the index is created. To change them, run tamer destroy --env <env> --target vectorize:<logicalName> and re-apply. The generated wrangler config emits vectorize[] bindings using the derived index name.
  • AI Gateway: gateways are account-scoped and have no Wrangler binding kind — Workers reference them per-request via env.AI.run(model, opts, { gateway: { id } }) (or by URL on the OpenAI-compatible endpoint). Tamer therefore emits no wrangler fragment for AI Gateway; use the cross-resource ref ${tamer:ai_gateway:<logical>.name} (or .id) in worker vars to inject the derived gateway id. Derived id pattern: aigw-{logical}-{env}. All listed cacheTtl / rateLimiting* / authentication / cacheInvalidateOnUpdate / collectLogs fields are optional with sensible defaults (caching off, rate limiting off, logs on, fixed-window).
  • Pipelines (V1, SQL): Tamer creates the pipeline against /accounts/{id}/pipelines/v1/pipelines and emits pipelines[] in the generated wrangler config. The sql field is mandatory and references streams (sources) and sinks (destinations) by name — those upstream/downstream resources are not Tamer-managed in this iteration; create them via the Cloudflare dashboard or Wrangler before applying, otherwise the pipeline will exist in non-running status. Pipeline SQL changes on existing pipelines require tamer destroy --env <env> --target pipeline:<logicalName> followed by re-apply (Cloudflare V1 has no PATCH endpoint today). Derived name pattern: pipe-{logical}-{env}. Default binding key: PIPE_{LOGICAL} (override with binding).
  • Workflows: Tamer registers the workflow with Cloudflare via PUT /accounts/{id}/workflows/{name} and emits workflows[] in the generated wrangler config. className is the exported class on the owning worker; script_name defaults to that worker's deployed script (env-suffixed for shared envs), so workflow classes co-located with their owning worker need no extra config. Set scriptName explicitly to bind to a different worker. Optional limits.steps (positive integer) is tracked in state and reapplied on drift. Derived name pattern: wf-{logical}-{env}. Default binding key: WF_{LOGICAL} (override with binding). Workflow registrations are immutable in shape but mutable in target — class-name or script-name changes are issued as in-place PUTs; deletes go through DELETE /accounts/{id}/workflows/{name} and tamer destroy removes them transactionally.
  • Durable Objects: Managed resources.durableObjects[] + worker/template doMigrations (append-only wrangler migrations[]). Emitted on tamer deploy / wfp tenant provisionnot tamer migrate. Instances and in-DO SQL schema are app-owned. New deleted_classes tags require --confirm-do-delete. See docs/guide/durable-objects.md.
  • DNS records: declared at the stack root (not per worker), since the same record shouldn't be redeclared by every worker that happens to live behind it. Add dnsRecords[] directly on the defineConfig({ ... }) document. Tamer creates each record via POST /zones/{zoneId}/dns_records and stamps a stable attribution comment (tamer:{stack}:{env}:{logicalName}) so subsequent sync / apply runs can re-adopt the record from a Cloudflare listing even after state loss. Mutable-field drift (content, ttl, proxied, priority, comment) is patched in place via PATCH /zones/{zoneId}/dns_records/{recordId}; type changes follow Cloudflare's delete-and-recreate convention. State key dns_record:{zoneId}:{type}:{name}. The local env is always implicitly skipped (DNS is a real-world side effect — wrangler dev does not own real DNS); use skipEnvs to opt out of additional envs. Set preserveOnDestroy: true on a record to keep it on Cloudflare past tamer destroy (the state row is dropped either way for clean teardown). Requires the DNS: Edit zone-scoped permission for every zone referenced in dnsRecords[].
  • Secrets Store: Tamer manages account-scoped stores (containers) — never the secret values inside them. On apply, each declared secretsStores[] entry is created via POST /accounts/{id}/secrets_store/stores (idempotent — a matching name in secretsStoreListAll short-circuits to the existing id) and the assigned id is recorded in state. To wire a stored secret into a worker, declare a secretsStoreSecrets[] entry referencing the store by its logical name (store: "apiKeys"); the wrangler generator resolves it to the live store_id and emits a secrets_store_secrets[] row with { binding, store_id, secret_name }. The named secret itself must already exist in the store — create it out-of-band via wrangler secrets-store secret create (or the dashboard) so secret material never enters tamer/project.config.ts or tamer-state. Derived store name: sec-{logical}-{env}. Cross-resource refs supported (${tamer:secret_store:<logical>.name|id|binding}). tamer destroy removes managed stores transactionally — Cloudflare cascades the deletion to all contained secrets, so make sure no live worker still binds into the store first.

Stack-scoped DNS records (dnsRecords)

Declared once on the defineConfig({ ... }) root, not per worker. Each entry pins the zone (zoneId), the record type, the DNS name, and the content; everything else is optional.

import { defineConfig } from "@dragonmastery/tamer";

export default defineConfig({
  workers: { /* ... */ },
  dnsRecords: [
    {
      logicalName: "apex",
      zoneId: "0123456789abcdef0123456789abcdef",
      type: "A",
      name: "todo.com",
      content: "192.0.2.1",
      proxied: true,
    },
    {
      logicalName: "www",
      zoneId: "0123456789abcdef0123456789abcdef",
      type: "CNAME",
      name: "www.todo.com",
      content: "todo.com",
      proxied: true,
    },
    {
      logicalName: "spf",
      zoneId: "0123456789abcdef0123456789abcdef",
      type: "TXT",
      name: "todo.com",
      content: "v=spf1 -all",
      ttl: 3600,
      comment: "anti-spoofing baseline",     // appended after Tamer's attribution comment
    },
    {
      logicalName: "mailRouting",
      zoneId: "0123456789abcdef0123456789abcdef",
      type: "MX",
      name: "todo.com",
      content: "mail.todo.com",
      priority: 10,
      preserveOnDestroy: true,                // survive `tamer destroy`
    },
    {
      logicalName: "stagingApex",
      zoneId: "0123456789abcdef0123456789abcdef",
      type: "A",
      name: "todo.com",
      content: "192.0.2.2",
      skipEnvs: ["dev", "prod"],              // only created in `staging`
    },
  ],
});

Cross-resource references (${tamer:...})

CloudFormation !Ref / !GetAtt analogue. Embed ${tamer:<kind>:<logicalName>.<field>} in worker vars or in tamerRoutes[].host / tamerRoutes[].zone to interpolate values from already-applied state at wrangler-config generation time.

| Kind | name (default) | id | binding | | --------------------- | ----------------------------- | --------------------------------- | ---------------------- | | d1 | derived database name | D1 UUID | wrangler binding key | | r2 | derived bucket name | (unsupported — buckets use name)| wrangler binding key | | kv | derived namespace title | KV namespace id | wrangler binding key | | queue | derived queue name | queue id | wrangler binding key | | hyperdrive | derived config name | hyperdrive config id | wrangler binding key | | vectorize | derived index name | vectorize index id | wrangler binding key | | ai_gateway | derived gateway id | derived gateway id | stable cross-ref key (no wrangler binding) | | pipeline | derived pipeline name | server-assigned pipeline id | wrangler binding key | | workflow | derived workflow name | server-assigned workflow id | wrangler binding key | | secret_store | derived store name | server-assigned store id | stable cross-ref key (no wrangler binding) | | dispatch_namespace | resolved per-env namespace | namespace name | — | | worker | env-suffixed deployed script | — | — |

Examples:

defineWorker({
  resources: { r2: [{ logicalName: "assets" }], queues: [{ logicalName: "events" }] },
  vars: {
    ASSETS_BUCKET: "${tamer:r2:assets.name}",
    EVENTS_QUEUE: "${tamer:queue:events.name}",
    INTERPOLATED: "queue=${tamer:queue:events.name};kv=${tamer:kv:cache.id}",
  },
  tamerRoutes: [
    // hosts/zones can also embed references resolved against state
    { host: "api-${tamer:worker:edge.name}.todo.com", zone: "todo.com" },
  ],
});

cf binding DSL (equivalent, typed)

The cf helper builds the same references with autocompletion instead of raw strings:

import { cf, defineConfig } from "@dragonmastery/tamer";

vars: {
  ASSETS_BUCKET: cf.r2("assets").name,
  EVENTS_QUEUE: cf.queue("events").name,
  API_SCRIPT: cf.worker("api").name,
  EDGE_QUEUE: cf.import("net", "edgeQueue"),
},

Each resource helper exposes .name / .id / .binding; cf.import(stack, output) is the ${tamer:import:…} analogue. Both styles resolve identically.

Resolution behavior:

  • Strict mode (apply, deploy, destroy): unresolved references throw TamerReferenceError with the field path (e.g. worker[default].vars.ASSETS_BUCKET) and a "run apply first" hint, so a typo in <logicalName> or a forgotten apply fails fast.
  • Tolerant mode (plan, drift, status, sync): unresolved references are left in place as their original ${tamer:...} placeholder, so read-only commands work on a fresh checkout before the first apply.

Stack outputs (outputs)

CloudFormation Outputs analogue: declare named values your stack publishes after a successful apply. Each value is a Tamer reference string (${tamer:<kind>:<logical>.<field>}) — full-string OR interpolated — resolved against the just-completed state and persisted under TamerState.stackOutputs so it survives across runs and is visible to tamer status (and to sibling stacks via ${tamer:import:<stack>.<output>}, shipping next).

export default defineConfig({
  stack: "ext",
  worker: { main: "src/index.ts", resources: { d1: [{ logicalName: "users", type: "single" }] } },
  outputs: {
    usersDbId:    "${tamer:d1:users.id}",
    eventsQueue:  "${tamer:queue:events.name}",
    adminUrl:     "https://admin.example.com/?db=${tamer:d1:users.id}",
    pinnedLiteral: "v1.0.0",
  },
});

Behavior:

  • Output names must match ^[a-zA-Z][a-zA-Z0-9_-]*$ (CloudFormation-style identifier — keeps them safe in env-var exports, filenames, and the ${tamer:import:…} parser). Validated at config-load time.
  • Resolution runs at the end of a successful apply, after every resource is in state. A typo in any output (unknown logical name, unknown field, unknown kind) fails the apply with TamerReferenceError tagged outputs.<name> and rolls back when --rollback-on-failure is set.
  • Persisted entry shape: { value, source, resolvedAt } — keeps both the resolved literal and the original ${tamer:...} source so status can flag drift.
  • tamer status prints a per-output row tagged resolved (declared source matches persisted source), pending (declared but never applied), stale (config edit since last apply — re-run apply), or orphan (persisted but no longer declared in config).
  • tamer destroy clears stackOutputs after teardown so a future status doesn't surface refs to deleted resources.
  • Re-applies are structurally idempotent: when value + source are unchanged, resolvedAt is updated in memory but state isn't dirtied (no gratuitous revision bump).

Cross-stack imports (${tamer:import:<stack>.<output>})

CloudFormation Fn::ImportValue analogue. One stack publishes named values via outputs: (above); a sibling stack consumes them by referencing ${tamer:import:<stack>.<output>} anywhere a regular ${tamer:...} reference is allowed (worker vars, tamerRoutes[].host / .zone, services[].service, dispatch_namespaces[].namespace, even another stack's outputs). Multiple stacks now coexist in the same account-scoped tamer-state D1 by namespacing the state row as tamer_state:{env}:{stack}, so the consumer just reads the producer's row and looks up the named output.

// fixtures/network/tamer/project.config.ts (the "producer")
export default defineConfig({
  stack: "net",
  description: "Network plane: shared edge router + queue",
  worker: { main: "src/edge.ts", resources: { queues: [{ logicalName: "events" }] } },
  outputs: {
    edgeQueue: "${tamer:queue:events.name}",
    region:    "iad",
  },
});

// fixtures/app/tamer/project.config.ts (the "consumer")
export default defineConfig({
  stack: "app",
  worker: {
    main: "src/index.ts",
    vars: {
      EDGE_QUEUE: "${tamer:import:net.edgeQueue}",
      REGION:     "${tamer:import:net.region}",
      QUEUE_URL:  "https://${tamer:import:net.region}.example.com/q/${tamer:import:net.edgeQueue}",
    },
    tamerRoutes: [{ host: "${tamer:import:net.region}.app.example.com", zone: "example.com" }],
  },
  outputs: {
    region: "${tamer:import:net.region}", // republish a sibling's value
  },
});

Behavior:

  • Stack identity. stack: <string> (required) pins the producer's row key (tamer_state:{env}:{stack}); description? is surfaced by tamer status. Output names match ^[a-zA-Z][a-zA-Z0-9_-]*$ (same as outputs:).
  • Pre-fetch once per command. At command start (apply, deploy, dev, migrate, types, plan, drift, sync, status) Tamer scans the config for ${tamer:import:…} sites, hydrates a read-only StateManager per referenced sibling, and flattens every stackOutputs.<output>.value into an in-memory imports map. Subsequent reference resolution is a pure map lookup — no D1 round-trips mid-build, no partial-failure modes.
  • Strict at write time (apply, deploy): a typo, a sibling that hasn't been applied yet, or an output the sibling never published all fail with TamerReferenceError tagged with the field path (e.g. worker[default].vars.EDGE_QUEUE) and an Available outputs on stack "<name>": … hint. Tolerant at read time (drift, status, sync): unresolved import refs stay in place as their literal ${tamer:import:…} placeholder, and sibling-fetch errors degrade to warnings. plan is partially strict: it tolerates siblings that exist but were never applied (placeholders preserved), but a failure reading the state database aborts the plan — a plan computed from degraded imports would be wrong, and plan output feeds CI gates and apply --plan.
  • Self-imports are filtered. A stack that mistakenly references its own name in ${tamer:import:…} is dropped at scan time so the typo doesn't silently resolve via the row apply is about to write — it surfaces as the same "no imported stack available" error a missing sibling would produce.
  • tamer status shows imports. A "Stack imports:" panel lists every ${tamer:import:…} site grouped by source stack, with the resolved value (or (missing — run \tamer apply` on that stack)) and a per-output resolved/unresolved` tag — the operator-facing equivalent of CloudFormation's stack-export view.
  • Greenfield row key. No compat alias for the legacy literal cfi_state row key (pre-0.26) — first run on an env D1 created by an older bootstrap will see no state and rebuild from sync.
  • local env skips import pre-fetch entirely (no shared state DB exists), so cross-stack composition is a non-local deployment concern.

Adding a new resource kind (resource registry)

Every Tamer-managed Cloudflare resource (D1, R2, KV, Queues, Hyperdrive, Vectorize, ...) is wired through a single registry at src/core/registry/registry.ts. The CLI commands (apply, sync, drift, destroy, status, import) and the wrangler generator iterate resourceModules — none of them special-case any kind. (Caveat: this holds for the worker-scoped storage kinds. The stack-rooted kinds — dispatch namespaces, DNS records, Logpush jobs — plus worker routes, tenants, and secrets are hand-wired inside apply / drift / destroy and are not picked up by the registry loop; a new stack-rooted kind needs explicit command wiring.)

To add a new kind (e.g. Pipelines, Workflows, Pages):

  1. Add the config + state types to src/types.ts (<Kind>ResourceConfig, <Kind>StateEntry, append to WorkerResources and StateEntry).
  2. Implement the per-feature module under src/features/<kind>/ (<kind>.apply.ts, <kind>.sync.ts, <kind>.drift.ts, <kind>.destroy.ts, <kind>.status.ts, <kind>.generate.ts).
  3. Author src/features/<kind>/<kind>.module.ts exporting a ResourceModule (see src/core/registry/types.ts); it wires the per-feature functions into the generic apply / sync / drift / destroy / status / generate / importOne / pickResources / fetchAll shape and declares its kind, label, configKey, and stateEntryType.
  4. Append the module to resourceModules in src/core/registry/registry.ts.

That's it — every command picks the kind up automatically and tamer status, tamer drift, tamer plan, tamer import --target <kind>:<logical>, and the generated wrangler.json will all include it. This 4-step recipe applies to worker-scoped storage kinds; stack-rooted kinds additionally need hand-wiring in the commands listed above.

Routes (tamerRoutes)

Workers can declare HTTP routes that Tamer expands per env:

  • prod / production → bare apex (todo.com, admin.platform.com).
  • any non-prod env (dev, staging, ephemeral pr-1234, …) → {env}.{apex} (dev.todo.com, pr-1234.todo.com).
  • local → no route.

wfp.ephemeralEnvPattern does not affect routing — ephemeral envs follow the same non-prod rule. The pattern controls dispatch-namespace sharing, the env.dev override fallback, and env gc eligibility.

defineWorker({
  scriptName: "portal-ui",
  tamerRoutes: [
    { host: "portal.todo.com" },                  // → {env}.portal.todo.com/* (zone_name=portal.todo.com)
    { host: "api.todo.com", zone: "todo.com" },   // explicit parent zone
    { host: "todo.com", customDomain: true },     // CustomDomainRoute
  ],
});

How routes land in Wrangler vs the Cloudflare API:

  • Zone-name routes (zone_name + pattern, including default tamerRoutes expansion) are not written into generated wrangler.json. They are tracked as worker_route entries in tamer-state and applied with the Workers Routes API after a successful wrangler deploy for that script (so the worker exists before the route binds). tamer sync reconciles listings into state; tamer destroy removes API routes before deleting workers; tamer drift reports mismatches for these routes.
  • Custom-domain tamerRoutes (customDomain: true) and any static routes you declare in worker config are still merged into generated wrangler.json and deployed by Wrangler only.

To opt out per env, set skipEnvs (default ["local"]) or prodEnvs (default ["prod", "production"]). Tamer never strips or rewrites the wrangler-native routes field — declare both if you need static escape-hatch routes alongside tamerRoutes.

Workspace tenants (runtime)

TamerState (D1 tamer-state) includes a tenants map keyed product:workspace for signup-time resources not declared in tamer/project.config.ts. The model is configured under wfp.namespaces.<product>.workers.<service> — one dispatch namespace per product, each holding one or more worker templates (services) that every provisioned tenant gets a copy of.

export default defineConfig({
  stack: "platform",
  wfp: {
    ephemeralEnvPattern: "^pr-",
    tenantDirectory: {
      worker: "dispatch",
      shardGroup: "directory",
      service: "tenant",
    },
    namespaces: {
      // key = product name = dispatch namespace logical name
      todo: {
        workers: {
          default: {                       // service template
            main: "apps/tenant/src/index.ts",
            shardGroups: [
              { name: "primary", binding: "DB", migrationsDir: "db/primary" },
            ],
            d1: [
              { logicalName: "system", binding: "DB_SYSTEM", migrationsDir: "db/system" },
            ],
            vars: { REGION: "iad" },
            services: [...],                // service bindings
            secrets: [...],                 // dispatch secrets
          },
          // ...additional service templates per tenant
        },
      },
    },
  },
});
  • tamer wfp tenant provision --env dev --workspace acme [--product todo] [--namespace todo] — selects the dispatch namespace (--namespace, or the single configured one; --product defaults to the namespace logical name), then iterates every worker template in namespace.workers: for each [service, template] it creates shard 0 for every template.shardGroups[] entry (physical name db_{group}_000_{product}_{workspace}_{env}, binding {prefix}_000), one D1 per template.d1[] utility database, assigns a monotonic tenantNumber and writes the tenant directory row when wfp.tenantDirectory is configured, and uploads the dispatch script named {service}-{workspace} inside the namespace ({service}-{workspace}-{env} for ephemeral envs). --main overrides every service's template.main; otherwise each template deploys its own configured main. Records TenantStateEntry as ready. Use --artifact-key <path/in/tamer-artifacts> instead of --main to deploy from R2. --shards is accepted for backward compatibility but is a no-op under the shard-group model. Additional flags: --compatibility-date <date> / --compatibility-flags <a,b> (override the template's wrangler compat settings for the uploaded script), --module-name <file> (entry module name inside the bundle), and --no-migrate / --no-seed (skip D1 migrations / seed before script upload; both run by default). Re-runs are idempotent for databases (existing shards adopted, missing ones added) but always re-upload scripts. Pass --json for a trailing JSON envelope on stdout — designed for the Cloudflare Container caller (provision-workflow, see container invocation contract).
  • tamer wfp tenant add-shard --env dev --workspace acme --group primary [--service default] — appends the next physical D1 to a tenant shard group, runs migrations + seed, and re-uploads the dispatch script with the new binding.
  • tamer wfp tenant reset --env dev --workspace acme [--product todo] [--kind d1 | --target d1:system | --target shard_group:primary] — deletes and recreates tenant D1(s) (same derived name, new UUID), updates tenant state, runs migrations + seed, and reuploads dispatch scripts. Requires exactly one scope flag. Does not remove the tenant record or dispatch namespace (unlike destroy). Shared envs require --confirm-tenant <workspace> or --force.
  • tamer wfp tenant destroy --env dev --workspace acme [--product todo] --confirm-tenant acme — deletes every dispatch script, every tenant D1 recorded in state (plus any discovered via CF-name identity that state lost — e.g. after a namespace rename), and removes the tenant record (shared envs require confirmation or --force). --namespace <name> also accepts a namespace no longer in config when a tenant in state still references it, so a renamed/removed namespace's tenant can be cleaned up without state surgery. Pass --json for { status: "destroyed"|"noop"|"failed", removed: { workers: { ... } }, errors }.

Namespace keys are immutable identity. A wfp.namespaces.<product> key flows into the dispatch namespace name, tenant state key, and D1/R2 names — renaming it (e.g. aattenant) orphans the old scripts/databases/buckets (Cloudflare can't rename them). Recover with tamer sync (refills state + flags the old namespace) → tamer drift (shows ghosts) → tamer wfp tenant destroy --namespace <old> --workspace <ws> (one tenant) or --namespace <old> --orphans (every tenant under the old key) → tamer destroy --orphans. tamer sync --strict-namespace-guard exits nonzero in CI when tenant entries reference namespaces no longer in config.

  • tamer status --env dev --tenant todo:acme — shows provisioning status and bound resources.

Container invocation contract

The tenant runtime commands are designed to be invoked by a Cloudflare Container (the same image CI uses — see Dockerfile). Build and run:

docker build -t tamer .
docker run --rm \
  -e CLOUDFLARE_ACCOUNT_ID -e CLOUDFLARE_API_TOKEN \
  -v "$PWD":/work -w /work \
  tamer wfp tenant provision --env dev --product todo --workspace acme \
    --artifact-key todo/worker.js --json

Contract:

  • Exit code is the source of truth. 0 on success or no-op, non-zero on failure. provision-workflow should branch on this, not on log scraping.
  • Stdout's last line is a JSON envelope when --json is passed (other lines remain human-readable). Failure path also emits an envelope (status: "failed", error: <message>) before the non-zero exit.
  • No interactive prompts. Shared-env safety still applies — destroy callers must pass --confirm-tenant <workspace> (or --force) explicitly.
  • Idempotent. Workflow retries are safe: re-invoking wfp tenant provision with the same args produces status: "noop" once the tenant is ready; partial-failure resumes pick up from the last persisted shard.

If two writers update state concurrently, persist throws StateConflictError and the CLI exits with code 3; re-run after refreshing state.

Plan (tamer plan)

CloudFormation-style preview of apply + deploy. Reads only — never mutates state or Cloudflare. Shows every D1 / R2 / KV / dispatch namespace / Workers zone route declared in tamer/project.config.ts that isn't yet on Cloudflare, plus declared global Worker scripts that have no deployment (queried directly via GET /accounts/.../workers/scripts/{name}; dispatch-namespace tenant scripts are skipped — those belong to wfp tenant provision / WFP).

tamer plan --env dev                    # human-readable summary, exit 0
tamer plan --env dev --json             # machine-readable for CI
tamer plan --env dev --detailed-exitcode
# ↑ exit 0 if no changes, 2 if changes pending (Terraform convention),
#   non-zero on errors as usual

tamer plan --env dev --out plan.json    # also persist a verifiable plan file

tamer plan --env dev --destroy          # preview what `tamer destroy` would remove
tamer plan --env dev --destroy --json --detailed-exitcode
# ↑ exit 2 when there is anything to delete (CI gate before stack teardown)

Scoped plan / apply / destroy (--target / --kind) — Limit preview, provisioning, or teardown to a single declared resource (--target <kind>:<logicalName>, e.g. d1:settings, r2:assets, dispatch_namespace:workspace, dns_record:apex_txt) or to every resource of one kind (--kind <kind>, e.g. --kind d1). The two flags are mutually exclusive. A scoped tamer apply still runs sync, still regenerates every worker’s wrangler.json, and still resolves the full outputs: block at the end so state stays coherent. A scoped tamer destroy deletes exactly that resource's state entries (every shard, for a sharded D1; or every resource of the kind for --kind) and nothing else — workers, routes, stack outputs, and metadata are untouched; the protected-env gate (--confirm-env) still applies; --wipe-metadata / --skip-workers / --plan are rejected. Destroy additionally accepts shard_group:<groupName> as a --target. Because scoped deletes swallow Cloudflare 404s and still prune state, tamer destroy --target is also the recovery path when a resource was deleted out-of-band (drift missingFromCloudflare) and apply refuses to recreate it. --target / --kind are incompatible with apply --plan and plan --out — saved plan files attest the whole stack. worker_route and worker_script are not supported here (zone routes apply after tamer deploy; scripts are deployed by wrangler).

tamer reset (D1 greenfield loop) — Chains targeted destroy → targeted apply → tamer migratetamer seed → redeploy for workers that bind the reset D1(s): tamer reset --env dev --target d1:app-db or --kind d1 for every declared stack D1. Squashing migration files on disk is manual; reset wipes the Cloudflare database and replays migrations + current seed from scratch. --skip-deploy opts out of the redeploy step (workers keep stale UUID bindings until tamer deploy). D1 only. For local SQLite, tamer reset --env local --target d1:app-db deletes miniflare files and reruns local migrations + seed (no deploy). Protected envs require --confirm-env. See Squashing migrations and Seed.

The plan reuses the drift engine for "would create" detection, so the rules are the same: drift filters by current-stack logicalNames in shared-state setups, and local envs only show storage that would be created in-memory.

For tracked resources whose recorded state has drifted from the declared config, tamer plan also emits update and replace items with terraform-style attribute change detail:

DNS records (1):
  ~ apex -> A todo.com (content: 192.0.2.1 -> 192.0.2.99, proxied: false -> true)
Vectorize (1):
  ± embeddings -> vec-embeddings-dev (dimensions: 768 -> 1536, metric: cosine -> euclidean)

Summary: 0 to create, 1 to update, 1 to replace.

+ is create, ~ is update (in-place PATCH on apply), ± is replace (Cloudflare's API rejects PATCH on the changed field, so apply must delete + recreate — DNS record type change, Vectorize dimensions / metric, Pipelines V1 sql), - is delete (destroy plan only). --json includes the same diffs as items[].changes[]: { field, from, to, kind: "mutable" | "immutable" }. Today the update / replace engine covers DNS records, Workflows, Vectorize, Pipelines, Hyperdrive, AI Gateway, and Logpush jobs; other kinds emit only create / delete.

Destroy preview (tamer plan --destroy)

Mutually exclusive with the forward plan. Walks tamer-state, filters to entries owned by the current tamer/project.config.ts stack (same scoping as tamer destroy / tamer drift), and emits a delete PlanItem per managed kind — D1, R2, KV, Queues, Hyperdrive, Vectorize, AI Gateway, Pipelines, Workflows, Secrets Stores, dispatch namespaces, DNS records, and zone tamerRoutes. Declared global Worker scripts that currently exist on Cloudflare also appear as worker_script deletes. Dispatch-namespace tenant scripts are intentionally excluded — those are managed via wfp tenant provision / wfp tenant destroy. Read-only: never mutates state or Cloudflare.

--out destroy.json writes the destroy plan with the same (config, state, cloudflare) attestation as forward plans (report.mode: "destroy"); tamer destroy --plan destroy.json then recomputes the three hashes and refuses to proceed unless they all match (override with --allow-stale). Mode mismatch is non-overridable: apply --plan rejects destroy plans and destroy --plan rejects forward plans regardless of --allow-stale, so a saved plan can only ever execute the operation the operator reviewed.

tamer plan --env dev --destroy --out destroy.json
git diff destroy.json && code review ...
tamer destroy --env dev --confirm-env dev --plan destroy.json
# Refuses with one of:
#   "config has changed since plan was generated; ..."
#   "recorded state has changed since plan was generated; ..."
#   "Cloudflare-side resources drifted since plan was generated (out-of-band create/delete); ..."
tamer destroy --env dev --confirm-env dev --plan destroy.json --allow-stale

Saved plans + transactional apply

--out plan.json writes a tamer-plan/v1 document containing the plan report and an attestation: SHA-256 hashes of (a) the parsed config, (b) the recorded state's resources/tenants/stack (timestamps and revision counters are excluded so legitimate noise doesn't trigger false mismatches), and (c) the live Cloudflare snapshot at plan time — the per-kind fetchAll + sync result against a fresh in-memory state, with timestamps stripped. cloudflareHash is omitted for env: local and for legacy plans written before drift-aware refresh.

tamer apply --plan plan.json recomputes all three hashes against the current (config, state, live Cloudflare) triple and refuses to proceed if any drifted, mirroring terraform apply <planfile> plus terraform refresh. Out-of-band creates or deletes on Cloudflare between plan and apply are caught here; the recorded state alone wouldn't notice. Pass --allow-stale only when you've reviewed the diff and want to override:

tamer plan --env dev --out plan.json
git diff plan.json && code review ...
tamer apply --env dev --plan plan.json
# Refuses with one of:
#   "config has changed since plan was generated; ..."
#   "recorded state has changed since plan was generated; ..."
#   "Cloudflare-side resources drifted since plan was generated (out-of-band create/delete); ..."
tamer apply --env dev --plan plan.json --allow-stale     # opt-in override

tamer apply --rollback-on-failure snapshots state-entry keys at start and, on any failure, walks every key created during the run in reverse insertion order and asks the owning module's destroyOne to delete the underlying Cloudflare resource (best-effort; failures log a warning and never mask the original error). State entries whose type no module owns are left in place with a warning rather than rolled back. Use it in CI to keep your account clean when an apply fails halfway:

tamer apply --env dev --plan plan.json --rollback-on-failure

Import (tamer import)

CloudFormation import_resources analogue. When state is missing one specific resource that already exists on Cloudflare (created out-of-band, or after a partial restore), import registers it under its logical name without touching the resource itself. Verifies the Cloudflare object exists and that the cf-side name matches Tamer's derived name for the <logical> in --target <kind>:<logical> before writing the state entry.

tamer import --env dev --target d1:settings --cf-id <d1-uuid>
tamer import --env dev --target d1:events --shard-date 2026-01-15 --cf-id <d1-uuid>
tamer import --env dev --target r2:assets --cf-id r2-assets-dev
tamer import --env dev --target kv:cache --cf-id <kv-namespace-id>
tamer import --env dev --target queue:events --cf-id <queue-id>
tamer import --env dev --target hyperdrive:primary --cf-id <hyperdrive-config-id>
tamer import --env dev --target dispatch_namespace:workspace-workers --cf-id workspace-workers-dev
tamer import --env dev --target worker_route:portal_ui --route-id <route-id> --zone-name portal.todo.com
tamer import --env dev --target dns_record:apex --cf-id <dns-record-id>

Use tamer sync for bulk discovery instead. import refuses to overwrite an existing state row that points at a different Cloudflare id.

Drift (tamer drift)

Read-only diff between Tamer state, Cloudflare reality, and the current tamer/project.config.ts. Useful for CI guards and pre-deploy sanity checks; never writes to state or to Cloudflare.

For each managed resource kind (D1, R2, KV, Queues, Hyperdrive, dispatch namespaces, HTTP routes when you use zone-name tamerRoutes, DNS records, Logpush jobs, workspace tenants, and worker secrets (declared-secret reconciliation), and Worker scripts for declared global workers) the report lists three buckets:

  • missingFromCloudflare — state references a resource that no longer exists on Cloudflare (deleted out-of-band). apply will not recreate it while the stale state entry exists; run tamer destroy --env <env> --target <kind>:<logical> to prune the entry (the delete tolerates the 404), then tamer apply to recreate.
  • unrecordedInState — Cloudflare has a resource that matches a declared logical name in this stack's config, but no state entry tracks it (run tamer sync).
  • undeployed — declared in this stack's config, present in neither state nor Cloudflare (run tamer apply).
tamer drift --env dev          # human-readable; non-zero exit when drift
tamer drift --env dev --json   # machine-readable for CI

Drift filters by the current config's logicalNames, so multi-stack environments sharing one tamer-state D1 only report on resources owned by the current stack. Zone-name tamerRoutes appear under HTTP routes (Workers Routes API); custom-domain and wrangler-only routes are not checked here (Wrangler remains their source of truth). The Worker scripts section only emits undeployed (declared but missing on Cloudflare) since global script ids aren't tracked in state today.

State and artifacts

For non-local envs, tamer bootstrap provisions three pieces of account-scoped metadata Tamer owns directly (run once per account — the same resources serve every env and every stack):

  • tamer-state (D1): stores deployment state as JSON (logical resources mapped to Cloudflare IDs — D1 UUIDs, KV IDs, dispatch namespaces, Workers zone route ids for API-managed tamerRoutes, etc.) plus optional CloudFormation-style stack metadata (stack.name, stack.owner) and a last operation marker (lastOperation: command name, in_progress / succeeded / failed, timestamps, error message) updated by apply / deploy / destroy / import. Each successful or failed run also appends a snapshot to operationHistory (newest first, capped at 50 entries) so operators can review recent changes. tamer events (read-only) prints that timeline, optional --limit N (1–100; rejected above 100), and --json for automation. sync merges API listings into this document; Cloudflare remains the source of truth for what exists. The state schema auto-migrates older versions in place on read. Each env+stack combo is isolated by row key tamer_state:{env}:{stack} (where {stack} comes from the root-level stack field in tamer/project.config.ts), so multiple envs and multiple stacks share the same tamer-state D1 without overwriting each other — required by ${tamer:import:<stack>.<output>} cross-stack references.
  • tamer-artifacts (R2): holds Tamer-managed bundles keyed by {resource-type}/{name}/{version}/.... Used by runtime provisioning paths (e.g. wfp tenant provision --artifact-key, read-only at deploy time otherwise).
  • tamer-secrets (D1): account-scoped encrypted secrets vault (see docs/guide/secrets.md). tamer destroy --wipe-metadata deletes the env's rows from here.

Multi-stack flows (fixtures/platform, fixtures/portal, and fixtures/internal) share one tamer-state D1, one tamer-artifacts R2 bucket, and one tamer-secrets D1 per account when you use the normal bring-up scripts; tamer:down passes --wipe-metadata only on the platform destroy (fixtures/platform) so the env's metadata rows are removed from tamer-state + tamer-secrets once at the end. The shared databases themselves persist (other envs may still rely on them); the tamer-artifacts bucket is not emptied per-env (disposable build bundles age out). An operator who wants to remove Tamer entirely from an account must drop the three metadata resources manually via the Cloudflare dashboard.

See Runbook for the platform + portal + internal fixtures (spin-up, tear-down, flags).

Developing

From the repo root after bun install:

  • CLI: bun run tamer -- <command> (root package.jsonbun src/cli/index.ts) or invoke bun src/cli/index.ts directly.
  • Fixtures: reference stacks under fixtures/platform, fixtures/portal, and fixtures/internal. Bring up / tear down with bun run tamer:up / bun run tamer:down — see Runbook.
  • Tests: bun run typecheck and bun run test. CI uses bun run test:report (JUnit → reports/junit.xml, dots output). Coverage: bun run test:coveragecoverage/lcov.info. Details: Testing.
  • CI: .github/workflows/ci.yaml — typecheck + unit tests on pushes and PRs to main / apex.
  • Publish: npm releases via .github/workflows/publish-npm.yaml (Trusted Publishing / OIDC). Packaging: npm-packaging.md. Cross-repo contracts: publish.md.

Fixture smoke test (requires .env under each fixture directory):

bun run tamer:up -- --env dev
# …
bun run tamer:down -- --env dev --confirm-env dev

Documentation

| Doc | Audience | | --- | --- | | Consumer quickstart | Downstream repos — install from npm, first deploy, version pinning | | Brownfield adoption | Downstream repos — legacy CF names, naming hooks, sync/plan flow (no IDs in config) | | Single-Product Multi-Tenant | Architecture guide — WFP tenants, dispatch routing, per-tenant D1 shards | | CLI reference | All commands and flags | | Resource kinds | Declaring resources; adding new kinds | | Field boundary | Which config fields are Tamer-only vs Wrangler passthrough | | Values lifecycle | Where vars/secrets/.env values come from and land | | Config types | Exported TypeScript types; derived name patterns | | Testing | Contributors — test runners, JUnit, coverage | | Publish / integration requirements | Maintainers — cross-repo contracts for DragonMastery stacks | | npm CLI packaging strategy | Maintainers — npm package build and release layout | | Runbook | Contributors — platform + portal + internal fixture walkthrough | | Platform architecture | Platform owners — repos, stacks, runtime model, rollout |

Full documentation site: bun run docs:dev (VitePress).

License

TAMER EVALUATION LICENSE. See LICENSE for terms.