> 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/key-store/keys.md).

# Keys

The server relies on three different keys, and which ones you actually need to configure depends on the mode you run it in. Here is the overview before we go through each mode:

| Key                                                                                                        | Scope       | Mode                                                                       |
| ---------------------------------------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- |
| **Expo signing key pair** — signs every manifest served to `expo-updates`; clients verify updates with it. | One per app | Both modes                                                                 |
| **Database master key** — encrypts the app signing keys stored in PostgreSQL. It is not a signing key.     | Server-wide | Control plane only                                                         |
| **CloudFront private key** — signs CDN asset URLs.                                                         | Server-wide | Both modes, only if you use [CloudFront](/expo-open-ota/cdn/cloudfront.md) |

### Expo signing key pair

#### Stateless mode

In stateless mode the server hosts a single app, and you generate and store its signing key pair yourself.

Start by generating the pair from the root of your Expo project:

```bash
npx eoas generate-certs
```

The command asks you for a certificate output directory, a key-pair output directory, a certificate common name and a validity duration, then creates three files:

* `public-key.pem` and `private-key.pem` — the server uses them to sign your updates.
* `certificate.pem` — your Expo client uses it to verify updates. Commit this file to your Expo project, and never commit the private key.

You then tell the server where to read the pair through `KEYS_STORAGE_TYPE`, which defaults to `local`:

| `KEYS_STORAGE_TYPE`   | Store                                     | Configuration                                                                  |
| --------------------- | ----------------------------------------- | ------------------------------------------------------------------------------ |
| `local`               | PEM files on disk, meant for development. | [Local Key Store](/expo-open-ota/key-store/local-key-store.md)                 |
| `environment`         | Base64-encoded environment variables.     | [Environment Key Storage](/expo-open-ota/key-store/environment-key-storage.md) |
| `aws-secrets-manager` | AWS Secrets Manager secrets.              | [AWS Secrets Manager](/expo-open-ota/key-store/aws-secrets-manager.md)         |

#### Control plane mode

In control plane mode, keys live at two levels: each app has its own signing key pair, and the server holds a single master key that protects the pairs stored in the database.

**The signing key pair of each app**

When you create an app from the dashboard or through the API, you choose where its signing keys live.\
This choice is made per app, it has nothing to do with `KEYS_STORAGE_TYPE` or the other stateless variables, which are ignored for app keys.

| Store                                     | Who generates the pair | How it works                                                                                                                                                                                                                           |
| ----------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `database` (preselected in the dashboard) | The server             | It generates an RSA pair when the app is created and seals it in PostgreSQL with the master key. You then download the certificate from the app page in the dashboard (`app-<id>-certificate.txt`) and commit it to your Expo project. |
| `aws-secrets-manager`                     | You                    | You run `npx eoas generate-certs`, store the two PEM files as AWS secrets, and reference their secret IDs when creating the app. The database stores no key material.                                                                  |

You can freely mix stores on the same server — one app on `database`, another on `aws-secrets-manager`. On the other hand, `local` and `environment` are rejected here, because files and environment variables cannot be provisioned for every replica.

{% hint style="info" %}
If any of your apps uses `aws-secrets-manager`, the server needs access to Secrets Manager: `AWS_REGION` plus an IAM role (or `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`) allowed to call `secretsmanager:GetSecretValue` on both secrets.
{% endhint %}

#### Migrating from stateless mode

If you started in stateless mode and later point the server at a database, the migration happens on its own: the server reads your local or environment keys once and seals them into the database with the master key. Your clients keep verifying updates with the same pair, so there is no key rotation to worry about.

### The database master key

(only in control plane mode)

As soon as one of your apps uses the `database` store, the server needs a master key to seal the pairs at rest. Provide it through exactly one of these variables:

| Variable                             | Source                     |
| ------------------------------------ | -------------------------- |
| `DB_KEYS_MASTER_KEY_B64`             | Environment variable       |
| `AWSSM_DB_KEYS_MASTER_KEY_SECRET_ID` | AWS Secrets Manager secret |

You can generate a valid key with:

```bash
openssl rand -base64 32
```

If you set both variables, or neither, the server refuses to start.

{% hint style="danger" %}
The master key is not recoverable, so back it up before your first deployment. If you lose it, every signing key stored in the database becomes unreadable.
{% endhint %}

### The CloudFront private key

The CloudFront key works the same way in both modes: a single server-wide key signs the asset URLs, independently of the per-app signing keys.

If `KEYS_STORAGE_TYPE` is set — in either mode — the server reads the key only from the source that matches it:

| `KEYS_STORAGE_TYPE`   | CloudFront key source                    |
| --------------------- | ---------------------------------------- |
| `aws-secrets-manager` | `AWSSM_CLOUDFRONT_PRIVATE_KEY_SECRET_ID` |
| `environment`         | `PRIVATE_CLOUDFRONT_KEY_B64`             |
| `local`               | `PRIVATE_CLOUDFRONT_KEY_PATH`            |

If another source is configured, it is ignored — and when the selected source turns out to be empty, the server logs a warning at startup so the misconfiguration doesn't go unnoticed.

If `KEYS_STORAGE_TYPE` is not set, the server simply tries the three sources in order: Secrets Manager, then the base64 variable, then the file path.

See [CloudFront](/expo-open-ota/cdn/cloudfront.md) to generate and configure the key pair itself.


---

# 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/key-store/keys.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.
