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 5 October 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. Every RPC and every heartbeat frame renews a session record. 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.
| Default | System pool | Tenant pool |
|---|---|---|
| Maximum connections | 8 | 16 |
| Minimum connections | 2 | 4 |
| application_ | hbcode-system | hbcode-user |
| Statement timeout | 30 seconds | 8 seconds |
| Idle-in-transaction timeout | 60 seconds | 10 seconds |
| Lock timeout | 5 seconds | 2 seconds |
Both pools use a 2-second acquire timeout, a 60-second idle timeout, and a maximum connection lifetime of 1,800 seconds. Every connection sets the time zone to UTC. A failed connect is tried up to five times, and the wait grows by one second with each attempt. Each server process opens its own pools, shared by its HTTP workers. More processes, including the second color of a blue-green cutover, multiply the 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, hbcode, or authenticated and is absent from DATABASE_RUNTIME_ROLES. Files marked execution once are sha256-locked in plantocode_internal.migration_execution_ledger, and the runner stops when a runtime role can reach that ledger. A checksum mismatch, a once file without a single transaction, or an unplanned .sql file stops the run. PRESERVE_EXISTING_DATABASE_IDENTITIES=true is accepted only for the expand phase. Do not replace the runner with sqlx migrate run.
| Purpose | Settings and when they are required |
|---|---|
| Core services | DATABASE_ |
| Account login | AUTH0_ |
| Tokens | JWT_ |
| Providers | Provider API keys. Only the corresponding configured provider routes. |
| Integrations | Stripe, APNs, FCM, mail and other integration settings. Their respective account or notification features. |
| Extra databases | METRICS_ |
| Service health | SERVICE_ |
| Previews | WORKSPACE_ |
| Deployment | PLANTOCODE_ |
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 chat_operation_ledger rows before inferring whether work ran.
- Backup/usr/local/bin/backup_postgresql.sh
Runs first on each host. Its verified output is the only way back after a forward migration.
- Expandsite-app.yml --tags api-expand
Applies the backward-compatible part of the migration plan. The binary that still runs keeps working on the expanded schema.
- Binary and soaksite-app.yml --tags api-binary-deploy
Activates the new binary with the blue-green services. The operator checks health and logs and waits for the soak to end.
- Migratesite-app.yml --tags api-migrate
Commits both release colors and applies the contract part of the plan. There is no generic rollback action after this step.
- One host per run--limit interserver-us · --limit hetzner-primary
Each action that changes production targets exactly one inventory host. The EU sequence starts only after the canary result is reviewed.
Production has two hosts in the Ansible inventory: interserver-us in the US and hetzner-primary in the EU. Each runs the server as two services, plantocode-blue on port 8080 and plantocode-green on port 8081. Every action that changes production runs through site-app.yml with one explicit --limit host. infrastructure/ansible/operations.md is the runbook. Its inventory and secrets are operator configuration.
| Step on one host | Action and result |
|---|---|
| Backup | backup_ |
| Expand | --tags api-expand. The backward-compatible phase of the migration plan is applied and attested. |
| Binary | --tags api-binary-deploy. The new binary is active. The operator checks health and logs and completes the soak. |
| Migrate | --tags api-migrate. Both release colors are committed and the contract phase is applied. |
The runbook forbids three shortcuts. Never copy a release SQL file to a server and run psql by hand. Never run an application or database change without one explicit --limit. FLUSHALL on Redis is not a routine recovery action and needs explicit operator approval.