Docs menu

Configure your server

Storage and encryption

Decide where files and bot homes live, how content is encrypted at rest, and how large uploads may be.

Data directory

DATA_DIR (/data in the Compose files, ./data in a source checkout) holds uploaded and generated files, bot homes and caches. The API and worker must share it and it must persist. Bot homes and new artifacts can live in object storage instead.

Content encryption

Preá encrypts chat messages, thread events, compacted history summaries, memory documents and their revisions, and uploaded or generated files before they reach PostgreSQL or DATA_DIR. Names, ids, timestamps and other metadata stay readable. Credentials are always encrypted, whatever this setting says. Details and the threat model are in content encryption.

The key. Content is encrypted under keys derived from ENCRYPTION_KEY (and ENCRYPTION_MASTER_KEYS, if set). Without them, a backup of the database and DATA_DIR cannot be read, by you or by anyone else. install-images.sh says so once, when it generates the key:

Back up ENCRYPTION_KEY from /path/to/prea/.env. Encrypted content cannot be recovered without it.

It never prints the key. Store a copy somewhere other than the server and its backups.

The switch. CONTENT_ENCRYPTION=on|off in .env:

Install Unset means
Compose files, published images, Railway on
Desktop app, This computer off (the computer’s disk encryption covers it)
Source checkout with NODE_ENV=development or PREA_ALLOW_DEV_SECRETS=1 off

With off, new content and files are stored readable, so you can query your own database directly and a lost key cannot lose them. Content encrypted earlier still reads with the key, so switching off never makes data unreadable. To convert it too, run content-encryption decrypt. Any other value stops the API and worker at startup.

To switch on later, set CONTENT_ENCRYPTION=on, restart, then encrypt what was stored while it was off. From the published-images folder (other stacks use their own Compose file):

docker compose -f docker-compose.images.yml exec api bun run --filter @orbin/api content-encryption migrate
docker compose -f docker-compose.images.yml exec api bun run --filter @orbin/api content-encryption status

Search works in both modes and while rows are mixed.

Reading your data. content-encryption export writes decrypted threads, messages, memory documents and revisions as JSON lines. It reads the whole database by default; narrow it with --space <id>, --thread <id> or --bot <id>. It only reads.

docker compose -f docker-compose.images.yml exec -T api \
  bun run --silent --filter @orbin/api content-encryption export --bot <bot id> > prea-export.jsonl

The export is plaintext: store it like the database itself. Files are not included; use content-encryption decrypt to make them readable on disk.

New credentials use versioned AES-GCM with per-record salt and row-bound AAD. Legacy ciphertext stays readable.

Bot homes in object storage

Each bot computer has a home: its files, logged-in browser profiles and CLI tokens. By default homes live under DATA_DIR/homes on the server. Set OBJECT_STORAGE_BUCKET to keep them in any S3-compatible bucket instead, so DATA_DIR is only a cache and API and worker hosts no longer need a shared disk for homes. Leave it unset to keep homes on local disk, as before. The same connection also stores new artifacts; see shared storage migration for migrating existing files and push registrations.

OBJECT_STORAGE_BUCKET=prea-homes
OBJECT_STORAGE_ENDPOINT=https://storage.example.com
OBJECT_STORAGE_REGION=us-east-1
OBJECT_STORAGE_ACCESS_KEY_ID=replace-me
OBJECT_STORAGE_SECRET_ACCESS_KEY=replace-me
# Optional: a key prefix, when the bucket holds anything else.
OBJECT_STORAGE_PREFIX=
# Optional: true for services that address buckets in the path rather than the host name.
OBJECT_STORAGE_PATH_STYLE=false

Only the API and worker read these settings. Bot computers never receive the credentials or links to stored objects; files still reach them through the server, as before. The settings apply to every provider, including Docker computers, which keep mounting their cached home.

Encryption. Homes are encrypted on the server before upload, whatever CONTENT_ENCRYPTION says; the bucket only ever holds ciphertext. Each home has its own random data key, kept on the computer’s database row and wrapped by its space’s content key (see content encryption). Each stored revision is one object, sealed in authenticated segments bound to that home and revision, so a modified, truncated or swapped object fails to restore instead of restoring the wrong files. Object keys are random ids (homes/<id>/<revision>) that name no bot, user or space. Back up the database and the encryption keys as before: the bucket cannot be read without them.

The bucket. Use a private bucket, or a prefix of one, for this deployment alone: the worker deletes objects under homes/ that no home in this database owns. Block public access. The credentials need to read, write, list and delete objects in it, and nothing else. Turn on bucket versioning, with a lifecycle rule that expires noncurrent versions after your backup window, so a mistaken deletion can be undone. scripts/backup.sh does not copy the bucket; versioning, or the service’s own replication, is its backup.

What is stored. Each save uploads the whole home as one object (in parts of up to 16 MiB for large homes) and then deletes the previous one, so a home costs about one object however many files it has. Homes are saved when a run finishes or fails, before a computer stops, and before it sleeps, as before.

Deleting. Deleting a bot with its own computer, or a space (including the personal spaces removed with an account), deletes each home’s key with its computer’s row, so the stored objects become unreadable at once. The worker deletes those objects within the hour, along with any revision an interrupted save left behind.

