OTA updates, end to end for your Expo apps.

xprem implements the official Expo Updates protocol, with channel-to-branch mapping, progressive rollouts and delivery from your own storage. Self-hosted, open source.

Coming from EAS Update? npx eoas init migrates your app in about 30 seconds.

ota.xprem.dev/branches/production
All branchesproduction1.0.0
Rollout in progress

Update 17844529170002 · publishing to this branch and runtime version is paused until the rollout ends.

26IncreaseFinish rolloutRevert
25%

Updates

UpdatePublished

feat(observe): Implement health history tracking a…

2group:ad83acfb-32b9-4e4a-b8e2-5399f744a43e

24 Jul 2026 at 16:54

feat(observe): Implement health history tracking a…

2group:68c97637-eaf0-4aa7-ade1-1e8ec9b64be5

24 Jul 2026 at 16:44

feat: add healthRelevant field

2group:481aa951-ef22-4715-847f-7dc2b1c4d1ab

24 Jul 2026 at 16:24

Merge branch 'main' into feature/observe

2group:4c9023e7-fc1f-4416-adfa-6e85aa35b110

24 Jul 2026 at 15:21

Update 17848984081811

Rollback to embedded

24 Jul 2026 at 15:06

Update 17848984081702

Rollback to embedded

24 Jul 2026 at 15:06

Update 17848897610932

2group:0fff5e25-fdc3-440c-93e5-617580920fb7

24 Jul 2026 at 12:42

Update 17849048644461

Published 24 Jul 2026 at 16:54:24

iOSNormal update

Health over time

Near-real-time health, retained in one-minute buckets.

24/07/2026, 16:45:00 → 25/07/2026, 14:30:00

Synced 14:43:29

HealthAdoptionFaults

Successful devices across all attempts

iOS 95.1%

100%50%0%
18:0000:0006:0012:00

Deployment

Branch
production
Runtime version
3.0.0
Platform
iOS
Published
24 Jul 2026 at 16:54:24

Manage your OTA Updates.

xprem implements the official Expo Updates protocol, so your app keeps the standard expo-updates runtime and the manifest it already speaks.

Publish

npx eoas publish --branch production

Builds the update, uploads it to your bucket, and adds it to a branch. Nothing is delivered until a channel points at that branch.

Rollback

npx eoas rollback --branch production

Sends devices back to the update they ran before, or all the way back to the build embedded in the store binary.

Republish

npx eoas republish --branch production

Promotes an older update to the head of the branch, so recovery is a publish and not a rebuild.

Support of Branches & Release Channels.

Each build of your app ships with one channel baked in, like production, staging or uat. Every OTA update you publish is tagged with a branch. You then attach branches to channels, and that mapping decides which updates a given build receives.

Channelproduction
Default branchproductionCurrent update

Many apps on one server.

A single deployment hosts as many Expo projects as you need. Clients identify themselves with the expo-app-id header, and each app keeps its own signing keys, API keys, branches, channels and update history, isolated in your database.

Identity
App ID · signing certificate · encrypted private key
Access
Named API keys, per app, with usage history
Routing
Its own branches, channels and update history

Split one channel across two branches.

A channel rollout sends a share of the channel to a second branch and the rest to the default one. Both groups keep receiving updates on their own line, so you can run two real release lines against each other and promote the winner.

Channelproduction
Default branchproduction90% of devicesCurrent update
Rollout branchalpha-release10% of devices

Progressive Update rollout.

Publish an update to a slice of the branch, follow its health from the dashboard or over MCP, and increase the percentage or revert at any time. The server hashes the device EAS-Client-ID with a salt unique to that rollout, so assignment is deterministic and needs no per-device state.

Rollout in progress

Update 17844529170002 · publishing to this branch and runtime version is paused until the rollout ends.

26IncreaseFinish rolloutRevert
25%
Read how assignment works ↗

Every update lands in your own bucket.

