Server, database roles, and operation
The account and relay service, the PostgreSQL role invariant, the configuration a development server needs, and what to verify beyond HTTP health.
Checked against the source on 17 September 2026
On this page
The server has account and relay responsibilities
The Actix application initializes shared services at startup: database pools, account authentication, connection management, and configured provider and push integrations. The DeviceConnectionManager is shared across HTTP workers in the process. Its live sockets, connection generations, and pending RPC records are process state, and the relay session store behind resume tokens is in memory too, with a 24-hour TTL and a 5-minute sweep. Redis is required for shared sign-in state, rate limiting, and credit reservations for paid requests, and the process exits at startup without it. It does not make a live socket registry portable between server processes.
Both devices and their pending request must reach that connection manager, so production runs one relay-serving process per region with explicit connection draining; the device routing chapter explains why a round-robin replica would break routing.
PostgreSQL roles are a startup invariant
- Runtime loginsDATABASE_SYSTEM_URL · DATABASE_USER_URL
assert_runtime_boundary checks the principals these URLs connect as, not the URLs. Before connecting, create_dual_pools also rejects two identical URLs.
- Tenant contextapp.current_user_id
Set with set_config for one transaction, so a pooled connection forgets it when the transaction ends. Row-level security policies written for each login decide what it may touch.
- PoolsDB_POOL_SYSTEM_MAX · DB_POOL_USER_MAX
Opened once per server process and shared by all its HTTP workers. Each extra process, including the second color during a blue-green cutover, opens its own.
create_dual_pools opens both pools and runs assert_runtime_boundary on the principals they actually connected as, so a URL that names the wrong role fails the same checks.
The pools default to 8 system and 16 tenant connections with minimums of 2 and 4, a 2-second acquire timeout, 60 seconds idle and 1,800 seconds maximum lifetime. Every connection sets UTC, an application_name of plantocode-system or plantocode-user, and statement, idle-in-transaction, and lock timeouts of 30, 60, and 5 seconds for the system role and 8, 10, and 2 seconds for the tenant role; connects retry five times with a linear one-second backoff. Each server process opens its own pools, shared by its HTTP workers, so more processes, including a blue-green cutover, multiply connections and need a database capacity check first.
Configure a local server
- Copy server/.env.example to server/.env and configure a local database, Redis URL, JWT and refresh-token encryption settings, and Auth0 values.
- Create separate migration and runtime principals against a local development database.
- Apply server/scripts/run-migrations.sh with the required migration environment; server/README.md describes role provisioning and the preservation policy.
cp server/.env.example server/.env
# Configure the local services and database roles first.
pnpm server:check
pnpm dev:serverThe migration runner consumes infrastructure/ansible/playbooks/plantocode/migration-execution-plan.yml, the same ordered plan deployment uses. The migration role must be a superuser that is none of postgres, plantocode, or authenticated and is absent from DATABASE_RUNTIME_ROLES. Files marked execution once are sha256-locked in plantocode_internal.migration_execution_ledger, which runtime roles cannot read; a checksum mismatch, a multi-transaction once file, or an unplanned .sql file fails closed, and PRESERVE_EXISTING_DATABASE_IDENTITIES=true is accepted only for the expand phase. Do not replace it with sqlx migrate run.
| Configuration family | Required for |
|---|---|
| DATABASE_SYSTEM_URL, DATABASE_USER_URL, REDIS_URL | Core database and shared sign-in services. |
| AUTH0_DOMAIN, AUTH0_API_AUDIENCE, AUTH0_ALLOWED_CLIENT_IDS | The configured account login trust boundary. |
| JWT and refresh-token encryption settings in .env.example | Issuing app credentials and storing refresh material. |
| Provider API keys | Only the corresponding configured provider routes. |
| Stripe, APNs, FCM, mail and other integration settings | Their respective account or notification features. |
| METRICS_DATABASE_URL, ACCOUNT_CONTROL_DATABASE_URL | Optional, but each pulls in companions: SERVICE_HEALTH_REGION and SERVICE_HEALTH_RELEASE, or WORKSPACE_PREVIEW_HMAC_SECRET. |
| PLANTOCODE_DEPLOYMENT_TOKEN | The protected /health/deployment endpoint and its drain action. |
Verify behavior beyond HTTP health
GET /health checks basic server health. Protected /health/deployment supports deployment inspection. Then verify an account login, two device registrations, one read-only desktop RPC, and one controlled outbox operation. A healthy HTTP listener alone does not test database role separation, a native callback, provider configuration, or relay routing.
A useful relay trace follows the request ID, source client, target desktop, method, and connection generation. The forwarding code extracts W3C trace context and injects the child context into the next hop. For ambiguous delivery, inspect the desktop’s remote_rpc_idempotency claim and its codex_chat_operation_ledger rows before inferring whether work ran.
Production uses the repository’s Ansible blue-green workflow with separate database expansion, binary activation, verification, and contract phases; its inventory and secrets are operator configuration.