Configuration
Preá reads its settings from environment variables, normally kept in the .env file next to
the Compose file.
Where settings live
Each service reads its settings when it starts, so restart the services that read a setting after
changing it. .env.example (source checkout) and .env.images.example (published images) list the
common ones with short notes.
Do not commit .env. Never put COMPOSIO_API_KEY, OpenRouter keys, or provider tokens in git, logs, or chat.
Settings with names you choose
The Compose files pass the API and worker only the settings each one reads, listed by name, rather than the whole .env. A setting whose name you choose, such as TELEGRAM_BOT_TOKEN_<NAME>, therefore needs its own line: add it under api and worker environment in an override file layered after the base file (and in PREA_COMPOSE_FILE when the updater is enabled), with no value so Compose fills it from .env. install-images.sh names any .env setting that no container receives.
# docker-compose.override.yml
services:
api:
environment:
TELEGRAM_BOT_TOKEN_SUPPORT:
TELEGRAM_WEBHOOK_SECRET_TOKEN_SUPPORT:
worker:
environment:
TELEGRAM_BOT_TOKEN_SUPPORT:
TELEGRAM_WEBHOOK_SECRET_TOKEN_SUPPORT:
install-images.sh layers a docker-compose.override.yml next to it automatically. In your own
commands, add -f docker-compose.override.yml after the base file.
Core settings
All of these are optional:
SIGNUPS_ENABLED=true
SIGNUP_ALLOWLIST=you@example.com,@company.com
SANDBOX_PROVIDER=docker # or none, e2b, novita, daytona, createos, boat. Keep fake only for bun run test.
AGENT_RUNTIME=pi # Keep scripted only for bun run test.
WAKEUP_DRIVER=graphile
SANDBOX_IDLE_MS=600000 # pause or stop after 10 minutes idle; 0 disables idle sleep
# Set 0 when self-hosting with Docker, where an idle computer costs nothing, to keep long-running apps and sessions up.
# E2B, CreateOS and Boat still apply their own timeouts.
COMPUTER_MAX_ACTIVE_MS=7200000 # uptime cap for providers that bill while running; minimum 60000; 0 disables
SANDBOX_COMMAND_TIMEOUT_MS=300000 # stop a shell command after 5 minutes
MAX_TOOL_CALLS_PER_TURN= # optional Pi turn tool-call fuse; unset/0 = unlimited
PREA_MESSAGE_USER_MAX_CHARS=500 # message_user length cap; narration stays at 500
BOT_MESSAGE_MAX_HOPS=6 # bot-to-bot messages one chain may carry before the next is refused
E2B_API_KEY= # when SANDBOX_PROVIDER=e2b
NOVITA_API_KEY= # when SANDBOX_PROVIDER=novita; build its template first
DAYTONA_API_KEY= # when SANDBOX_PROVIDER=daytona
CREATEOS_SANDBOX_API_KEY= # when SANDBOX_PROVIDER=createos
BOAT_API_KEY= # when SANDBOX_PROVIDER=boat
COMPUTER_TAKEOVER_TTL_MS (default 900000, 15 minutes) is how long a person’s takeover of a bot’s
screen lasts before control expires.
MODEL_STREAM_MAX_RETRIES (0 to 5, default 1) retries a model stream that fails partway; more
retries let long turns survive brief provider failures.
Feature switches
| Variable | Default | Effect |
|---|---|---|
TICKET_BOARD_ENABLED |
false |
Turns on the ticket board. Set it on the API and worker. |
USER_DEVICES_ENABLED |
on | false stops signed-in desktop apps from letting bots run commands on the user’s own computer. Set it on the API and worker. |
PI_SESSION_RECORDING |
off | true records agent sessions as JSONL under DATA_DIR/pi-sessions. The files are not encrypted, so do not use it on a shared or multi-tenant install. |
PRIVACY_POLICY_URL |
Preá’s policy | The privacy policy linked from the AI data-sharing notice. Set it on the API. |
Database roles
Preá uses two Postgres logins:
- The owner (
POSTGRES_USER, orDATABASE_MIGRATION_URLoutside Compose) runs migrations and owns the tables. - The app login (
prea_appwithPOSTGRES_APP_PASSWORD, orDATABASE_URL) is not a superuser, cannot bypass row-level security, cannot create objects, and can only read and write the app tables.
bun run db:migrate (in containers, its prebuilt form bun run --filter @orbin/api db:deploy, which the
one-shot migrate container runs before the API starts) applies the migrations and the job queue
schema as the owner. Only that container gets the owner login, so code running in the API or
worker cannot find one that bypasses row-level security. It then creates the app login named in DATABASE_URL, or updates its
password, and grants it the prea_app role. When migrations run as a role without
CREATEROLE, as on some managed Postgres, create the login yourself and
GRANT prea_app TO <login>.
Messages, events, threads, memory, artifacts, secrets, model and voice credentials, skills, bots,
runs, tasks, routines, connections, MCP servers and sign-ins, computers, boards and tickets,
messaging identities and outbox, external conversations, steering messages, usage, scratchpad
items and approval rules have row-level security. Each signed-in request sees only its Space’s
rows, plus the user’s own account-level credentials. A query that leaves out its Space filter
returns nothing from other Spaces. A background job that acts for one run, routine, computer,
skill, thread or cloud agent runs as that row’s Space and user, so agent tools get the same
backstop. Security events are written and read only in system scope. Reconcilers, queue sweeps,
the messaging outbox drain and webhooks run in a named system scope (runAsSystem in
packages/db). The job queue’s tables (graphile_worker) hold job metadata, not Space content:
graphile-worker turns on row-level security for them, and bun run db:migrate adds a policy that
gives the app login every row.
With only DATABASE_URL set, migrations and the app share one login. Everything keeps working,
but bun run db:migrate, the API and the worker warn that row-level security is not enforced. To fix
this, set DATABASE_MIGRATION_URL to the owner and DATABASE_URL to a new login.
Run pg_dump as the owner. The app login would only dump rows it is allowed to see.
Database connections
The API keeps a pool of 4 Postgres connections and the worker a pool of 8. DB_POOL_MAX sets both
to the same value, so size it for the worker: Graphile Worker needs at least 6, and the worker also
holds three listening connections and a leadership connection.
REALTIME_DATABASE_URL sends the long-lived LISTEN connections that carry live updates to a
different address than DATABASE_URL, for example a direct connection when DATABASE_URL goes
through a transaction pooler. It defaults to DATABASE_URL.
GRAPHILE_WORKER_CONCURRENCY (default 4) caps concurrent deliveries from the older interactive
job queue during rolling upgrades. New agent runs dispatch straight from Postgres and do not take
those slots.
API and worker memory
Chat runs execute in the API container that receives the message, and routines and other
background runs execute in the worker, so both hold live agent turns. In the production Compose
file, PREA_API_MEMORY_LIMIT and PREA_WORKER_MEMORY_LIMIT cap them (Compose sizes such as
2g or 3072m, default 2g each). Raise them on a larger host. Deploys briefly run the old and
new container of each side by side, so leave room for both.
Listening addresses
The API listens on API_HOST (default 127.0.0.1) and API_PORT (default 3100). Keep loopback
unless a container or a trusted reverse proxy must reach it; the Compose files set this for you.
The published-images stack publishes the web app on 127.0.0.1:5173 and the API on
127.0.0.1:3100; set PREA_WEB_PORT or PREA_API_PORT to use other host ports.
Outbound proxy
To send the API and worker’s outbound traffic through a corporate proxy, set HTTP_PROXY,
HTTPS_PROXY and NO_PROXY (either case works) in .env. List the stack’s own service names in
NO_PROXY so internal traffic skips the proxy:
HTTPS_PROXY=http://proxy.example.com:3128
NO_PROXY=localhost,127.0.0.1,postgres,supervisor,api,worker,web
For mirrors of the installer and images, see restricted networks.