xprem never holds your bundles. It writes them to the object storage you already pay for, under a key prefix you choose, and hands devices a URL into it. Switching provider is an environment variable.

  • Amazon S3STORAGE_MODE=s3
  • Cloudflare R2Cloudflare R2STORAGE_MODE=s3
  • Google Cloud StorageGoogle Cloud StorageSTORAGE_MODE=gcs
  • Azure Blob StorageAzure Blob StorageSTORAGE_MODE=azure
  • Supabase StorageSTORAGE_MODE=s3
  • MinIOSTORAGE_MODE=s3
  • DigitalOcean SpacesSTORAGE_MODE=s3
  • Local volumeSTORAGE_MODE=local

And any other S3-compatible provider. Key prefix optional on S3, GCS and Azure.

Assets download from your own infrastructure.

When a device checks for an update, xprem answers with a manifest: the list of every asset the device has to download, each with a URL. xprem decides what those URLs are. Point them at your CDN, or sign them into a private bucket.

xprem serverresolves the update
1 · asks for the manifest2 · manifest with CDN URLs
deviceexpo-updates
3 · downloads the assets
your CDNcaches the assets
4 · origin fetch
your bucketS3 · GCS · Azure
GET /manifest200
{
  "id": "b0b76751-afe8-58dc-ed2c-d02d9cd1bec5",
  "runtimeVersion": "3.0.0",
  "launchAsset": {
    "contentType": "application/javascript",
    "url": "https://cdn.xprem.dev/assets/3b1f9c…"
  },
  "assets": [
    { "contentType": "image/png",
      "url": "https://cdn.xprem.dev/assets/a7c204…" },
    { "contentType": "font/ttf",
      "url": "https://storage.googleapis.com/…?X-Goog-Signature=…" }
  ]
}
These URLs are yours. A CDN domain, or a signed URL into a private bucket.
Fast

Point the URLs at a CDN and the download traffic never touches the update server, which only has to answer a small JSON check.

Private

Signed URLs mean a private GCS or Azure bucket serves updates without ever being exposed.

Yours

Every URL in the manifest resolves inside your infrastructure, under your contract, in the regions you picked.

Generic CDN

CDN_BASE_URL

One base URL, and every asset URL in the manifest points at it. This is the Cloudflare in front of R2 route, the Azure Front Door route, and whatever else you already run.

CloudflareCloudflareFront DoorFront Door

CloudFront

domain · key pair ID · private key

Your distribution with your bucket as the origin. xprem writes CloudFront URLs into the manifest, and it takes precedence over the other CDN options.

CloudFrontCloudFront

Signed S3, GCS and Azure

15-minute validity

The bucket stays private. xprem signs each asset URL at manifest time, valid for fifteen minutes, so nothing has to be made public to be reachable.

Amazon S3GCSGCSAzure BlobAzure Blob

Pick the mode that fits.

xprem runs in two shapes, and you choose one at deploy time. If you do not want multi-app or metrics, and the only thing you want is to ship updates from your own bucket, stateless mode needs no database: a bucket, your Expo token, your app, and you are running.

Stateless

no database

A bucket, an Expo token, one app. Expo stays the source of truth for release metadata, and you run nothing else.

  1. devicecalls /manifest, then downloads /assets
  2. xprem serverone app, no database to run
  3. Expo EAS APIauthenticates publishes and holds the release metadata
  4. your bucketholds every update you publish
    Google Cloud StorageAzure Blob Storage
  5. your CDNserves the assets to the device
    Cloudflare

Control plane

Postgres

Your database is the source of truth. Many apps on one server, progressive rollouts, and no Expo account anywhere in the path.

  1. devicecalls /manifest, then downloads /assets
  2. xprem servermany apps, isolated by expo-app-id
  3. PostgreSQLrelease state, keys, branches and channels. No Expo account
  4. ClickHouseoptionalevents, metrics and logs, only if you want Observe
  5. your bucketholds every update you publish
    Google Cloud StorageAzure Blob Storage
  6. your CDNserves the assets to the device
    Cloudflare

What stateless gives up

One app per server

Stateless mode serves a single application. Running several means running several servers.

Expo stays in the loop

Branches, channels and their mappings are read from Expo, and publishing authenticates with Expo tokens.

No progressive rollouts

Percentage rollouts and channel splits need the release state that only control plane mode keeps.

Compare the two modes in the docs ↗

Run it on infrastructure you own.

xprem is one Go process: your Postgres, your bucket, your network. Deploy it on Railway, Docker, Kubernetes or a bare binary, and ship your first update today.