You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit cf11071
Browse filesBrowse the repository at this point in the historyBrowse files
|`ENCRYPTION_KEY`| Encryption key, exactly 64 hex characters (32 bytes): `openssl rand -hex 32`. A key in any other format leaves [account token sealing](/platform/self-hosting/security#account-token-sealing) off|
20
20
|`INTERNAL_API_SECRET`| Internal API secret, at least 32 characters: `openssl rand -hex 32`|
21
21
|`NEXT_PUBLIC_APP_URL`| Public app URL |
22
22
23
23
`openssl rand -hex 32` prints 64 hex characters. Encryption keys require that format. The app skips its env schema validation; individual consumers can reject invalid values when first used. Realtime validates `BETTER_AUTH_SECRET` and `INTERNAL_API_SECRET` at boot and rejects values shorter than 32 characters.
24
24
25
25
<Callouttype="error">
26
-
Retain `ENCRYPTION_KEY` and `API_ENCRYPTION_KEY` across upgrades and restores. There is no supported in-place key rotation procedure; losing a key makes values encrypted with it unreadable — workspace and personal environment variables, stored provider API keys, MCP OAuth credentials, and deployment/chat secrets in the first case, user-generated Sim API keys in the second. Back them up separately from the database.
26
+
Retain `ENCRYPTION_KEY` and `API_ENCRYPTION_KEY` across upgrades and restores. There is no supported in-place key rotation procedure; losing a key makes values encrypted with it unreadable — workspace and personal environment variables, stored provider API keys, MCP OAuth credentials, connected-account OAuth tokens, and deployment/chat secrets in the first case, user-generated Sim API keys in the second. Back them up separately from the database.
27
27
</Callout>
28
28
29
29
## Feature prerequisites and coordination [#strongly-recommended]
@@ -39,6 +39,7 @@ Production Compose derives `DATABASE_URL` and `BETTER_AUTH_URL` on its service d
39
39
|`AUTH_TRUSTED_PROXIES`| Comma-separated reverse-proxy IPs/CIDRs so the client IP cannot be forged through `X-Forwarded-For`|
40
40
|`INTERNAL_API_BASE_URL`| Internal URL for server-side self-calls, e.g. `http://sim-app.simstudio.svc.cluster.local:3000`. Defaults to the public base URL. The Trigger.dev worker ignores it and uses the public app URL. |
41
41
|`SIM_MCP_URL`| Public URL of the [Sim MCP server](/mcp/overview) when you serve it on its own host, e.g. `https://mcp.example.com/mcp`. Point that host at the app; Sim serves only the MCP endpoint and its OAuth metadata there, and stops serving `/api/mcp` on the app host so clients use one URL. Optional — defaults to `<NEXT_PUBLIC_APP_URL>/api/mcp`|
42
+
|`ENCRYPT_ACCOUNT_TOKENS`| Set to `false` to stop sealing connected-account OAuth tokens on write and during `migrate`. Sealing is on by default with a 64-hex `ENCRYPTION_KEY`; see [Account token sealing](/platform/self-hosting/security#account-token-sealing)|
42
43
|`DATABASE_REPLICA_URL`| Read-replica connection string for log listing, audit logs, and dashboard aggregations. Falls back to the primary when unset |
|`API_ENCRYPTION_KEY`| Encrypted stored API-key material when configured | Rotation needs re-encryption; existing hash-based authentication continues, but old encrypted material cannot be decrypted with a new key |
46
46
|`INTERNAL_API_SECRET`| Service-to-service calls | Yes — roll app and realtime together |
47
47
|`CRON_SECRET`| Background job endpoints | Yes — roll app and cron together |
@@ -52,6 +52,19 @@ Five secrets drive the security of a deployment. Generate each with `openssl ran
52
52
53
53
`BETTER_AUTH_SECRET` must be **identical** on the app and realtime services — they share sessions through the database, and a mismatch means realtime rejects every authenticated socket.
54
54
55
+
### Account token sealing
56
+
57
+
Sim seals the OAuth access and refresh tokens of connected accounts at rest with `ENCRYPTION_KEY`. Each token is encrypted and bound to its column, so a value copied from the database is unusable without the key.
58
+
59
+
-**On by default** whenever `ENCRYPTION_KEY` is exactly 64 hex characters (`openssl rand -hex 32`). Each process logs `Account token sealing: enabled` with a key id at startup, never the key itself.
60
+
-**Without a usable key**, tokens are stored in plain text and the app keeps working. Each process logs a single warning at startup: `Account token sealing: disabled (ENCRYPTION_KEY is not a 64-character hex key)`. Other features that use `ENCRYPTION_KEY` keep their current behavior.
61
+
-**Tokens stored earlier** are sealed by the migrations container during `migrate`, in small batches. The step needs `ENCRYPTION_KEY` on the migrations container: the Helm chart and the Compose files pass the app's value. Until the key is usable, the step does nothing and stays pending, so it runs on a later `migrate` once sealing is on. It also waits for a later `migrate` when it can't finish because rows are busy, rather than failing the upgrade.
62
+
-**Opt out** with `ENCRYPT_ACCOUNT_TOKENS=false` on the app, background workers, and migrations container. New writes are then stored in plain text, and tokens that are already sealed stay readable.
63
+
64
+
<Callouttype="error">
65
+
Once tokens are sealed, do not roll back to a release older than v0.9.33. Those releases cannot read sealed tokens, so every connected account would have to be reconnected. Opting out does not unseal stored tokens.
Copy file name to clipboardExpand all lines: apps/docs/content/docs/platform/self-hosting/upgrades.mdx
+8-1Lines changed: 8 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -227,6 +227,8 @@ PROFILE=gpu # or cpu
227
227
docker compose -f docker-compose.ollama.yml --profile "$PROFILE" up -d --build
228
228
```
229
229
230
+
Never roll back below v0.9.33 once account tokens are sealed: those releases cannot read them, and every connected account would have to be reconnected. See [Account token sealing](/platform/self-hosting/security#account-token-sealing).
231
+
230
232
**Rollback after an incompatible schema change** requires restoring the pre-upgrade database backup. This loses writes made since the snapshot. Stop all writers before restoring:
231
233
232
234
1. Stop traffic and all processes that can write to the database, including the app, realtime service, scheduled jobs, and any background workers.
@@ -270,6 +272,10 @@ Migrations are Drizzle SQL files plus registered script (data) migrations, appli
270
272
271
273
Migrations are **forward-only**. Take the backup described in the upgrade procedure before applying them; there are no down-migrations.
272
274
275
+
<Callouttype="warn">
276
+
Upgrading from a release older than v0.9.33 with a rolling deploy leaves old pods running next to new ones for a while. Old pods cannot read sealed account tokens, so integrations can fail on them until the rollout finishes. The `migrate` backfill waits an hour after such an upgrade before it seals stored tokens, but tokens the new pods write are sealed straight away. To avoid that window, upgrade to v0.9.33 first, stop the old pods before starting the new ones, or set `ENCRYPT_ACCOUNT_TOKENS=false` for the upgrade and remove it once every pod runs the new release.
277
+
</Callout>
278
+
273
279
Migrations apply in sequence, so skipping several releases at once generally works mechanically. It is riskier — those combinations get less testing, and you take several releases' worth of behavior changes in one step. Upgrade in smaller increments where you can.
274
280
275
281
### How concurrent replicas are handled
@@ -289,6 +295,7 @@ These control the migrator and are set on the `migrations` container, not the ap
289
295
| Variable | Purpose |
290
296
|---|---|
291
297
|`MIGRATION_DATABASE_URL`| A **direct, non-pooled** database connection URL (DSN). Falls back to `DATABASE_URL`. Setting it also arms the backend-pid check that aborts the run if the session — and with it the advisory lock — is silently recycled mid-migration; with only `DATABASE_URL` set, that check is skipped |
298
+
|`ENCRYPTION_KEY`, `ENCRYPT_ACCOUNT_TOKENS`| Let the migrator seal account tokens stored before [account token sealing](/platform/self-hosting/security#account-token-sealing) was on. The Helm init container reads both from the app Secret, and the Compose `migrations` services pass the `.env` values. Without a usable key the step stays pending and does nothing |
292
299
293
300
<Callouttype="warn">
294
301
Set `MIGRATION_DATABASE_URL` if anything sits between Sim and Postgres. Session advisory locks and session-level `SET`s do not survive PgBouncer transaction pooling — the lock silently does not hold, and consecutive statements can land on different backends. Point it at the database directly, bypassing the pooler.
@@ -299,7 +306,7 @@ Supply the migration URL separately from the app URL:
299
306
300
307
| Deployment | How to pass `MIGRATION_DATABASE_URL`|
301
308
|---|---|
302
-
| Helm | The init container reads only the database Secret. Include the key in a Secret supplied through `postgresql.auth.existingSecret` or `externalDatabase.existingSecret`; `app.env` does not reach it. The chart-managed database Secret carries only the password, and there is no dedicated values key for the migration URL |
309
+
| Helm | The init container reads the database Secret, plus only `ENCRYPTION_KEY` and `ENCRYPT_ACCOUNT_TOKENS` from the app Secret. Include the key in a Secret supplied through `postgresql.auth.existingSecret` or `externalDatabase.existingSecret`; `app.env` does not reach it. The chart-managed database Secret carries only the password, and there is no dedicated values key for the migration URL |
303
310
| Compose | Add the variable to the `migrations` service's explicit `environment:` list. Setting it in `.env` alone does not pass it to that service |
304
311
| Separate migration run | Run the migration image yourself with a direct DSN before upgrading |
# DAYTONA_SHELL_SNAPSHOT_ID= # Mothership shell snapshot ref, required when Mothership runs code on Daytona
56
56
57
57
# Security (Required)
58
-
ENCRYPTION_KEY=your_encryption_key# Use `openssl rand -hex 32` to generate, used to encrypt environment variables
58
+
ENCRYPTION_KEY=your_encryption_key# Use `openssl rand -hex 32` to generate, used to encrypt environment variables and connected-account OAuth tokens (ENCRYPT_ACCOUNT_TOKENS=false opts the tokens out)
59
59
INTERNAL_API_SECRET=your_internal_api_secret# Use `openssl rand -hex 32` to generate, used to encrypt internal api routes
60
60
API_ENCRYPTION_KEY=your_api_encryption_key# Use `openssl rand -hex 32` to generate, used to encrypt api keys
61
61
CRON_SECRET=your_cron_secret# Use `openssl rand -hex 32` to generate. Authenticates the scheduler against the background job endpoints (scheduled workflows, polling triggers, connector syncs)
TURNSTILE_SECRET_KEY: z.string().min(1).optional(),// Cloudflare Turnstile secret key for captcha verification
145
145
ENCRYPTION_KEY: z.string().min(32),// Key for encrypting sensitive data
146
146
API_ENCRYPTION_KEY: z.string().min(32).optional(),// Dedicated key for encrypting API keys (optional for OSS)
147
-
ENCRYPT_ACCOUNT_TOKENS: z.boolean().optional(),// Seal OAuth access/refresh tokens on write (read by @sim/db); takes effect only with a 64-hex ENCRYPTION_KEY. Enable only once every app, worker, and background-task deployment runs a release that reads sealed tokens
147
+
ENCRYPT_ACCOUNT_TOKENS: z.boolean().optional(),// Opt-out: OAuth access/refresh tokens are sealed on write (read by @sim/db) and backfilled by migrate whenever ENCRYPTION_KEY is 64 hex characters; false or 0 writes plaintext. Sealed tokens need v0.9.33+ on every process that reads them
148
148
INTERNAL_API_SECRET: z.string().min(32),// Secret for internal API authentication
149
149
INTERNAL_JWT_SECRET: z.string().min(32).optional(),// Dedicated signing key for internal JWTs (falls back to INTERNAL_API_SECRET); separating limits blast radius if one leaks
0 commit comments