Skip to content

Zabaca/zbc

Repository files navigation

zbc

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.

Layout

├── 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

CLI

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 resources

init 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

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/destroy logic via defineModule
  • registry.json — manifest read by zbc 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

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 databaseUrlMAIN_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,
  },
})

Environments

  • productionzbc apply production, triggered by main merge via GitHub Actions
  • previewzbc apply preview, ephemeral per-PR resources, triggered on PR open/push, cleaned up on PR close via zbc destroy preview

Secrets Management

All secrets are committed to the repo, encrypted with SOPS + age. Each developer and CI environment has their own age keypair.

How it works

  • .sops.yaml lists 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

Adding a new developer

  1. Developer generates their own keypair: age-keygen
  2. Developer shares their public key (not secret)
  3. Add the public key to .sops.yaml
  4. Re-encrypt all secrets with the new recipient: sops updatekeys <secrets.yaml>
  5. Developer stores their private key at the default SOPS location:
    • macOS: ~/Library/Application Support/sops/age/keys.txt
    • Linux: ~/.config/sops/age/keys.txt

Removing a developer

  1. Remove their public key from .sops.yaml
  2. Re-encrypt: sops updatekeys <secrets.yaml>
  3. Rotate any secrets they had access to

CI (GitHub Actions)

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.

Onboarding a new project

  1. Add the project under packages/<project>/.
  2. For each environment it needs, add module instances in packages/infra/environments/<env>/ — typically a Turso database and a Vercel deploy, wired via imports.
  3. Put any required secrets (API tokens, provider credentials) into packages/infra/environments/<env>/secrets.yaml, encrypted via SOPS.
  4. Run zbc apply <env> locally to validate. CI will take over on push to main and on PRs.

Releasing the CLI

@zabaca/zbc is published from packages/cli/. Tags follow zbc-cli-v<version>; release commits follow release(cli): @zabaca/zbc <version>.

  1. Bump the version in packages/cli/package.json (semver — patch for template/bugfix tweaks, minor for new commands or modules).

  2. Commit the bump (plus any code/template changes shipping with it):

    git commit -m "release(cli): @zabaca/zbc <version>"
  3. Tag the release commit and push both:

    git tag zbc-cli-v<version>
    git push origin main zbc-cli-v<version>
  4. Publish to npm with Bun (never npm publish — npm strips the bun shebang from bin/zbc.js and breaks the CLI):

    cd packages/cli && bun run publish:npm

    Requires npm auth (npm whoami to verify) and publish rights on the @zabaca scope.

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

Design system

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-viewer

Opens at http://localhost:3000. The viewer shows all components and pages in isolation, with dark/light toggle.

Working with this repo

  • Module/src layout: packages/infra/modules/ and packages/infra/src/ are symlinks into packages/cli/templates/infra/. The cli/templates/ tree is the source of truth (it's what zbc init scaffolds into new projects); this repo is a live consumer of its own templates. Edit modules at packages/cli/templates/infra/modules/<name>/, not via the symlink.
  • Runtime: Bun — use bun everywhere (bun install, bun run, bunx). Do not use npm or yarn.
  • Publishing @zabaca/zbc: use bun publish, never npm 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. No tailwind.config.js.
  • No shadcn/ui — this is a deliberate choice. Components are hand-authored to match the Prose design system exactly.
  • Single-tenant design systempackages/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-b and /visual-review.

About

Zabaca stack bootstrap — zbc CLI + infrastructure modules

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors