> 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/getting-started.md).

# Getting Started

By the end of this guide you'll have a server running locally, storing and delivering your updates from your own infrastructure — while Expo keeps managing your channels, branches, and publishing rights.

> **Want to cut the cord to Expo entirely?** The [control plane](/xprem/installation-guide/overview.md) runs on Postgres, hosts several apps, and needs no Expo account at all. You don't have to decide now: set `DB_URL` later and the server migrates this setup — your app, your keys, your updates — into the control plane on its own. Your clients never notice. The first control-plane boot also requires a master key (`DB_KEYS_MASTER_KEY_B64` or `AWSSM_DB_KEYS_MASTER_KEY_SECRET_ID`) and the flat-env variables still present, so the keys can be sealed into the database — plus `ADMIN_EMAIL` and `ADMIN_PASSWORD`, which seed the first dashboard account (`ADMIN_PASSWORD` must then meet the dashboard password policy).

### Prerequisites

* Docker
* An Expo project using `expo-updates`
* An Expo account

### 1. Get your Expo access token

The server uses this token on your behalf: it asks Expo which branch each channel points to on every update check, and it validates every publish against your Expo account.

1. Open the [Access tokens page](https://expo.dev/settings/access-tokens) in your Expo dashboard.
2. Select **Create token**.
3. Enter a name, then select **Create**.
4. Copy the token and keep it — you'll paste it in step 4.

This is your `EXPO_ACCESS_TOKEN`.

### 2. Get your project ID

From the EAS CLI, in your project directory:

```bash
eas project:info
```

Or from your [Expo dashboard](https://expo.dev/): open **Projects**, select your project, and copy the project ID shown at the top.

This is your `EXPO_APP_ID`. It's also the identity your clients send on every request, via the `expo-app-id` header.

### 3. Generate your signing certificates

From your Expo project directory:

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

This writes three files to `certs/`:

| File              | Who uses it                                                        |
| ----------------- | ------------------------------------------------------------------ |
| `private-key.pem` | The server, to sign manifests                                      |
| `public-key.pem`  | The server                                                         |
| `certificate.pem` | Your app, to verify updates — **commit this to your Expo project** |

### 4. Start the server

Generate a JWT secret first, and keep the value:

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

> Paste the generated value into the command below rather than inlining `$(openssl rand -base64 32)`. A secret that changes on every restart logs you out of the dashboard and invalidates CLI sessions each time the container comes back.

```bash
docker run --rm -it \
  -p 3000:3000 \
  -e BASE_URL=http://localhost:3000 \
  -e EXPO_ACCESS_TOKEN=your-expo-token \
  -e EXPO_APP_ID=your-project-id \
  -e JWT_SECRET=your-jwt-secret \
  -e STORAGE_MODE=local \
  -e LOCAL_BUCKET_BASE_PATH=/updates \
  -e KEYS_STORAGE_TYPE=local \
  -e PUBLIC_LOCAL_EXPO_KEY_PATH=/keys/public-key.pem \
  -e PRIVATE_LOCAL_EXPO_KEY_PATH=/keys/private-key.pem \
  -e CACHE_MODE=local \
  -e USE_DASHBOARD=true \
  -e ADMIN_EMAIL=admin@example.com \
  -e ADMIN_PASSWORD='Admin123!' \
  -v "$(pwd)/certs:/keys:ro" \
  -v "$(pwd)/updates:/updates" \
  ghcr.io/mercuretechnologies/xprem:latest
```

Leaving `DB_URL` unset is what selects stateless mode. Look for this line in the logs:

```
⚙️  [STATELESS] Initializing Stateless Mode (Flat-Env Mode)...
```

Verify the server is up:

```bash
curl http://localhost:3000/hc
```

> Mounting key files works locally but rarely survives contact with a real deployment. For production, `KEYS_STORAGE_TYPE` also accepts `aws-secrets-manager` and `environment` (inline base64) — see [Key storage](/xprem/key-store/keys.md).

### 5. Open the dashboard

Go to [**http://localhost:3000/dashboard**](http://localhost:3000/dashboard) and log in with the `ADMIN_EMAIL` and `ADMIN_PASSWORD` you set above (`admin@example.com` / `Admin123!`). In stateless mode this pair is the dashboard's only account.

In stateless mode the dashboard is read-mostly: it shows your updates, your runtime versions, and the channel-to-branch mappings it reads back from Expo. Expo stays the source of truth.

### 6. Configure your Expo app

```bash
npx eoas init
```

Four answers matter:

| Prompt                                           | Answer                                                |
| ------------------------------------------------ | ----------------------------------------------------- |
| Project id (sent as the `expo-app-id` header)    | Accept the detected value — it's your Expo project ID |
| URL of your update server                        | `http://localhost:3000`                               |
| Do you have already generated your certificates? | **Yes** — step 3                                      |
| Path to your code signing certificate            | `./certs/certificate.pem`                             |

`eoas init` writes your update URL, the certificate path, and the `expo-app-id`, `expo-channel-name` and `xprem-branch` headers into `app.config.(js|ts)`. The last one is used by [branch surfing](/xprem/features/branch-surfing.md), a control-plane feature; declaring it now costs nothing and spares a rebuild if you migrate later. Since all of it lives in the native build:

> ⚠️ **You need a new native build for any of this to take effect.** See the [EAS Build guide](https://docs.expo.dev/build/introduction/).

### 7. Create a release channel

Your app asks for a **channel**. Updates live on a **branch**. In stateless mode, Expo owns the mapping between the two — your server queries it on every update check.

In your Expo dashboard, open **Over-the-air updates → Channels** and create a channel named `production`. Or with the EAS CLI:

```bash
eas channel:create production
```

> **The branch names must match.** Your server stores updates under whatever `--branch` you publish to, and looks them up under whatever branch name Expo's channel points at. Nothing links the two but the string — so publish to the branch your channel actually maps to.

### 8. Publish your first update

```bash
export RELEASE_CHANNEL=production #you can use other value
export EXPO_TOKEN=your-expo-token # If you are already authentified with eas login eoas will use the existing session
npx eoas publish --branch production
```

Open the dashboard — your update is now listed under the `production` branch. To see exactly what a client would receive, ask for a manifest yourself:

```bash
curl -sD - "http://localhost:3000/manifest" \
  -H "expo-app-id: your-app-id" \
  -H "expo-channel-name: production" \
  -H "expo-runtime-version: 1.0.0" \
  -H "expo-platform: ios" \
  -H "expo-protocol-version: 1"
```

**Your server works.** The `expo-manifest-filters: branch="production"` header confirms it resolved your channel through Expo and found your update.
