fluffy-chainsaw.yaml reference

The complete configuration surface. It is a closed, typed schema: anything not listed here — an unknown collection, an unknown field, a wrong type, an out-of-set enum value — fails fluffy-chainsaw scan with a source-pointing error.

The file sits next to the app (default <dir>/fluffy-chainsaw.yaml). Code decides which resources exist (via markers); this file only tunes them — with two exceptions that have no marker and are declared here directly: instances and auth.

Top-level structure

stacks: [dev, prod]  # optional: the closed set of deployment stacks (see below)

deploy: {}           # deploy parameters: project, region, stack, select, public, pinned

defaults:            # per-collection defaults (a database default never leaks onto a runner)
  databases: { instance: app-db }
  secrets: { unused: true }

databases: {}        # tunes db.connect() markers
instances: {}        # DECLARES the Cloud SQL server(s) — no code marker
buckets: {}          # tunes storage.bucket() markers
topics: {}           # tunes pubsub.topic() markers
secrets: {}          # tunes secret.get() markers
runners: {}          # tunes cloudrun.entrypoint() markers

auth: {}             # DECLARES the auth singleton (one per app) — no code marker
realtime: {}         # DECLARES the realtime singleton — paired with realtime.channel() in code
hosting: {}          # DECLARES the app's own Firebase Hosting site — no code marker
artifact_registry: {} # singleton: the shared runner Docker repo

existing: true is available on instances, databases, buckets, topics, secrets, auth, and artifact_registry — it marks a resource another app owns: looked up at deploy (fails loudly if missing), granted onto, never created or destroyed.

Stacks: one topology, per-stack values

Declare stacks: [dev, prod] and any scalar value in the file may become a {stack: value} map instead. The reading rule is one line: scalar = shared by all stacks; {stack: value} map = varies, and every declared stack must be present. Resource names are never per-stack — topology is identical across stacks by construction, only leaf values differ.

stacks: [dev, prod]

deploy:
  region: us-central1
  project:              # the stack → project binding: one GCP project per stack
    dev:  app-dev
    prod: app-prod
  pinned:               # prod deploys release-pinned digests, never builds
    dev:  false
    prod: true

instances:
  primary:              # declared once → exists in every stack, can't drift
    tier:
      dev:  db-custom-1-3840
      prod: db-custom-8-30720
    disk_size:
      dev:  10
      prod: 100
    database_version: POSTGRES_16   # scalar → identical in both stacks

What to expect

databases.<name>

Field Type Notes
instance string The server it lives on. Required (directly or via defaults.databases.instance); placement is never inferred
existing bool Owned by another app on a (usually shared) instance; its schema is the owner's business
extensions string list Postgres extensions, e.g. [pgcrypto]
migrations.run string The database's schema-change command, run by fluffy-chainsaw migrate as a dedicated migrator identity with DATABASE_URL set to this database. Declaring it drops CREATE from the runner's grants. Rejected on existing: true. See migrations.md

instances.<name> (operator-declared)

All fields optional — strong defaults fill the rest.

Field Type Notes
existing bool Reference a server another app owns and already provisions — looked up at deploy, never created or destroyed by this app. The first app to declare the instance provisions it; every other app sets existing: true.
tier string e.g. db-custom-1-3840. May vary per stack (see Stacks)
region string Reserved — not applied yet; the deploy region comes from deploy.region / --region. Cannot vary per stack
database_version string e.g. POSTGRES_16. May vary per stack
availability_type ZONAL | REGIONAL REGIONAL = HA. May vary per stack
disk_size number GB. May vary per stack
disk_autoresize bool Default true. May vary per stack
deletion_protection bool Default true. May vary per stack

buckets.<name>

Field Type Notes
provider gcs | s3 Default gcs (only gcs has a client today)
existing bool Owned by another app; the owner-only knobs below are then rejected
location string e.g. US
public bool
versioning bool
lifecycle.abort_incomplete_upload_days number > 0

topics.<name>

Field Type Notes
provider gcp | memory Default gcp (real Pub/Sub, or the emulator under fluffy-chainsaw local); memory = in-process bus, single process only
existing bool A topic another app owns — reference it with no pubsub.topic() marker

secrets.<name>

Field Type Notes
existing bool A secret another app owns — granted onto, never created/destroyed
unused bool Suppress the "declared in code but granted to no runner" check

runners.<name>

Field Type Notes
uses object The runner's grants — a map per binding kind, not a flat list (below)
kind cloud-run Optional; the only kind today
trigger object How it's invoked (below)
build.target string Make target that builds the image (default: runner name); last stdout line = image ref
build.makefile string Optional
env string → string Injected onto the service. Quote values that look like numbers/bools
vpc.connector string Serverless VPC connector for egress
vpc.egress all-traffic | private-ranges-only

uses

