Tooling

Promote Configuration

Copy a project's collection schema, access rules, and per-environment config into another project — without ever touching its data. The clean way to keep separate dev and prod projects in sync.

When you'd use it

Koolbase recommends separate projects for dev and prod so their data never mixes. The trade-off is that your project's structure doesn't automatically carry across. Promote closes that gap: build and validate everything in your dev project, then promote the configuration to prod in one step. It copies structure, not content.

What it copies

Promoted

  • Collections — names & access rules
  • Unique constraints — fields & case setting
  • Storage buckets — mode, limits, MIME rules
  • Functions — source, runtime, timeout, auth
  • Cron schedules
  • Triggers — the bindings that fire your functions
  • Secret names (never values)
  • Feature flags & their rules
  • Remote config values
  • Version policies

Never copied

  • Records / row data
  • Stored files (buckets arrive empty)
  • Secret values
  • Function version history
  • API keys & credentials
  • OAuth & SMS provider secrets

Clean the source project first

A snapshot is only as clean as the project you take it from. Dev projects accumulate things you never meant to keep — a collection you made while testing, a function you deployed once to try something, a bucket named test-uploads. Apply cannot tell those apart from the structure you care about. It creates all of it, in production, silently.

So before you promote: delete what you do not want in production, then pull a fresh snapshot and look at what you actually captured.

# after cleaning up the dev project
koolbase snapshot pull --project <dev-project-id> --output koolbase.json

# see what you are about to promote
python3 -c "import json; s=json.load(open('koolbase.json')); print({k: len(s.get(k) or []) for k in ['collections','unique_constraints','functions','buckets','crons','triggers','secret_names']})"

If those counts are not what you expect, stop and fix the dev project — not the file. The snapshot is meant to be a faithful description of a project, and hand-editing it means your committed definition no longer matches anything real.

Deleting from the target later is harder than not creating it

Apply never removes anything on its own, and --prune only ever deletes collections — never buckets, functions, crons, or triggers. Something promoted by mistake has to be removed by hand, from a production project. Two minutes of cleanup before the first pull is worth considerably more than that afterwards.

Always preview first

Every promote runs as a diff first. You see exactly what will be added and changed in the target before anything is written. The dashboard previews automatically; the CLI previews with --dry-run.

Dry-run writes nothing

A dry-run computes the full diff against the target and returns it without making a single change. Use it freely to check what a promote would do.

From the dashboard

  1. Open the source project and go to Infrastructure → Promote.
  2. Pick the target project from the dropdown.
  3. Click Preview to see the diff — collections to add or change, and per-environment changes.
  4. Review the summary, then click Promote to apply.

Collections already in sync are collapsed so only the actual changes stand out. Environments that have no matching slug in the target are shown as skipped.

From the CLI

Pull your project's definition from the source, then apply it to the target. By default the CLI reads and writes koolbase.json — a plain-JSON file you commit to your repo and treat as the source of truth for your backend.

terminal
bash
# 1. Pull the source project's definition (writes ./koolbase.json)
koolbase snapshot pull --project SOURCE_PROJECT_ID

# 2. Commit it — your backend definition now lives in version control
git add koolbase.json && git commit -m "Update backend definition"

# 3. Preview what would change in the target (writes nothing)
koolbase snapshot apply --project TARGET_PROJECT_ID --dry-run

# 4. Apply for real
koolbase snapshot apply --project TARGET_PROJECT_ID

Pass -o <file> / -f <file> to use a different filename, or -o - to print to stdout instead of writing a file.

Which project is --project?

On pull, --project is the source you're copying from. On apply, it's the target you're writing to.

Backend as code (CI/CD)

Once koolbase.json lives in your repo, your backend definition is reviewable, versioned, and reproducible. Wire it into CI so a pull request previews the diff and merging to main reconciles your production project — the flow you'd expect from infrastructure-as-code.

CI has no interactive login, so the CLI authenticates from environment variables instead — set KOOLBASE_API_KEY and KOOLBASE_BASE_URL and it skips the login file entirely.

Authority tracks how destructive the operation is. Exporting and applying additively need a write key; pruning needs admin. A read key cannot export — a snapshot carries your function source, so it is not read-tier data.

