> For the complete documentation index, see [llms.txt](https://mercure-technologies.gitbook.io/xprem/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mercure-technologies.gitbook.io/xprem/references/environment-variables.md).

# Environment variables

The server reads its full configuration from environment variables. Set them in a `.env` file for local development, or in your deployment environment.

### Reading the requirements

* **✅**: always required.
* **with `VARIABLE=value`**: required only for that configuration.
* **Choose exactly one**: set one variable from the listed group. Do not set both.

### Core

| Name                      | Required | Description                                                                                                                                                                                                                                                                                                                       | Example                   |
| ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `BASE_URL`                | ❌        | Public root URL of the server, optionally with a path prefix (`https://api.example.com/ota`). Manifest and asset URLs are built from it. Defaults to `http://localhost:3000`, so set it in any real deployment.                                                                                                                   | `https://ota.example.com` |
| `SERVE_FROM_SUB_PATH`     | ❌        | `true` makes the server serve every route under `BASE_URL`'s path prefix, for gateways that forward paths untouched. Leave unset when the proxy strips the prefix before forwarding. Routing only: emitted links always carry `BASE_URL`'s path. See [Serve under a path prefix](/xprem/deployment/serve-under-a-path-prefix.md). | `true`                    |
| `JWT_SECRET`              | ✅        | Secret used to sign dashboard sessions and upload tokens.                                                                                                                                                                                                                                                                         | Random string             |
| `PORT`                    | ❌        | Listening port. Defaults to `3000`.                                                                                                                                                                                                                                                                                               | `3000`                    |
| `BIND_TO_ADDRESS`         | ❌        | Address the server listens on. Defaults to `0.0.0.0`, every interface. Set `127.0.0.1` to keep the server on the loopback interface behind a reverse proxy on the same host. An invalid address prevents the server from starting.                                                                                                | `127.0.0.1`               |
| `DASHBOARD_ROOT_REDIRECT` | ❌        | Redirect "/" -> "/dashboard"                                                                                                                                                                                                                                                                                                      | true (default false)      |

### Database

Apps, branches, channels and update metadata live in PostgreSQL.

<table><thead><tr><th>Configuration</th><th width="208.10546875">Value</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Database</td><td><code>DB_URL</code></td><td>✅</td><td>PostgreSQL connection string. Schema migrations run on boot.</td></tr><tr><td>Master key source</td><td><strong>Set exactly one:</strong><br><code>DB_KEYS_MASTER_KEY_B64</code><br><code>AWSSM_DB_KEYS_MASTER_KEY_SECRET_ID</code></td><td>✅</td><td>Use a base64-encoded 32-byte master key, or an AWS Secrets Manager secret containing it. Generate a value with <code>openssl rand -base64 32</code>.</td></tr><tr><td>Connection pool</td><td><code>DB_MAX_CONNS</code>, <code>DB_MIN_CONNS</code></td><td>❌</td><td>Connection pool bounds. Default to <code>DB_MAX_CONNS=25</code> and <code>DB_MIN_CONNS=5</code>.</td></tr><tr><td>Connection lifetime</td><td><code>DB_MAX_CONN_LIFETIME</code>, <code>DB_MAX_CONN_IDLE_TIME</code></td><td>❌</td><td>Connection lifetime limits. Default to <code>DB_MAX_CONN_LIFETIME=30m</code> and <code>DB_MAX_CONN_IDLE_TIME=5m</code>.</td></tr></tbody></table>

{% hint style="warning" %}
Setting both master-key variables, or neither, prevents the server from starting.

The master key is not recoverable. Back it up before the first deployment.
{% endhint %}

Each app's signing keys are managed per app at creation time, see [Keys](/xprem/key-store/keys.md). `KEYS_STORAGE_TYPE` only selects the CloudFront private key source, see the [CDN](#cdn) section.

### Cache

See [Caching](/xprem/caching/overview.md) for details.

| Name                         | Required                         | Description                                                                   |
| ---------------------------- | -------------------------------- | ----------------------------------------------------------------------------- |
| `CACHE_MODE`                 | ❌                                | `redis`, `redis-sentinel`, or any other value for in-memory caching.          |
| `REDIS_HOST`, `REDIS_PORT`   | with `CACHE_MODE=redis`          | Redis endpoint.                                                               |
| `REDIS_PASSWORD`             | ❌                                | Redis password.                                                               |
| `REDIS_USERNAME`             | ❌                                | Redis username for ACL authentication.                                        |
| `REDIS_USE_TLS`              | ❌                                | `true` enables TLS.                                                           |
| `REDIS_CA_CERT_B64`          | ❌                                | Base64-encoded CA certificate for TLS with a custom CA.                       |
| `REDIS_SENTINEL_ADDRS`       | with `CACHE_MODE=redis-sentinel` | Comma-separated Sentinel addresses, e.g. `sentinel-0:26379,sentinel-1:26379`. |
| `REDIS_SENTINEL_MASTER_NAME` | ❌ (defaults to `mymaster`)       | Sentinel master name.                                                         |
| `CACHE_KEY_PREFIX`           | ❌ (defaults to `expoopenota`)    | Key namespace when several deployments share one Redis.                       |

### Storage

See [Storage](/xprem/storage/overview.md) for details.

| Name                                 | Required                  | Description                                                                                                                           |
| ------------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `STORAGE_MODE`                       | ❌ (defaults to `local`)   | `local`, `s3`, `gcs`, or `azure`.                                                                                                     |
| `S3_BUCKET_NAME`                     | with `STORAGE_MODE=s3`    | S3 bucket name.                                                                                                                       |
| `GCS_BUCKET_NAME`                    | with `STORAGE_MODE=gcs`   | GCS bucket name.                                                                                                                      |
| `GOOGLE_APPLICATION_CREDENTIALS_B64` | with `STORAGE_MODE=gcs`   | Base64-encoded service account JSON, used for signed URLs.                                                                            |
| `AZURE_BLOB_CONTAINER_NAME`          | with `STORAGE_MODE=azure` | Blob container name.                                                                                                                  |
| `AZURE_STORAGE_ACCOUNT_NAME`         | with `STORAGE_MODE=azure` | Storage account name.                                                                                                                 |
| `AZURE_STORAGE_ACCOUNT_KEY`          | with `STORAGE_MODE=azure` | Account access key. Also signs the SAS URLs used for uploads and asset delivery.                                                      |
| `AZURE_BLOB_ENDPOINT`                | ❌                         | Custom blob service URL (Azurite, private endpoints).                                                                                 |
| `LOCAL_BUCKET_BASE_PATH`             | with `STORAGE_MODE=local` | Directory where updates are stored. Defaults to `./updates`.                                                                          |
| `BUCKET_KEY_PREFIX`                  | ❌                         | Key prefix namespacing all objects inside a shared bucket. Applies to `s3`, `gcs` and `azure`.                                        |
| `S3_KEY_PREFIX`                      | ❌                         | **Deprecated.** Use `BUCKET_KEY_PREFIX` instead. Kept for backward compatibility.                                                     |
| `AWS_BASE_ENDPOINT`                  | ❌                         | Custom S3-compatible endpoint (Cloudflare R2, MinIO, Railway…).                                                                       |
| `AWS_S3_FORCE_PATH_STYLE`            | ❌                         | `true` forces path-style S3 addressing for providers that require it.                                                                 |
| `DISABLE_S3_DIRECT_CDN`              | ❌                         | `true` streams S3 objects through the server instead of redirecting clients to pre-signed URLs. For providers that cannot issue them. |

### AWS credentials

Static credentials are optional. When absent, the AWS SDK uses the runtime credential chain. This supports IAM roles attached to EC2 instances, ECS tasks, or EKS service accounts.

| Name                                         | Required                                                          | Description                                                                                 |
| -------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `AWS_REGION`                                 | with `STORAGE_MODE=s3` or `KEYS_STORAGE_TYPE=aws-secrets-manager` | AWS region. Defaults to `eu-west-3` when unset, a surprising default, so set it explicitly. |
| `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` | ❌                                                                 | Static credentials. Omit both to use the runtime IAM role.                                  |

### CDN

See [CDN](/xprem/cdn/overview.md) for details.

<table><thead><tr><th>Configuration</th><th width="230.85546875">Value</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>CloudFront distribution</td><td><code>CLOUDFRONT_DOMAIN</code></td><td>❌</td><td>Distribution domain. Enables CloudFront delivery.</td></tr><tr><td>Public key ID</td><td><code>CLOUDFRONT_KEY_PAIR_ID</code></td><td>with <code>CLOUDFRONT_DOMAIN</code> set</td><td>CloudFront public key ID.</td></tr><tr><td>Private key source</td><td><strong>Set exactly one:</strong><br><code>AWSSM_CLOUDFRONT_PRIVATE_KEY_SECRET_ID</code><br><code>PRIVATE_CLOUDFRONT_KEY_B64</code><br><code>PRIVATE_CLOUDFRONT_KEY_PATH</code></td><td>with <code>CLOUDFRONT_DOMAIN</code> set</td><td>Provide the private key through AWS Secrets Manager, as base64, or from a local PEM file. When <code>KEYS_STORAGE_TYPE</code> is set, only the source matching it is used; when unset, sources are tried in the listed order (Secrets Manager → base64 → file path).</td></tr><tr><td>Generic CDN</td><td><code>CDN_BASE_URL</code></td><td>❌</td><td><a href="/xprem/cdn/generic-cdn.md">Generic CDN</a> base URL in front of a public S3, GCS or Azure bucket. Takes precedence over GCS and Azure signed URL delivery.</td></tr><tr><td>Generic CDN (legacy)</td><td><code>S3_CDN_PREFIX</code></td><td>❌</td><td><strong>Deprecated.</strong> Use <code>CDN_BASE_URL</code> instead. Kept for backward compatibility.</td></tr></tbody></table>

### Bundle diffing

See [Bundle diffing](/xprem/features/bundle-diffing-bsdiff.md) for details. All four need the control plane.

| Name                                | Required                | Description                                                                                                                                                                     |
| ----------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BUNDLE_DIFFING`                    | ❌ (defaults to `false`) | `true` computes a bsdiff patch from each earlier update to every new one, and serves it to devices that accept patches.                                                         |
| `BUNDLE_DIFFING_CDN_REDIRECT`       | ❌ (defaults to `false`) | `true` redirects patch requests to the CDN like any other asset. Only with a CDN configured to add the `im` and `expo-base-update-id` response headers; otherwise updates fail. |
| `BUNDLE_DIFFING_MAX_BUNDLE_SIZE_MB` | ❌ (defaults to `64`)    | Bundles above this size get no patch. A patch job peaks at about six times the bundle size, and two run at once.                                                                |
| `BUNDLE_DIFFING_PATCH_MAX_RATIO`    | ❌ (defaults to `0.3`)   | A patch is stored only if it is at most this fraction of the gzipped full bundle. A number between 0 and 1.                                                                     |

### Dashboard

| Name             | Required          | Description                                                                                                                                                                                            |
| ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `USE_DASHBOARD`  | ❌                 | `true` enables the dashboard.                                                                                                                                                                          |
| `ADMIN_EMAIL`    | on the first boot | Read once, at the first boot, to seed the first admin account (see [Users](/xprem/dashboard/users.md)).                                                                                                |
| `ADMIN_PASSWORD` | on the first boot | The first admin's password. It must meet the dashboard password policy (at least 8 characters, with an uppercase letter, a lowercase letter, a digit and a special character) or the first boot fails. |

Both variables are required on the very first boot, whatever `USE_DASHBOARD` is set to. The migration that seeds the first admin account runs with the rest of the schema migrations, and it aborts the boot when either variable is missing or when the password does not satisfy the policy. Once that first boot has succeeded the pair is never read again, so you can drop both from your deployment.

### Observe and device telemetry

See [Get started with Observe](/xprem/observe/get-started-with-observe.md) and [Identity](/xprem/identity/overview.md) for details.

| Name                                   | Required                                              | Description                                                                                                                                                                                                                           |
| -------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CLICKHOUSE_URL`                       | with Observe                                          | ClickHouse connection string, `clickhouse://user:password@host:9000/xprem`. The server runs its own ClickHouse migrations at startup. Without it, metric batches are acknowledged and dropped, and the Observe dashboards stay empty. |
| `DISABLE_DEVICE_TELEMETRY`             | ❌                                                     | `true` stops every recording about devices: the check-ins that feed the device registry and update health, the telemetry the Observe SDK sends, and the ClickHouse connection. Data recorded before the switch is kept.               |
| `UPLOAD_SOURCEMAPS`                    | ❌                                                     | Enables source-map uploads and storage in control-plane mode.                                                                                                                                                                         |
| `LOCAL_SOURCEMAPS_BASE_PATH`           | If bucket mode = local & upload sourcemaps is enabled | Directory for source maps and indexes when using local storage; `/app/sourcemaps` inside Docker.                                                                                                                                      |
| `S3_BUCKET_SOURCEMAPS_NAME`            | If bucket mode = s3 & upload sourcemaps is enabled    | Private bucket required when source-map uploads are enabled with S3 storage.                                                                                                                                                          |
| `GCS_BUCKET_SOURCEMAPS_NAME`           | If bucket mode = gcs & upload sourcemaps is enabled   | Private bucket required when source-map uploads are enabled with GCS storage.                                                                                                                                                         |
| `AZURE_BLOB_SOURCEMAPS_CONTAINER_NAME` | If bucket mode = azure & upload sourcemaps is enabled | Private container required when source-map uploads are enabled with Azure storage.                                                                                                                                                    |
| `OBSERVE_INGEST_LIMIT_PER_IP`          | ❌ (Default 120)                                       | Maximum HTTP batches per minute per IP address **and application**. Set to `0` to disable this limit.                                                                                                                                 |
| `OBSERVE_INGEST_LIMIT_PER_APP`         | ❌ (Default 6000)                                      | Maximum HTTP batches per minute per application, across all IP addresses. Set to `0` to disable this limit.                                                                                                                           |

### Geolocation

See [Geolocation](/xprem/identity/geolocation.md) for details. Pick one strategy; when both are configured, the headers win.

| Name                                                                                           | Required | Description                                                                                                                                                                                 |
| ---------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRUST_GEOIP_HEADERS`                                                                          | ❌        | `true` reads the visitor-location headers added by your CDN or proxy (Cloudflare, CloudFront, Vercel, generic `X-Geo-*`, Akamai). Only when the server is reachable exclusively through it. |
| `GEOIP_HEADER_COUNTRY`, `GEOIP_HEADER_CITY`, `GEOIP_HEADER_LATITUDE`, `GEOIP_HEADER_LONGITUDE` | ❌        | Header names to read instead of the built-in catalog, for a proxy that uses its own. The four replace the catalog together.                                                                 |
| `MAXMIND_ACCOUNT_ID`, `MAXMIND_LICENSE_KEY`                                                    | together | MaxMind credentials. The server downloads the free GeoLite2 City database at startup and resolves client IPs locally. Setting only one of the two prevents the server from starting.        |
| `GEOIP_CACHE_DIR`                                                                              | ❌        | Where the downloaded database is cached. Defaults to the user cache directory; point it at a writable volume when the container filesystem is read-only.                                    |

### Metrics

| Name                 | Required | Description                                      |
| -------------------- | -------- | ------------------------------------------------ |
| `PROMETHEUS_ENABLED` | ❌        | `true` exposes Prometheus metrics on `/metrics`. |

### Telemetry

| Name                | Required | Description                                                                                  |
| ------------------- | -------- | -------------------------------------------------------------------------------------------- |
| `DISABLE_TELEMETRY` | ❌        | `true` disables the hourly usage ping (instance id, base URL, version, configuration shape). |

### Migration

| Name                           | Required             | Description                                                                                                                                                                                                                                         |
| ------------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SKIP_LEGACY_APP_ID_FALLBACK`  | ❌                    | `true` stops resolving manifest/asset requests without an `expo-app-id` header to the legacy `EXPO_APP_ID` app imported from a v2 deployment. Those requests then fail with `400`. Leave unset while clients built before v2 are still in the wild. |
| `BUCKET_MIGRATION_CONCURRENCY` | ❌ (defaults to `16`) | How many objects the v2 to v3 bucket re-path moves in parallel. See [Migrate from v2 to v3](/xprem/changelog-and-migrations/migrate-from-v2-to-v3.md).                                                                                              |

### Enterprise features

| Name                                   | Required                                                | Description                                                                                                                                                                                                                                     |
| -------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRUST_PROXY_HEADERS`                  | ❌                                                       | Used for [Token access](/xprem/security/token-access.md) IP allowlists. Only enable this when the server is reachable exclusively through a trusted proxy, since a client that can reach the server directly could otherwise forge that header. |
| `TRUST_PROXY_DEPTH`                    |                                                         | Used for [Token access](/xprem/security/token-access.md) IP allowlists. Tells the server how many trusted proxies sit in front of it.                                                                                                           |
| `AUDIT_LOG_RETENTION_DAYS`             | ❌ (defaults to `550`)                                   | How many days [audit log](/xprem/security/audit-log.md) entries stay in the database before the daily purge removes them.                                                                                                                       |
| `ARCHIVE_AUDIT_LOGS`                   | ❌                                                       | `true` enables the periodic NDJSON export of the [audit log](/xprem/security/audit-log.md) to a dedicated bucket.                                                                                                                               |
| `AUDIT_LOGS_EXPORT_INTERVAL_SECONDS`   | ❌ (defaults to `300`)                                   | How often the audit archive exporter runs, in seconds.                                                                                                                                                                                          |
| `S3_BUCKET_AUDIT_LOGS_NAME`            | with `ARCHIVE_AUDIT_LOGS=true` and `STORAGE_MODE=s3`    | Dedicated S3 bucket receiving the audit archive. Must differ from `S3_BUCKET_NAME`.                                                                                                                                                             |
| `GCS_BUCKET_AUDIT_LOGS_NAME`           | with `ARCHIVE_AUDIT_LOGS=true` and `STORAGE_MODE=gcs`   | Dedicated GCS bucket receiving the audit archive. Must differ from `GCS_BUCKET_NAME`.                                                                                                                                                           |
| `AZURE_BLOB_AUDIT_LOGS_CONTAINER_NAME` | with `ARCHIVE_AUDIT_LOGS=true` and `STORAGE_MODE=azure` | Dedicated blob container receiving the audit archive. Must differ from `AZURE_BLOB_CONTAINER_NAME`.                                                                                                                                             |
| `LOCAL_AUDIT_LOGS_BASE_PATH`           | ❌ (defaults to `./audit-logs`)                          | Directory receiving the audit archive with `STORAGE_MODE=local`. Must differ from `LOCAL_BUCKET_BASE_PATH`.                                                                                                                                     |
