zbc_
Declarative infra · Bun · MIT

One command. The whole environment.

Describe your database, your workers and your whole applications in TypeScript. zbc apply provisions what is missing, converges what drifted, and deploys your code, on your laptop and in CI, by the same path. There is no state file.

$ bunx @zabaca/zbc apply production
$ zbc apply production

→ r2:inbox-raw
 Bucket "myproject-inbox-raw" ready
✓ r2:inbox-raw applied

→ turso:main-db
 Created database "myproject-production" in group "default"
✓ turso:main-db applied

→ cloudflare:web
 Built packages/web
 Deployed: https://myproject-web.workers.dev
✓ cloudflare:web applied

→ cloudflare-email:mail
 SPF, DKIM, DMARC and bounce MX provisioned for mail.myproject.com
✓ cloudflare-email:mail applied

Done. 4 instances, 1 graph.

Five commands is the entire surface area.

01zbc init

Scaffolds the infra skeleton, the SOPS config and your CI workflows. Skips anything that already exists; it will not clobber your repo.

02zbc add

Vendors a module, or a whole app, into your repo as source, installs its dependencies, and tells you which secrets to add and where to get them.

03zbc apply

Sorts the graph from your imports, decrypts your secrets, converges every resource. Idempotent, so run it as often as you like.

04zbc destroy

Tears an environment down in reverse dependency order, which is how per-PR previews clean up after themselves.

05zbc secret

request, list, edit. Reads and writes an environment’s encrypted secrets file without ever printing a value to your terminal.

App templates

zbc add inbox is not a resource. It’s an inbox.

A Cloudflare Worker that receives routed mail, stores raw MIME in R2, and exposes a bearer-authed JSON API, an MCP server at /mcp, and a web UI. Three instance files and zbc apply. Vendored into your repo as source: read it, change it, it’s yours.

zbc add inbox

An agent-accessible email inbox. Routed mail lands as raw MIME in R2 with its metadata in a SQLite Durable Object; reads and sends go through a bearer-authed JSON API, an MCP server at /mcp, and a small web UI.

zbc add warehouse

A Container running dlt and dbt-duckdb. Connectors extract incrementally into a durable append-only raw layer in R2; a daily Cron Trigger materializes schema-declared parquet marts, read at the edge with no container wake.

zbc add secret-relay

A permanent worker that brokers secret requests between the CLI and a human’s browser. It carries ciphertext only. The value is decrypted on your machine, never in the relay and never in an agent’s context.

Every PR gets its own

Preview environments that tear themselves down.

// packages/infra/environments/preview/web.ts
const pr = process.env.PR_NUMBER ?? 'local'

export default cloudflareModule.instance({
  name: 'web',
  config: {
    workdir: 'packages/web',
    workerName: `zbc-web-pr-${pr}`,
    build: { command: 'bun run build' },
  },
})

Open a pull request and zbc apply preview stands up an isolated worker and database, then comments the URL back onto the PR.

Close it and zbc destroy preview takes them down again, dependents first, dependencies after.

Four modules ship today: turso provisions the libSQL database, mints its token and runs your migrations; cloudflare builds and deploys the Worker or Container; r2 provisions object storage; cloudflare-email onboards a sending domain and routes inbound mail. zbc add vendors any of them into your repo as source you can read and change.

Read the source →

Your secrets live in your repo. Encrypted, committed, reviewable.

No dashboard, no vault, no password manager. .sops.yaml lists the public age keys; private keys never leave a laptop, and the ciphertext sits in the same pull request as the code that reads it. Onboarding a developer is a PR. Offboarding is a PR.

zbc secret request opens a link a non-technical human can paste an API key into. It travels end-to-end encrypted through a relay you deploy yourself and lands in secrets.yaml. Whoever asked for it learns only that it arrived.

Live · this page

Those cursors are other people.

They reach you through a NATS server running as a Cloudflare Container, declared as one more instance in the same environment directory as this page, and applied by the same command. No special case.

This page, that NATS server, an inbox, a warehouse and a secret relay are five instances in one environments/production/ directory. One command applies all of them.

connecting…

Companion CLI

apply changes it. c9s is how you look at it.

A k9s-style terminal UI for the account you just deployed into. Workers with live request, error and CPU numbers, Containers, Durable Objects, D1, R2, KV and Queues, in one table you can page through, filter and drill into. It never writes: changing infrastructure stays zbc apply’s job.

$ c9s

Account:  99a19e58    Project:  all

  PROJECT               TOTAL  WORKERS  CONTAINERS  DO  D1  R2  
> agent-canvas          8      2        2           3   0   1   
  foothill-inbox        4      1        0           1   1   1   
  tour-guide            3      1        0           1   0   1   
  zbc-inbox             3      1        0           1   0   1   
  zbc-nats              3      1        1           1   0   0   

Projects(all)[33]  1-5/33

Inside a zbc project there is nothing to configure. It walks up to your zbc.config.ts and decrypts that project’s own secrets.yaml with your age key, so one global install serves every repo and you always get the account you are standing in.

Cloudflare has no namespace, so c9s infers one. It attributes every bucket, database and container to the Worker it belongs to, and :proj foothill scopes each view to that project as you browse.

describes a resource, l tails its logs and s drops you into a running container.

$ bun add -g @zabaca/c9s

Start with an empty repo, or the one you have.

init works on greenfield projects and existing Bun workspaces alike, and never overwrites a file you already wrote.

$ bunx @zabaca/zbc init --ci github