Apply exits non-zero when anything did not land

koolbase snapshot apply exits 0 only when every item succeeded. Failures and conflicts exit 1, so a job that half-applied fails the build instead of passing quietly. Pass --ignore-partial if you deliberately want to continue past conflicts, and --verbose to list every item rather than only those needing attention.
.github/workflows/koolbase.yml
yaml
name: Koolbase backend

on:
  pull_request:
    paths: ["koolbase.json"]
  push:
    branches: [main]
    paths: ["koolbase.json"]

env:
  KOOLBASE_BASE_URL: https://api.koolbase.com
  KOOLBASE_API_KEY: ${{ secrets.KOOLBASE_API_KEY }}

jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: stable
      - run: go install github.com/kennedyowusu/koolbase-cli@latest
      - run: koolbase-cli snapshot apply --project ${{ vars.KOOLBASE_PROD_PROJECT_ID }} --dry-run

  apply:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: stable
      - run: go install github.com/kennedyowusu/koolbase-cli@latest
      - run: koolbase-cli snapshot apply --project ${{ vars.KOOLBASE_PROD_PROJECT_ID }}

One-time setup

Add a repository secret KOOLBASE_API_KEY — a write-scoped key, or admin if the job prunes — a repository variable KOOLBASE_PROD_PROJECT_ID, and commit koolbase.json. A PR that touches it shows the plan; merging applies it.

What gets copied, in detail

Collections

Matched by name. For each collection, Promote copies the schema and full access-rule definition:

nameCollection identifier (the match key)
read_rule / write_rule / delete_ruleAccess rule expressions
owner_fieldField used for owner-based rules
rule_mode / rule_conditionsStructured rule configuration
append_onlyWhether the collection rejects updates and deletes

Unique constraints

Each collection's unique constraints travel with the snapshot: the fields, and whether matching ignores case. Apply creates any the target lacks, after the collections they belong to exist. A constraint the target already has on the same fields is left as it is; one with a different case setting is reported as a conflict and not changed.

If the target's existing records already break a constraint, apply creates nothing and reports a conflict with the number of duplicate groups: remove the duplicates, then apply again. Koolbase never merges them for you. A dry run cannot check for duplicates, so they only show up on a real apply. Constraints the snapshot does not mention are never removed.

Storage buckets

A bucket's configuration travels: visibility, access mode, size and file-size limits, allowed MIME types, and whether versioning is on. The files inside do not — a cloned bucket arrives empty and ready.

Only versioning can be changed after creation. If a bucket of the same name already exists in the target with any other setting different, apply reports a conflict and leaves it alone rather than deleting and recreating it, which would take its contents with it.

Functions

Source, runtime, timeout, auth requirement, and enabled state travel together. The target redeploys from the snapshot — no local checkout, no build step — and the deployment goes through exactly the checks a normal deploy does: import validation, dependency resolution, the package denylist, and a vulnerability scan.

A clone can refuse a function your source project still runs

Advisories are checked against current data, not the data from when the function was first deployed. A dependency that was clean in May can be blocked in July. Your original keeps running because nothing redeployed it; the clone is a fresh deploy and is held to today's standard. Apply reports which package and which advisory. This is working as intended — the alternative is silently seeding a new project with a known-vulnerable dependency.

For dart-standard functions the snapshot carries the pubspec you wrote, not the merged one, and the runtime string pins the exact profile version — so a clone resolves to the same dependency set as the original, today or in two years.

Triggers and crons

Triggers are what make a cloned function actually fire, so they travel as their own section — a project with every function deployed and nothing bound to events is not a working clone. Crons carry their environment slug and are matched the same way per-environment config is.

Per-environment config

Environments are matched by slug. For each matched environment, Promote copies:

Feature flagsenabled state, rollout %, kill switch, description — and each flag’s rules
Remote configkey + value + description
Version policiesper platform: min/latest version, force-update, message, store URL

Secrets: names travel, values don't

A snapshot lists which secrets a project expects, never what they contain. Values are encrypted at rest and are not returned by any read path, so there is nothing for an export to copy even in principle.

