Backup & Restore
STELLA can export an entire deployment to a single bundle and restore it onto another machine — an exact clone with secrets intact. Use this for disaster-recovery backups and for moving a deployment to new hardware.
A bundle contains:
- The database — every application table (users, projects, sessions, messages, agent types, env-var templates, …), exported as JSON.
- The agent packages — every uploaded custom-agent zip from
AGENT_STORAGE_PATH. - The deployment config — the wizard's
.env(secrets + settings:ENV_VAR_ENCRYPTION_KEY,JWT_SECRET, API keys, LiveKit, TTS, …), so the clone comes up identical. - A manifest — metadata that makes a bundle safe to move between machines (see What the manifest guards).
Excluded by design (rebuilt on the target, not data): agent Docker images, TTS/STT model weights, and live LiveKit rooms.
The backup engine is pure Node + Postgres — no database-specific tooling such as
pg_dump or zip is needed anywhere. Table fidelity (timestamps, big integers)
is handled by Postgres itself, so a restore reproduces the source exactly. (The
scripts still use the host's normal deploy toolchain — node and kubectl; see
Prerequisites.)
Schema-driven — extends itself. The set of tables a bundle covers is derived
from the live Prisma schema, not a hand-maintained list. Add a model to
schema.prisma and it is automatically included in every backup and restore —
there is no list to update and no way to silently leave a new table out. The only
hand-maintained input is a small opt-out list of high-volume observability tables
(METRICS_MODELS) that are excluded by default; forgetting to add a new table
there is harmless (it just exports as normal data).
Bounded memory at any size. Every stage streams: tables are paginated into fixed-size chunk files (never one giant JSON string), the zip is read/written one entry at a time, and encryption streams through the cipher. A bundle the size of the whole deployment moves through constant RAM — the practical ceiling is disk, not memory. Export reads under a single consistent snapshot; the destructive part of import runs in one transaction, so a failure mid-restore rolls back instead of leaving a half-wiped database.
Because the bundle embeds the deployment config, it contains every secret —
password hashes, ENV_VAR_ENCRYPTION_KEY, API keys. A leaked bundle is a total
compromise. Encrypt it (see below) and store/transfer it only over trusted
channels.
Optional encryption
Export can encrypt the whole bundle with a passphrase (AES-256-GCM, scrypt-derived
key — pure Node, no external tools). An encrypted bundle is named ….zip.enc.
Restore detects encryption and asks for the passphrase. Carry the passphrase
separately from the bundle — it is the one secret that is never written into the
file.
Who can do this
- Export runs only as a wizard/deploy script (
scripts/backup-export.sh), because gathering the data, the agent-package volume, and the deployment config together is a deploy-layer operation. There is no UI export. - Restore is a wizard/deploy script (
scripts/backup-restore.sh) for a full relocation (config + data), or a data-only import from the Admin Dashboard (/settings/admin, SystemAdmin only) when the deployment is already configured.
Prerequisites
The export/restore scripts run on the deploy host (the machine you run
start-k8s.sh from) and need its standard toolchain — node, npx, and
kubectl. These are already required to deploy STELLA, and the scripts check
for them up front, failing with the exact install command if any is missing.
Export is a logical backup, so it needs a running system:
- Postgres must be running — the database is read live (a stopped database is just opaque files and cannot be exported).
- The backend pod must be running — the export engine runs inside it (the only place that can see both the database and the agent-package volume).
backup-export.sh verifies both before doing anything and tells you which part
is down if not. A fully wound-down deployment cannot be exported — bring it up
(or scale Postgres + backend up) first.
Guided wizard (easiest)
For an interactive flow that walks you through both directions — choosing encryption, metrics, and the bundle to restore — run:
./scripts/start-k8s.sh --backup
It prompts for the high-level choices and then runs the same export/restore scripts below (which still handle passphrase entry and confirmations). The direct script invocations remain available for automation/CI.
Export
# Writes ./stella-backup-<timestamp>.zip
./scripts/backup-export.sh [--production|--local]
# Include the high-volume metrics/observability tables (larger bundle)
./scripts/backup-export.sh --include-metrics
# Encrypt the bundle at rest (prompts for a passphrase) → ….zip.enc
./scripts/backup-export.sh --encrypt
Under the hood the script execs the in-pod backup CLI to produce the data bundle
(only the backend pod can see both the database and the package volume), copies
it out, embeds the deployment .env, and optionally encrypts the result.
Restore — full relocation (script)
This permanently replaces ALL data and config in the target namespace. It cannot be undone.
./scripts/backup-restore.sh --in stella-backup-<timestamp>.zip [--production|--local]
The script: backs up the current .env, installs the restored config, recreates
the stella-ai-secrets secret and restarts the backend (so the restored
ENV_VAR_ENCRYPTION_KEY and keys take effect), then imports the database and
agent packages in-pod — overwriting all data. Intended for a fresh target
already brought up with start-k8s.sh.
Restore — data only (admin UI)
When the target is already configured with the matching ENV_VAR_ENCRYPTION_KEY,
an admin can restore just the data from the dashboard:
- Settings → Admin Dashboard → Restore from backup.
- Import backup…, choose the bundle, enter the passphrase if it is encrypted.
- Confirm Overwrite everything?.
This path restores data + packages but not deployment config — so the target must already have the correct encryption key, or the key-fingerprint guard will stop the import.
Restore empties the bundle's tables with TRUNCATE … CASCADE. When the bundle
excludes metrics (the default), truncating core tables such as User/Session/
Room cascades into the metrics/log tables that reference them — so those tables
are emptied even though they are not restored. This is expected and harmless when
cloning onto a fresh target, but on a live system it drops the target's
existing metrics/logs. The import report lists exactly which tables this affected.
Export with --include-metrics if you need them carried over.
What the manifest guards
Before any data is written, import checks the bundle's manifest against the target server:
| Guard | Behaviour on mismatch |
|---|---|
| Format version | Hard abort — the bundle layout isn't understood. |
| Migration head | Hard abort — the bundle's schema doesn't match the target's migrations. Deploy the matching app version first, then retry. |
| Encryption-key fingerprint | Hard abort — restored secrets would not decrypt. |
The full restore script recreates the secret from the bundle's config before
importing, so the encryption-key guard passes automatically. For the data-only
UI path, the target must already run the original key. A --allow-key-mismatch
override exists (data-only "I'll re-enter secrets" restores) but is intentionally
not surfaced in the dashboard.
Relocating to a new machine — runbook
- On the source, run
./scripts/backup-export.sh --encryptand copy the….zip.encto the target. Keep the passphrase separate. - On the target, bring up the stack with
start-k8s.shand the same app version (so the migration head matches). - Restore:
./scripts/backup-restore.sh --in <bundle>— it applies the config, recreates secrets, restarts the backend, and imports the data. - Verify: login works, projects/sessions/messages are present, agent packages resolve, and env-var-template secrets decrypt.
This restores persistent data and config and keeps encrypted secrets usable. It is a disaster-recovery / relocation capability, not a zero-downtime cutover — in-flight voice sessions are not migrated.