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
- Validation is closed and complete. A map missing a declared stack,
naming an undeclared stack, or appearing without a
stacks: list is a hard
error — there is never a fallback value. Two stacks binding the same
project is an error (E_STACK_PROJECT_COLLISION).
- What may actually differ between stacks: instance sizing. Any scalar
validates as a per-stack map, but at deploy time only
instances.{tier, disk_size, availability_type, database_version, disk_autoresize, deletion_protection} may hold different values across
stacks — anything else that differs is a hard error during the generate
step. Per-stack deploy.* values (project, region, public, pinned) resolve
on the CLI side and may always differ.
- The active stack is
--stack if passed, else deploy.stack, else
dev — the same chain for every command (deploy, scan, build,
migrate, firebase, adopt). fluffy-chainsaw local always uses the default
stack.
--project behavior. With a per-stack deploy.project, --stack
alone picks the project; --project alone selects the stack bound to that
project; passing both with a mismatch is refused. With a plain scalar
deploy.project, an explicit --project simply wins.
deploy.stack, deploy.select and deploy.infra-root cannot vary per
stack; neither can instances.region.
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.