Docker
One image, one env file. The same container runs on a laptop, on a VM and on your orchestrator.
Publish, roll out, roll back, and watch every update you ship. Health, adoption, events and logs come back from every device that received it, into your servers, your storage and your ClickHouse, over the official Expo Updates protocol.
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. Publish, roll back and roll out progressively, with every bundle in your own bucket and every download URL pointing at infrastructure you own.
~/acme-app $ npx eoas publish --branch production
✔Exporting bundleios · android · hermes0.9s
✔Uploading assetss3://acme-updates · 412 files · 2.1 MB1.4s
✔Publishing to branchproduction0.2s
Update 1784952901 is live on production
devices pick it up on their next check-in
~/acme-app $
Publish, roll back and republish are eoas commands. Run them by hand or script them in your pipeline.
Each build ships with a channel baked in. Point the channel at a branch, and that's what those builds receive.
Ship to a slice of the branch, watch the health chart, raise it. Deterministic, no per-device state.
Bundles land in a bucket you own, and devices download from your CDN, or over signed URLs into a private bucket.
Every crash, metric, event and log carries the exact update that produced it. A regression shows up while the rollout is still on a slice of the branch, and a republish reverts it.
Explore Observe →Near-real-time health, retained in one-minute buckets.
24/07/2026, 17:30:00 → 25/07/2026, 17:30:00
Synced 17:30:19
Successful devices across all attempts
Bundle load
Time to first render
Time to interactive
Cold launch
Both land tied to the update that shipped them, with the device count for each, so you see within minutes when a release breaks something.
Every metric breaks down by device model, OS, region, app version and screen, straight from react-navigation.
Ship structured events and logs from the app, and read them next to the delivery data, filtered by the same update, branch and channel.
The data lives in a database you own. Put Grafana, PostHog or Datadog on it, or query it raw.
Tag devices with your own attributes and read health for one tenant, plan or cohort instead of the whole fleet.
xprem ships an MCP server, so agents reach the same control plane you do: branches, channels, updates, rollouts and the whole Observe dataset. Agents sign in with OAuth as dashboard users, and every call runs with that user's per-app permissions. Nothing more.
Which screens had the worst time to interactive since 3.4.2 rolled out?
Checkout regressed on 3.4.2. The other screens are flat against 3.4.1:
time to interactive · p90 · last 24 h · 3.4.2 vs 3.4.1
Want me to roll 3.4.2 back? You have that permission on acme-app.
Agents sign in over OAuth as a dashboard user, and inherit exactly what that user can do on each app. No API keys.
List branches, channels and updates, check a rollout, then roll back or republish, the same operations the dashboard exposes.
Plain-language questions over metrics, events and logs. The agent answers straight from Observe.
xprem is one Go process. It holds no session state, so you run as many replicas as you need behind your load balancer and they stay consistent on their own.
One image, one env file. The same container runs on a laptop, on a VM and on your orchestrator.
Deploy with the Helm chart and scale the deployment like any other stateless service in your cluster.
No runtime to install, no sidecars. Drop the binary on a box, give it its environment, run it under systemd.
Update checks are read-heavy and cheap. Add replicas behind a load balancer and they all resolve the same manifest from the same Postgres, with no leader, no sticky sessions and no coordination between them. Assets never pass through the process, so a replica only ever handles small JSON.
Open the deployment guide ↗$ docker run --env-file .env \
ghcr.io/mercuretechnologies/expo-open-ota:latest
EAS Update is the fastest way to get OTA updates running on a small app. These are the reasons teams move to self-hosted infrastructure instead.
Nothing is metered. Unlimited devices and unlimited updates, for the cost of the server, the database and the bucket you already run.
Progressive rollouts, channel splits across branches, per-update health, metrics, events and logs. Built on the MIT expo-updates packages, the official release path for React Native.
Air-gapped deployments, internal apps and regulated environments. No outbound connection, no telemetry, and Enterprise licence keys verify offline.
Stateless replicas behind your own load balancer, your CDN and the regions you choose. Capacity and rate limits are set by your infrastructure.
Update history in your Postgres, runtime signals in your ClickHouse, bundles in your bucket. Retention, residency, access rules and audit trail are yours to configure.
Device to your server to your edge, over private links and internal DNS, inside the regions you already operate in.
Publishing, branches, channels, rollbacks, progressive rollouts, every storage backend, every CDN integration, the dashboard and the Prometheus metrics are MIT. Everything you need to run OTA updates in production, free. A feature released under MIT never moves behind the commercial licence.
They live in ee/ directories in the same repository and ship in the same binary. The key is verified offline: the server never contacts us, sends no telemetry, and keeps working in an air-gapped network.
Roles instead of one shared admin account. Decide who can publish, who can move a channel to another branch, and who can only look.
Sign in through your own identity provider, so access follows the accounts you already manage and revoke.
Mark the branches that reach real users as protected, then decide per API key whether it may publish to them. A sandbox CI job or a developer testing on staging gets a token that cannot touch production, instead of the one token that can do everything.
Attach your own attributes to logs, metrics and events, then slice by them. Your plan tier, your tenant, your feature flag, whatever your app knows about the device.
The things people ask before they commit to running their own release infrastructure.
It requires expo-updates in your app, yes. That is the protocol xprem implements, and it is what lets you keep the standard runtime instead of a proprietary client. expo-observe is separate: without it xprem still runs your releases, health and rollouts, you just do not get the events, metrics and logs. We bet on the Expo ecosystem on purpose. The packages are MIT, the community is large, and expo-updates is the default release path for React Native.
Not in control plane mode. xprem holds the release metadata itself and authenticates publishing with a per-app API key. Stateless mode is the exception: it uses Expo as the source of truth and does need an Expo access token.
One Go process, a Postgres database and an object storage bucket. Add ClickHouse only when you want the full Observe layer. Everything ships as a single Docker image, a Helm chart or a static binary.
You point expo-updates at your own server URL and publish through the eoas CLI. The manifest, the asset download and the runtime behaviour are the standard protocol, so the client side of your app stays as it is.
In your bucket. S3, Google Cloud Storage, Azure Blob, Cloudflare R2, MinIO, DigitalOcean Spaces, Supabase Storage, any S3-compatible provider, or a local volume. xprem writes to it and hands devices a URL into it.
Yes. It runs inside a private or air-gapped network. There is no telemetry and no outbound connection, and Enterprise licence keys are verified offline, so the server keeps working with no route to the public internet.
There is no per-user pricing. Nothing is metered, so unlimited devices and unlimited updates cost you the server, the database and the bucket you already run. The release engine is MIT; only RBAC, SSO, branch protection and custom device attributes need a commercial licence.
The release engine is MIT: publishing, branches, channels, rollbacks, progressive rollouts, every storage backend, every CDN integration, the dashboard and Prometheus metrics. Enterprise adds RBAC, SSO, branch protection and custom device attributes, and lives in ee/ directories under a commercial licence. A feature released under MIT never moves behind the commercial licence.
Yes. Observe writes into a ClickHouse you own, so you can point Grafana, Datadog, PostHog or plain SQL at it. Release state is in your Postgres. Nothing is locked behind an API we control.
EAS Update is hosted by Expo and priced per monthly active user, and your update and device data lives on their infrastructure. xprem runs on yours: no metering, data in your own Postgres, ClickHouse and bucket, deployment on a private network, and per-update health, metrics, events and logs. EAS Update is the faster start for a small app; xprem is for teams that need to own the release path.
No. xprem is an independent open-source project. It is not affiliated with, endorsed by, or supported by Expo (650 Industries, Inc.).
Everything above is covered in more depth in the documentation.