> 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/stateless-mode/overview.md).

# Overview

## Stateless mode

Stateless mode is xprem's original operating model. It powered v1 and v2.

It keeps the server lightweight. No database or separate control plane is required.

### How it works

xprem reuses Expo tokens and sessions to authenticate these operations:

* Publish an update.
* Roll back an update.
* Republish an update.

The server also reads branches, release channels, and their mappings from Expo. It does not manage this state locally.

```mermaid
flowchart LR
    CLI["eoas CLI"] -->|publish| SRV["xprem"]
    APP["expo-updates client"] -->|manifest| SRV
    SRV --> ST[("Your storage (bucket)")]
    SRV -->|"auth on every publish<br/>channel → branch lookup"| EXPO["Expo API"]
    style EXPO stroke-dasharray: 5 5
```

### Update storage

Updates live in an object-storage bucket. Each app uses this hierarchy:

```
appId/
├── cas/
│   └── {sha256}          bundles and assets, one object per distinct file
└── branch/
    └── runtimeVersion/
        ├── update-1
        ├── update-2
        └── update-n
```

Since v3.2.0, bundles and assets live in `cas/`, addressed by the hash of their content and shared between updates: deduplication is a lookup by hash in the bucket. Each update directory keeps `metadata.json`, `expoConfig.json` and the marker files described below. Updates published before v3.2.0 keep their files inside their directory and are still served.

```
Dashboard or client manifest request
                │
                ▼
        Resolve branch and channel in Expo
                │
                ▼
 Search the bucket under appId/branch/runtimeVersion
                │
                ▼
      Return the matching update or manifest
```

The dashboard searches this hierarchy to list updates. Manifest generation performs the same lookup.

Bucket searches can become slow as the update history grows. Stateless mode adds caching to reduce lookup time.

### When to use it

Choose stateless mode when you want a simple OTA server without operating a database. It still scales well for a single application.

You need one app, one storage bucket, and optionally Redis for caching. In return, you forgo control-plane features and remain dependent on Expo for release metadata and update operations.

{% hint style="warning" %}
Stateless mode supports **one application per xprem server**. Use the control plane when one server must manage multiple applications.
{% endhint %}

## Differences from control plane

The rest of the documentation describes a control-plane server. This section is the reference of everything that works differently on a stateless one; for a stateless deployment, this list wins over any other page.

### Mode selection

The server runs stateless when `DB_URL` is not set. Setting it switches the server to control-plane mode; your keys and existing updates are migrated automatically, as described in [Keys](/xprem/key-store/keys.md).

### App configuration

One server hosts exactly one app, configured entirely through environment variables:

| 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](/xprem/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.                                                                            |

The database variables (`DB_URL`, the master key) are not used, and there is no per-app key store: the variables above are the whole key configuration.

### Signing keys

You generate and store the app's signing pair yourself. 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.

Then point the server at the pair through `KEYS_STORAGE_TYPE` and the matching variables from the table above.

### Publishing and authentication

EOAS uses Expo authentication instead of `EOO_TOKEN`:

* Set `EXPO_TOKEN` to provide an Expo access token explicitly. In CI this is mandatory: no session exists as a fallback.
* Without `EXPO_TOKEN`, EOAS reuses the active EAS session from `eas login`.
* Leave `EOO_TOKEN` unset: its presence switches EOAS to control-plane authentication and the publish is rejected.

Channels are managed on Expo's side: create them with `eas channel:create`, and keep branch and channel names identical.

### Storage is the source of truth

With no database, the bucket carries the update metadata itself. Next to the files EOAS uploads, the server maintains its own marker files in each update directory:

| File                   | Written                         | Purpose                                                                                                           |
| ---------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `update-metadata.json` | At publish time                 | Holds the platform, the git commit hash, the optional update message, and the update UUID served in the manifest. |
| `.check`               | Last, after upload verification | Marks the update as complete. An update without it is invisible to clients and the dashboard.                     |
| `rollback`             | By `eoas rollback`              | Marks the directory as a rollback, served as a `rollBackToEmbedded` directive instead of a manifest.              |

These files are internal to the server. Do not edit or delete them: without `update-metadata.json` the update loses its identity, and deleting `.check` hides a published update.

### Dashboard

The dashboard is read-mostly: it shows your updates, your runtime versions, and the channel-to-branch mappings it reads back from Expo. `ADMIN_EMAIL` and `ADMIN_PASSWORD` are its only account and stay required for as long as the server runs; they are the sign-in credentials, not a first-boot seed.

### Not available in stateless mode

* **Progressive rollouts**: `--rollout-percentage` is rejected with an explicit error, and the dashboard shows no rollout controls.
* **Branch surfing**: the per-channel setting lives in the control-plane database. The `/branch_lists` endpoint answers `404`, so the in-app branch picker never appears. See [Branch surfing](/xprem/features/branch-surfing.md).
* **Bundle diffing**: patches are computed by background jobs and tracked in the control-plane database. See [Bundle diffing](/xprem/features/bundle-diffing-bsdiff.md).
* **API tokens**: publishing authenticates with Expo credentials, so [Token access](/xprem/security/token-access.md) does not apply.
* **Every Enterprise feature**: audit log, SSO, user roles, Observe and device identity all live in the control-plane database.
* **Multiple applications** on one server.