Apply reports the ones the target is missing, so finishing a clone is a checklist rather than a hunt:

Secrets: 2 pending
  · FCM_SERVICE_ACCOUNT — awaiting a value
  · MESSAGING_SEND_KEY — awaiting a value

Secrets needing values (2) — the clone is ready except these:
  koolbase secrets set FCM_SERVICE_ACCOUNT --project <target>
  koolbase secrets set MESSAGING_SEND_KEY --project <target>

Why apply doesn't create them empty

It could — an empty value is storable. But a secret that exists and is blank fails deep inside whatever uses it, at runtime, in production. A secret that is absent fails at the boundary with a clear message. Pending is not a failure: a clone whose only outstanding work is entering values has succeeded at everything apply can do.

Conflicts

Some differences cannot be reconciled without destroying something. Apply reports these, leaves them exactly as they are, and carries on with the rest — it never guesses on your behalf.

Buckets: 1 conflict
  ! profiles — bucket exists with different immutable settings and was left
               unchanged; converging would require deleting it and its objects
      access_mode (snapshot="owner" destination="project")

Bucket settings

Access mode, visibility, size limits, and allowed MIME types are fixed when a bucket is created. Changing one means deleting the bucket, which means deleting the files in it. Only versioning can be reconciled.

Cron expressions

A schedule's expression cannot be edited in place. If the target runs the same function on a different schedule, apply reports both rather than adding the new one alongside the old — otherwise the job would fire twice, forever. Delete the old schedule if the new one should replace it.

Trigger state

A trigger that exists in the target with a different enabled state cannot be flipped in place. Reported rather than ignored: a trigger that is live when the snapshot says otherwise fires on every matching event.

Missing dependencies

A cron whose environment or function is absent from the target is reported rather than created — a schedule pointing at nothing is worse than one that did not arrive.

How merging behaves

BehaviorWhat happens
Create + update by defaultApply creates missing resources and updates existing ones, and by default never deletes.
Removals need --pruneA collection in the target but absent from the file is reported and left untouched — unless you opt in with --prune (see Pruning below).
Unmatched environments are skippedA source environment with no matching slug in the target is reported as skipped — it is not auto-created.
Flag rules are replacedPromoting a flag replaces that flag’s rule set in the target (rules have no stable cross-project identity).
IdempotentRe-running the same promote against an in-sync target is a no-op.
Non-transactionalResources are applied one at a time, not in a single transaction. If an apply fails partway, just re-run it — idempotency lets it safely finish.

Create missing environments first

Promote matches environments by slug and won't create them. If your target is missing an environment you want to promote into, create it in the target project first (so it gets its own keys), then run Promote.

Pruning

By default apply never deletes — removing a collection from your file does nothing on its own. To make the file fully canonical, so it can express deletion too, pass --prune. Pruning is deliberately conservative: it can only delete what apply itself created, and never silently destroys data.

Manifest-owned onlyPrune deletes only collections that a previous apply created. Anything created in the dashboard is never touched — it is reported as kept.
Data is protectedA collection that still holds records is refused. Pass --force to delete it — and its records — anyway.
Always shown in the planA --dry-run with --prune lists exactly what would be pruned, blocked (needs --force), or kept, before anything is deleted.
terminal
bash
# Preview what prune would remove (writes nothing)
koolbase snapshot apply --project TARGET_PROJECT_ID --prune --dry-run

# Delete manifest-owned collections absent from the file
koolbase snapshot apply --project TARGET_PROJECT_ID --prune

# Also delete collections that still hold records
koolbase snapshot apply --project TARGET_PROJECT_ID --prune --force

What prune can and cannot delete

Prune removes collections this tool created that are absent from your file — and with --force, it removes their records too. That is irreversible.

It never touches buckets, functions, crons, triggers, or secrets. Collections carry a record of whether this tool created them and nothing else does, so pruning anything else would mean deleting on a guess about ownership — for a bucket, that guess would take the stored files with it.

API reference

EndpointDescription
GET /v1/projects/{id}/snapshotExport the source project's config as a snapshot.
POST /v1/projects/{id}/snapshot/applyApply a snapshot to the target. Add ?dry_run=true for a diff with no writes.