Migration

Everything a project holds, in one file you keep. Move it into another Koolbase, keep it as your own backup, or take it somewhere else entirely. Leaving is meant to be easy — that is what makes arriving safe.

What an export contains

One .tar.gz, from Backups → Your own copy in the dashboard, or from the API:

Export
HTTP
GET /v1/projects/{project_id}/export
Authorization: Bearer <dashboard session>
Inside the archive
text
snapshot.json                schema: collections, rules, append-only, environments, buckets
references.json              declared references
constraints.json             unique constraints
collections/grants.jsonl     one record per line
collections/disbursements.jsonl
storage.json                 every object's bucket, path, size and type — a listing
manifest.json                format version, source project, per-collection counts

Each record line carries the record's id, created_at, updated_at and revision beside its data, so an import can put it back exactly. Collections stream a thousand records at a time, so a million-row collection exports without loading it all at once.

collections/disbursements.jsonl
json
{"id":"8a1c…","data":{"grant_id":"3f2b…","amount":50},"created_at":"2026-09-01T10:00:00Z","updated_at":"2026-09-01T10:00:00Z","revision":1}

What is never exported

Secrets — function secrets, payment and fiscal credentials — are never part of an export. The snapshot carries their names so a restore knows what to ask for, never their values. Storage objects are listed, not copied: the bytes move bucket-to-bucket.

The manifest is written last. A download that was cut off has no manifest, and an import refuses it rather than loading half a project.

Importing

Import
HTTP
POST /v1/projects/{project_id}/import
Authorization: Bearer <dashboard session>
Content-Type: application/gzip

<the archive as the body>

Two rules make an import safe, and both are deliberate:

  • Only into an empty project. If the target already has collections, the import is refused. Merging into existing data means deciding, record by record, which side wins — that is what seed files are for. An import is one predictable operation.
  • All or nothing. Every collection, record and declaration lands in one transaction. A record that does not parse, a reference to a record the archive does not contain, a duplicate under a unique constraint — any of them, and nothing is written.

References and unique constraints are declared after the records, so they validate the real data rather than an empty collection. A reference in the archive that points at a record the archive lacks fails the whole import — the same check that protects a live project.

Ids

Records keep their ids, so every reference between them survives. The one exception: an export from a project on this same Koolbase, imported back into it — a clone. Those ids are already in use, so the colliding records get new ones, and every declared reference that pointed at them is rewritten to match. The result reports how many were remapped. A migration from another Koolbase remaps none.

What an import does not restore

Environment flags, remote config and version policies; functions, crons and triggers; bucket definitions. Those already have a restore path — the snapshot inside the archive. After the import, apply the snapshot to bring them across. Storage objects are listed in storage.json; move the bytes between buckets with any S3 tool.

created_by is cleared on import: the source project's users do not exist in the target. Timestamps and revisions are kept.

Leaving Koolbase

The archive is plain: gzip, tar, JSON and JSON Lines. Nothing in it needs Koolbase to read. Each .jsonl file loads into any database that takes JSON — Postgres with a jsonb column, MongoDB, SQLite, a spreadsheet — and snapshot.json tells you the rules each collection had, so you can rebuild them wherever you go.

Into Postgres, for example
bash
tar xzf koolbase-export.tar.gz
psql -c "CREATE TABLE grants (id uuid PRIMARY KEY, data jsonb, created_at timestamptz)"
jq -c '[.id, .data, .created_at]' collections/grants.jsonl \
  | psql -c "COPY grants FROM STDIN" # or your loader of choice

Arriving from somewhere else

The same archive is how data comes in. Write one — the format above is the whole specification — and import it. Anything that can produce a tar of JSON Lines can produce a Koolbase import: a script over a Firebase export, a pg_dump, a CSV.

Keep your existing ids as the record ids where you can — they must be UUIDs — and put the references between records in references.json so the database enforces them from the first day.