Zabaca's stack bootstrap. Holds the zbc CLI and the infrastructure modules used to provision and deploy Zabaca projects.
Projects live here under packages/<project>/; per-environment module instances live under packages/infra/environments/<env>/. Applying an environment is one command — zbc apply <env> — which discovers every instance in the environment directory, resolves the dependency graph from imports, decrypts secrets, and converges each service to the desired state.
├── zbc.config.ts # project metadata + environment list
├── .sops.yaml # age public keys for secrets encryption
├── vercel.json # git.deploymentEnabled: false
│
├── packages/
│ ├── cli/ # zbc — CLI source + bundled module templates
│ │ ├── src/
│ │ │ ├── commands/ # init, add, apply, destroy
│ │ │ ├── engine/ # apply graph + secret loading
│ │ │ └── utils/
│ │ └── templates/ # source of truth scaffolded by `zbc init` / `zbc add`
│ │ ├── infra/
│ │ │ ├── modules/ # turso, vercel, … (each with registry.json)
│ │ │ └── src/ # defineModule helpers + shared types
│ │ ├── workflows/ # CI workflows (preview.yml, production.yml)
│ │ ├── root/ # package.json, tsconfig.json scaffolds
│ │ ├── sops.yaml
│ │ └── zbc.config.ts
│ └── infra/ # live consumer of the templates above
│ ├── modules → ../cli/templates/infra/modules # symlink
│ ├── src → ../cli/templates/infra/src # symlink
│ └── environments/
│ ├── production/ # add module instances per project
│ └── preview/ # ephemeral preview resources
│
└── .github/
└── workflows/
├── production.yml # zbc apply production on push to main
└── preview.yml # zbc apply/destroy preview on PR events
zbc init [project] [--ci github] # scaffold zbc into a repo (greenfield or existing)
zbc add <module> # add a built-in module (turso, vercel, …)
zbc apply <env> # apply all module instances for an environment
zbc apply <env> <instance> # apply a specific instance (+ its dependencies)
zbc destroy <env> # tear down ephemeral resourcesinit is the one-time scaffold. It drops zbc.config.ts, .sops.yaml, the packages/infra/ skeleton, and (with --ci github) the workflows. It does not add modules — those come on demand.
add brings in a single module: copies the module's index.ts into packages/infra/modules/<name>/, runs bun add for its declared dependencies, and prints the secrets you need to put in secrets.yaml along with the provider's signup/token URLs.
apply is declarative and idempotent. Run it the first time — everything is provisioned and deployed. Run it again — no-op except code deploy. Config changed — it converges. Same command locally and in CI.
For ephemeral instances (ephemeral: true), apply destroys and recreates the resource on every run, ensuring a clean state. destroy tears down ephemeral resources (reverse dependency order) and is used for cleanup when a PR is closed.
Modules live in packages/infra/modules/ (consumer-side) — really at packages/cli/templates/infra/modules/<name>/ (source of truth). Each module is a directory with two files:
index.ts— schema (zod) +apply/destroylogic viadefineModuleregistry.json— manifest read byzbc add: files to copy, npm dependencies, required secrets, signup/token URLs, post-install instructions
A new built-in module = drop a directory under packages/cli/templates/infra/modules/<name>/ containing those two files. It's then available via zbc add <name> in any consumer repo.
Module shape (index.ts):
import { z } from 'zod'
import { defineModule } from '../../src/define-module'
export const tursoModule = defineModule({
name: 'turso',
configSchema: z.object({
orgName: z.string(),
dbName: z.string(),
group: z.string().default('default'),
primaryLocation: z.string().default('iad'),
ephemeral: z.boolean().default(false),
}),
outputs: z.object({
databaseUrl: z.string(),
authToken: z.string(),
}),
async apply(config, ctx) {
// idempotently ensure database exists
return { databaseUrl, authToken }
},
})Instances live in packages/infra/environments/<env>/ and wire a module to specific config:
// packages/infra/environments/production/main-db.ts
import { tursoModule } from '../../modules/turso'
export default tursoModule.instance({
name: 'main-db',
config: {
orgName: 'zabaca',
dbName: 'myproject-production',
primaryLocation: 'aws-us-west-2',
},
})// packages/infra/environments/production/web.ts
import { vercelModule } from '../../modules/vercel'
import mainDb from './main-db'
export default vercelModule.instance({
name: 'web',
imports: [mainDb],
config: {
projectName: 'myproject-production',
domain: 'myproject.com',
},
})Imports are between instances — typed, refactor-safe, with outputs flowing from dependency to dependent. Outputs from imported instances are synced to the downstream service as environment variables (e.g. main-db's databaseUrl → MAIN_DB_DATABASE_URL on the Vercel project).
Ephemeral preview instances use dynamic naming and destroy+recreate on every apply:
export default tursoModule.instance({
name: 'main-db',
config: {
dbName: `myproject-preview-pr-${process.env.PR_NUMBER}`,
ephemeral: true,
},
})- production —
zbc apply production, triggered by main merge via GitHub Actions - preview —
zbc apply preview, ephemeral per-PR resources, triggered on PR open/push, cleaned up on PR close viazbc destroy preview
All secrets are committed to the repo, encrypted with SOPS + age. Each developer and CI environment has their own age keypair.
.sops.yamllists all age public keys (committed to repo) as recipients- Secrets are encrypted to all recipients — anyone listed can decrypt
- Each developer's private key stays on their machine only (never shared)
- No secrets stored in Vercel dashboard, password managers, or other external systems
- Developer generates their own keypair:
age-keygen - Developer shares their public key (not secret)
- Add the public key to
.sops.yaml - Re-encrypt all secrets with the new recipient:
sops updatekeys <secrets.yaml> - Developer stores their private key at the default SOPS location:
- macOS:
~/Library/Application Support/sops/age/keys.txt - Linux:
~/.config/sops/age/keys.txt
- macOS:
- Remove their public key from
.sops.yaml - Re-encrypt:
sops updatekeys <secrets.yaml> - Rotate any secrets they had access to
CI has its own age keypair. The private key is stored as a single GitHub Actions secret (SOPS_AGE_KEY). The public key is listed in .sops.yaml alongside developer keys.
- Add the project under
packages/<project>/. - For each environment it needs, add module instances in
packages/infra/environments/<env>/— typically a Turso database and a Vercel deploy, wired via imports. - Put any required secrets (API tokens, provider credentials) into
packages/infra/environments/<env>/secrets.yaml, encrypted via SOPS. - Run
zbc apply <env>locally to validate. CI will take over on push to main and on PRs.
@zabaca/zbc is published from packages/cli/. Tags follow zbc-cli-v<version>; release commits follow release(cli): @zabaca/zbc <version>.
-
Bump the version in
packages/cli/package.json(semver — patch for template/bugfix tweaks, minor for new commands or modules). -
Commit the bump (plus any code/template changes shipping with it):
git commit -m "release(cli): @zabaca/zbc <version>" -
Tag the release commit and push both:
git tag zbc-cli-v<version> git push origin main zbc-cli-v<version>
-
Publish to npm with Bun (never
npm publish— npm strips the bun shebang frombin/zbc.jsand breaks the CLI):cd packages/cli && bun run publish:npm
Requires npm auth (
npm whoamito verify) and publish rights on the@zabacascope. -
Verify the new version is live:
npm view @zabaca/zbc version
Note: existing scaffolded repos have their own checked-in workflows from whenever they last ran zbc init — template changes do not flow into them automatically. They need a re-scaffold or manual patch to pick up workflow updates.
The Prose design system is split across two packages:
packages/design-system/— pure component library. No build, no app. Exports React components + CSS tokens.packages/design-system-viewer/— Astro showcase app that consumes the library via@zbc/design-system. The first proving ground for the consumer pattern.
Run the viewer locally:
bun run dev # turbo dispatches to @zbc/design-system-viewerOpens at http://localhost:3000. The viewer shows all components and pages in isolation, with dark/light toggle.
- Module/src layout:
packages/infra/modules/andpackages/infra/src/are symlinks intopackages/cli/templates/infra/. Thecli/templates/tree is the source of truth (it's whatzbc initscaffolds into new projects); this repo is a live consumer of its own templates. Edit modules atpackages/cli/templates/infra/modules/<name>/, not via the symlink. - Runtime: Bun — use
buneverywhere (bun install,bun run,bunx). Do not use npm or yarn. - Publishing
@zabaca/zbc: usebun publish, nevernpm publish. npm strips non-node shebangs from bin entries, breaking the CLI. Run:cd packages/cli && bun run publish:npm. - Styling: Tailwind CSS v4 — uses the new
@import "tailwindcss"syntax and CSS-first config. Notailwind.config.js. - No shadcn/ui — this is a deliberate choice. Components are hand-authored to match the Prose design system exactly.
- Single-tenant design system —
packages/design-system/is purpose-built for Zabaca. Do not treat it as a generic component library. .claude/directory — mostly gitignored. The exception is.claude/skills/, which is committed and contains AI slash command definitions.- Mode A vs Mode B — design system authoring (Mode A) happens in claude.ai/design; implementation (Mode B) happens here, governed by
/mode-band/visual-review.