> For the complete documentation index, see [llms.txt](https://mercure-technologies.gitbook.io/expo-open-ota/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/expo-open-ota/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 for the selected server mode.
* **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. Manifest and asset URLs are built from it. Defaults to `http://localhost:3000` — set it in any real deployment. | `https://ota.example.com` |
| `JWT_SECRET` | ✅        | Secret used to sign dashboard sessions and upload tokens.                                                                                      | Random string             |
| `PORT`       | ❌        | Listening port. Defaults to `3000`.                                                                                                            | `3000`                    |

### App configuration

One server hosts one app (stateless mode) or many apps ([Control Plane mode](/expo-open-ota/controle-plane-mode/overview.md)). The server picks the mode from whether `DB_URL` is set.

#### Stateless mode (single app)

| Name                                                                  | Required                                     | Description                                                                                                        |
| --------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `EXPO_APP_ID`                                                         | ✅                                            | Expo project ID (EAS `projectId`) that clients send as the `expo-app-id` header.                                   |
| `EXPO_ACCESS_TOKEN`                                                   | ✅                                            | Expo access token scoped to that project.                                                                          |
| `KEYS_STORAGE_TYPE`                                                   | ❌ (defaults to `local`)                      | Signing key store: `local`, `environment`, or `aws-secrets-manager`. See [Keys](/expo-open-ota/key-store/keys.md). |
| `PUBLIC_LOCAL_EXPO_KEY_PATH`, `PRIVATE_LOCAL_EXPO_KEY_PATH`           | with `KEYS_STORAGE_TYPE=local`               | Paths to the signing key PEM files.                                                                                |
| `PUBLIC_EXPO_KEY_B64`, `PRIVATE_EXPO_KEY_B64`                         | with `KEYS_STORAGE_TYPE=environment`         | Base64-encoded signing keys (PEM).                                                                                 |
| `AWSSM_EXPO_PUBLIC_KEY_SECRET_ID`, `AWSSM_EXPO_PRIVATE_KEY_SECRET_ID` | with `KEYS_STORAGE_TYPE=aws-secrets-manager` | AWS Secrets Manager secret IDs.                                                                                    |

#### Control plane mode (multi-app)

<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. Its presence enables control-plane mode. 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 %}

Stateless key variables (`KEYS_STORAGE_TYPE`, `EXPO_APP_ID`, `EXPO_ACCESS_TOKEN`, `*_EXPO_KEY_*`) are ignored for app signing keys in control-plane mode. `KEYS_STORAGE_TYPE` also selects the CloudFront private key source in every mode — see the [CDN](#cdn) section.

### Cache

See [Caching](/expo-open-ota/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](/expo-open-ota/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`. 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.                          |

### 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](/expo-open-ota/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="/pages/n9xmpNPWPqtNSVHHApC2">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>

### Dashboard

| Name             | Required                  | Description                                                                                                                                                                                                                                                                                                                                                                               |
| ---------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `USE_DASHBOARD`  | ❌                         | `true` enables the dashboard.                                                                                                                                                                                                                                                                                                                                                             |
| `ADMIN_EMAIL`    | with `USE_DASHBOARD=true` | Dashboard sign-in email. On the control plane it is read once, at the first boot, to seed the first admin account — see [Users](/expo-open-ota/dashboard/users.md).                                                                                                                                                                                                                       |
| `ADMIN_PASSWORD` | with `USE_DASHBOARD=true` | Dashboard sign-in password. Paired with `ADMIN_EMAIL`: the login credentials in stateless mode, the first-admin seed on the control plane (removable once the first boot has completed). The seed 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 control-plane boot fails. |

### Metrics

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

### Migration

| Name                          | Required | Description                                                                                                                                                                                            |
| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `SKIP_LEGACY_APP_ID_FALLBACK` | ❌        | `true` stops resolving manifest/asset requests without an `expo-app-id` header to `EXPO_APP_ID`. Those requests then fail with `400`. Leave unset while clients built before v2 are still in the wild. |

### Enterprise features

| Name                                   | Required                                                | Description                                                                                                                                                                                                                                     |
| -------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRUST_PROXY_HEADERS`                  | ❌                                                       | Used for [IP whitelisting](/expo-open-ota/security/ip-whitelisting.md). 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 [IP whitelisting](/expo-open-ota/security/ip-whitelisting.md). Tells the server how many trusted proxies sit in front of it.                                                                                                           |
| `AUDIT_LOG_RETENTION_DAYS`             | ❌ (defaults to `550`)                                   | How many days [audit log](/expo-open-ota/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](/expo-open-ota/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`.                                                                                                                                     |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://mercure-technologies.gitbook.io/expo-open-ota/references/environment-variables.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
