Generic CDN
CDN_BASE_URLOne 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.
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.
Update 17844529170002 · publishing to this branch and runtime version is paused until the rollout ends.
feat(observe): Implement health history tracking a…
2group:ad83acfb-32b9-4e4a-b8e2-5399f744a43e
feat(observe): Implement health history tracking a…
2group:68c97637-eaf0-4aa7-ade1-1e8ec9b64be5
feat: add healthRelevant field
2group:481aa951-ef22-4715-847f-7dc2b1c4d1ab
Merge branch 'main' into feature/observe
2group:4c9023e7-fc1f-4416-adfa-6e85aa35b110
Update 17848984081811
Rollback to embedded
Update 17848984081702
Rollback to embedded
Update 17848897610932
2group:0fff5e25-fdc3-440c-93e5-617580920fb7
Published 24 Jul 2026 at 16:54:24
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
Successful devices across all attempts
iOS 95.1%
xprem implements the official Expo Updates protocol, so your app keeps the standard expo-updates runtime and the manifest it already speaks.
npx eoas publish --branch productionBuilds the update, uploads it to your bucket, and adds it to a branch. Nothing is delivered until a channel points at that branch.
npx eoas rollback --branch productionSends devices back to the update they ran before, or all the way back to the build embedded in the store binary.
npx eoas republish --branch productionPromotes an older update to the head of the branch, so recovery is a publish and not a rebuild.
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.
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.
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.
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.
Update 17844529170002 · publishing to this branch and runtime version is paused until the rollout ends.
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.
And any other S3-compatible provider. Key prefix optional on S3, GCS and Azure.
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.
{ "id": "b0b76751-afe8-58dc-ed2c-d02d9cd1bec5", "runtimeVersion": "3.0.0", "launchAsset": { "contentType": "application/javascript", "url": "https://cdn.acme.internal/assets/3b1f9c…" }, "assets": [ { "contentType": "image/png", "url": "https://cdn.acme.internal/assets/a7c204…" }, { "contentType": "font/ttf", "url": "https://storage.googleapis.com/…?X-Goog-Signature=…" } ] }
Point the URLs at a CDN and the download traffic never touches the update server, which only has to answer a small JSON check.
Signed URLs mean a private GCS or Azure bucket serves updates without ever being exposed.
Every URL in the manifest resolves inside your infrastructure, under your contract, in the regions you picked.
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.
Your distribution with your bucket as the origin. xprem writes CloudFront URLs into the manifest, and it takes precedence over the other CDN options.
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.
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.
A bucket, an Expo token, one app. Expo stays the source of truth for release metadata, and you run nothing else.
Your database is the source of truth. Many apps on one server, progressive rollouts, and no Expo account anywhere in the path.
Stateless mode serves a single application. Running several means running several servers.
Branches, channels and their mappings are read from Expo, and publishing authenticates with Expo tokens.
Percentage rollouts and channel splits need the release state that only control plane mode keeps.
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.