The cache. DATA_DIR/homes holds a copy of homes in use. A copy whose revision matches the bucket is downloaded again whenever it is missing, so you can remove it. Docker computers work directly in their copy until their next save, so only remove the copy of a Docker computer that is stopped. Remote computers save straight from their files to the bucket; the worker removes their copies after an hour unread. Homes never saved to the bucket keep their only copy.

Saving large homes. A save streams files through DATA_DIR/staging on the data volume, so memory use does not grow with file or home size. A computer with more than 5 GiB or a million files, or more than the volume has free, is not saved and keeps running unsaved. A system note in each bot’s thread asks you to remove large or unneeded files. The worker also logs computer.sleep_deferred with reason too_large or too_many_files. Saves leave out rebuildable caches, dependencies (node_modules and .venv), build output (.next and .turbo), logs and core dump files. These files stay on the running computer but are absent on a fresh restore. Docker, Kubernetes and Railway put tool caches under /tmp/prea-cache, outside the saved home. Staging left by a killed process is removed at startup and by the worker within minutes.

Moving existing homes

After setting the bucket, restart the API and worker. Each home is uploaded the next time it is saved; until then its local copy is used. To upload the rest at once, run this where DATA_DIR is mounted. It skips homes already stored, so it can be stopped and run again. Each upload is downloaded and checked before the home counts as moved. It never deletes local files.

docker compose -f docker-compose.images.yml exec api bun run --filter @orbin/api home-storage migrate --dry-run
docker compose -f docker-compose.images.yml exec api bun run --filter @orbin/api home-storage migrate

Homes listed as having no computer belong to bots that no longer exist; they are left alone. Once a run reports no failures and nothing left to upload, remove the local copies: stop Docker computers, stop the API and worker, move DATA_DIR/homes and DATA_DIR/home-revisions aside, start them again and check a few bots’ files, then delete the moved directories.

Shared storage migration

Push notification registrations now live in Postgres, encrypted with ENCRYPTION_KEY. The same key must be configured on every API and worker replica. Registrations remain bound to their authenticated session; deleting an account also deletes its registration.

With OBJECT_STORAGE_BUCKET configured, new artifact contents use the existing S3-compatible connection alongside computer homes. Artifact contents are always encrypted before upload using the space’s content key and authenticated segments bound to the artifact identity. This applies even with CONTENT_ENCRYPTION=off. Metadata and access checks remain in Postgres; clients and sandboxes receive neither bucket credentials nor public object URLs. Without object storage, artifacts continue using the local volume.

Existing artifact rows continue reading their local files until migrated. Before spreading API or worker replicas across machines, migrate files from every original application volume. Back up the database and volume first. Deploy the schema migration and the new application version, then stop all API and worker processes, including old releases, while running the following commands from a maintenance container with the original volume, DATABASE_URL, ENCRYPTION_KEY, content master keys, and object storage configuration:

# Published image (run from /app/apps/api):
node dist/cli/shared-storage.js artifacts --dry-run
node dist/cli/shared-storage.js artifacts
node dist/cli/shared-storage.js push-tokens --dry-run
node dist/cli/shared-storage.js push-tokens

# Source checkout equivalents:
bun run --filter @orbin/api shared-storage artifacts
bun run --filter @orbin/api shared-storage push-tokens

The artifact migration uploads and decrypts each file to verify it before conditionally updating its database storage key. It keeps the original local files and skips rows already migrated, so interrupted runs can resume. A database failure after upload can leave an unreferenced object; do not delete objects blindly after a failed migration. Keep local originals until the migration is complete and verified. Files absent from the mounted volume are counted as pending while migration continues for files present on that volume. Run the command on each original volume, then run artifacts --dry-run again and require pending: 0 before starting independent replicas. Object-backed artifacts require object storage to remain configured; old application versions cannot read their new keys. The local content-encryption CLI operates on local artifacts only; object artifacts stay encrypted and retain access through the key versions referenced in their headers.

The push import preserves registrations with a live session, skips missing/expired or sessionless registrations, accounts already registered in Postgres, and devices owned by any existing database registration. Import never hands a device back to an older account. It archives processed files with a .migrated suffix. Those archives are never used as a runtime fallback, so they cannot restore a registration after logout. Clients skipped during migration register again when they next open the app. Remove the archived token files after verifying the import. Restart API and worker processes after both commands finish. A schema-only rollback remains compatible with the previous release, but the old code does not read database registrations or object-backed artifacts.

Upload size

MAX_UPLOAD_SIZE_MB sets the per-file chat attachment and computer file transfer limit in MiB (1 MiB = 1,048,576 bytes). It defaults to 10 and must be a positive integer. Set it in .env and restart the API, worker, and Docker supervisor (when used). Web, desktop, and mobile use the limit advertised by the server. If a reverse proxy limits request bodies, allow room for base64 encoding (about 4/3 of the file size) and the JSON envelope.

MAX_ATTACHMENTS_PER_MESSAGE sets the chat attachment count per message. It defaults to 4 and accepts integers from 1 to 100. Set it in .env and restart the API. Web, desktop, and mobile use the advertised count; older clients and clients connected to older servers keep the default of four. Chat bridges retain their existing safe platform limits, and model image budgets still apply.