> 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/3-0-x/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?** [Control plane mode](/xprem/3-0-x/controle-plane-mode/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/expo-open-ota: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/3-0-x/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 — see [Users](/xprem/3-0-x/dashboard/users.md).

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` header into `app.config.(js|ts)`. 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.

<br>


---

# 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/xprem/3-0-x/stateless-mode/getting-started.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.