Field Type Notes
databases string list Declared databases the runner is granted onto
buckets object list { name, prefix, viewer, writer }prefix required (trailing /), at least one of viewer/writer true, one binding per bucket
publishes string list Topics it may publish to
secrets string list Secrets it may read (requires env.GCP_PROJECT)
auth bool Opt into the auth: singleton (injects the auth descriptor)
realtime bool Opt into the realtime: singleton (injects the realtime descriptor + publish rights)

trigger

Field Type Notes
http bool true = public (unauthenticated); omit/false = private (IAM callers only)
ingress all | internal | internal-load-balancing Where it's reachable from
schedules object list { path, cron } — each path needs a cloudrun.scheduled(path, handler) in code
subscriptions object list { topic, path, dead_letter?, max_retries?, ack_deadline? } — each path needs a pubsub.subscription(path, handler) in code; the runner must be private

Subscription bounds: ack_deadline 10–600 s; max_retries 5–100, only with dead_letter; dead_letter must be a declared topic. All schedule/subscription paths on one runner must be unique.

auth (singleton)

Field Type Notes
engine firebase The only engine today
providers string list Supported: google, github
expected_origin string Pin the Origin check behind a proxy
session.ttl string 7d, 12h, 30m; must be within [5m, 14d] (default 7d)
session.store firebase | db Reserved for DB-backed sessions
session.database string Reserved, with store: db
router string When multiple HTTP runners set uses.auth: true, name the one that should receive /auth/** traffic. Without this, having more than one such runner is an error (E_ROUTER_INVALID).
existing bool The project-level auth config (user pool) is owned by another app in the same project — a shared pool: one sign-in works across all apps sharing it. This app skips creating the config and only consumes it. Exactly one app in the project leaves this unset. Each app's Hosting domain still needs adding to the shared Authorized-domains list (Firebase Console, one-time)

realtime (singleton)

Field Type Notes
engine firebase The only engine today (Firebase Realtime Database). Zero other knobs by design: client reads on /channels are auth-gated iff auth: is declared, client writes are always denied. Paired with realtime.channel() in code (dynamic names allowed) and uses.realtime: true on the publishing runner. See realtime.md
router string When multiple HTTP runners set uses.realtime: true, name the one that should receive /realtime/** traffic. Without this, having more than one such runner is an error (E_ROUTER_INVALID).
instance string Name a non-default RTDB instance owned by this app, so several apps can each run their own database in one shared project. Instance ids are globally unique (served at <id>.<region>.firebasedatabase.app); 1–30 chars, lowercase letters/digits/dashes. Unset = the project's default instance (<project>-default-rtdb) — exactly one app in the project may own that

hosting (singleton)

Field Type Notes
site string The non-default Firebase Hosting site this app's frontend deploys to, so several apps can share one Firebase project without republishing each other's frontends. Site ids are globally unique (served at <site>.web.app); 1–30 chars, lowercase letters/digits/dashes. The site is provisioned by the app's own Pulumi stack; custom domains are attached manually in the Firebase Console. Omit the whole block to use the project's default site (id = project id)

artifact_registry (singleton)

Field Type Notes
existing bool The runner Docker repo is one shared repo per project. First app to deploy creates it; every other app in the project sets existing: true so its destroy can never delete the shared repo

deploy (singleton)

The parameters fluffy-chainsaw deploy (and workspace deploy) run with. Every CLI flag beats its field here; in a workspace, the manifest's children[] entry beats this block, which beats the manifest's defaults:.

Field Type Notes
project string GCP/Firebase project ID. May be a {stack: project} map — that map is the stack→project binding (--stack prod then implies the project); values must be distinct across stacks
region string GCP region. May vary per stack
stack string The default stack when --stack is omitted (falls back to dev). Scalar only
select string The infra target: the Pulumi program lives at infra/<select>. Unset = the app dir itself is the program dir. Scalar only
infra-root string Base dir holding infra/<select> roots (default infra, relative to the repo root). Scalar only
public string Firebase Hosting public dir (default public). May vary per stack
pinned bool A pinned stack never builds: deploy loads releases/<stack>.images.json (written by fluffy-chainsaw release, committed by you) and deploys exactly those digests. Missing manifest or a non-digest ref is a hard error. May vary per stack — the usual shape is {dev: false, prod: true}

Naming rules (checked at scan)

Resource Rule
database 1–63 chars, starts with a letter, then letters/digits/underscores
instance 1–98 chars, starts with a lowercase letter, then lowercase letters/digits/dashes
bucket 3–45 chars: lowercase letters, digits, dashes, underscores
topic 3–255 chars, starts with a letter, then letters/digits/- . _ ~ + %; not prefixed goog
hosting site / realtime instance 1–30 chars: lowercase letters, digits, dashes; no leading/trailing dash. Globally unique across all Firebase projects

Validation failures reference stable [E_*] codes — see errors.md.