Skip to content

Guides

Deploy and backups

One Node 22 process with no runtime dependencies and one SQLite file. Run it in your own VPC, keep the master key safe, back up the evidence, and know what failover does and does not exist.

Hosted plans run in the EU (Frankfurt). Enterprise customers can run Immiscible inside their own VPC, where nothing leaves the network and Immiscible adds no sub-processor at all. Your security review covers Node and the code; there is no package supply chain beyond that.

#Run it

Shell
docker build -t immiscible .
docker run -d --name immiscible -p 8787:8787 \
  -v imm-data:/data --read-only --tmpfs /tmp \
  -e IMMISCIBLE_MODE=selfhost \
  -e PUBLIC_URL=https://immiscible.internal.example \
  -e IMMISCIBLE_MASTER_KEY="$(openssl rand -base64 32)" \
  -e IMMISCIBLE_ADMIN_EMAIL=platform@example.com \
  -e IMMISCIBLE_ADMIN_PASSWORD="change-me-to-something-long" \
  -e ANTHROPIC_API_KEY=... -e OPENAI_API_KEY=... \
  immiscible

The image runs as an unprivileged user with a fixed uid (10001); the code is read-only to it and the only writable path is /data, so the root filesystem can be mounted read-only. fly.toml, render.yaml and docker-compose.yml in the repository are working starting points.

In self-hosted mode there is exactly one workspace. The owner comes from IMMISCIBLE_ADMIN_EMAIL and IMMISCIBLE_ADMIN_PASSWORD; sign-up closes once that owner exists, and everyone else is invited. Provider keys may come from the environment.

#Configuration

VariableMeaning
PUBLIC_URLthe address people and agents use; links, receipts’ iss and OAuth metadata come from it
IMMISCIBLE_MASTER_KEY32 bytes, base64: seals stored provider keys, upstream credentials and secrets. Required in production
IMMISCIBLE_MODEselfhost for one workspace; unset for the hosted, multi-workspace mode
DATA_DIR, DATABASE_FILEwhere the SQLite file lives (default $DATA_DIR/immiscible.db)
PORTdefault 8787
LOG_FORMATjson for JSON lines on stdout
IMMISCIBLE_KEY_RPM, IMMISCIBLE_AGENT_RPM, IMMISCIBLE_HOOK_RPM, IMMISCIBLE_VERIFY_RPMrate limits per key, per agent, for the Claude Code hook’s tool calls per agent, and for public receipt verification
IMMISCIBLE_SESSION_IDLE_MINUTES, IMMISCIBLE_SESSION_MAX_DAYSconsole session lifetimes

Sign-in providers, Slack and Teams have their own variables: see set up sign-in and approvals in Slack and Teams.

#Health

RouteUse
GET /healthzliveness: the process answers, with its version and uptime
GET /readyzreadiness: the database answers and its schema is the one this build expects; 503 otherwise

npm run check runs the pre-flight checks a deployment should pass before it takes traffic.

#Backups

Shell
node scripts/backup.mjs /backups

Writes a consistent copy of the database while the gateway keeps serving, checks the copy opens and counts its ledger, and keeps the newest 14 (BACKUP_KEEP). The evidence ledger is inside it. Run it at least daily, from cron or your platform’s job runner, and ship the copy off the machine: a backup on the same volume is not a backup. Keep copies for as long as your record-keeping obligations require.

Rehearse a restore, and time it, before you rely on it. A restore is a copy of the file into DATA_DIR and a start; then verify the evidence:

Shell
node scripts/verify-evidence.mjs bundle.json --keys keys-you-kept.json

#Restore drill

With encrypted backups going to a bucket, a drill fetches the newest one, decrypts it into a scratch file and checks every evidence chain against its manifest, without touching live data:

Shell
immiscible-server restore --from-bucket latest --to /data/drill.db

The result, pass or fail and how long it took, is kept beside the backups as restore-drill.json. Security posture in the console and trust.json (backup.lastRestoreDrill) report it. Delete the scratch file afterwards.

#Several processes, one host

Several Immiscible processes on one machine can share one database file in WAL mode. Everything they must agree on is in that file and changed in short BEGIN IMMEDIATE transactions: the evidence chain (one chain, no forks), rate limits, ring-fence counters, provider breakers, freezes and their ceilings, and workspace settings, written by compare and swap so two processes changing different settings both keep their change. A few figures (budget spend, key counters) are flushed on a one-second debounce, so one process’s view can lag another’s by about a second. Evidence is never debounced.

SQLite allows one writer at a time, so this shape scales with cores on one host, not with hosts. Never put the database file on a network file system (NFS, SMB, EFS): SQLite’s locking is not reliable there.

#Failover

Failover across hosts is not built in. Two ways to get it today, both outside Immiscible:

  • Litestream replicates the SQLite file to object storage continuously. It is a backup with a recovery point of seconds, not a hot standby: to fail over, restore onto a new host and start there. Run exactly one writing host at a time.
  • LiteFS replicates to read replicas with one primary. Immiscible does not route writes to the primary or handle promotion, so run it only on the primary and treat promotion as an operator action.

Either way a host failure means a short outage while the standby takes over, and with Litestream the last few seconds of writes can be lost. If your recovery objectives cannot accept that, run it on Postgres (DATABASE_URL), where your provider’s replication and point-in-time recovery apply. One server process per database is what is tested.

#Upgrading

Take a backup, stop, replace the image, start. Schema migrations run on boot and are forward-only, and /readyz stays 503 until the schema matches the build.