My OTA update server for Expo apps
  • Go 76.5%
  • JavaScript 12.6%
  • CSS 6%
  • Shell 3.2%
  • HTML 1.1%
  • Other 0.6%
Find a file
Uwayss e8e914cdd5
All checks were successful
Deploy / deploy (push) Successful in 1m13s
Checks / checks (push) Successful in 1m16s
ci(docker): run on ubuntu-latest and drop the docker client install
2026-09-21 23:28:32 +03:00
.agents/skills/simple-english feat: add the simple-english skill 2026-08-21 16:39:51 +03:00
.claude/skills feat: add the simple-english skill 2026-08-21 16:39:51 +03:00
.github/workflows ci(docker): run on ubuntu-latest and drop the docker client install 2026-09-21 23:28:32 +03:00
cmd fix(tooling): write the public Expo config into the export 2026-09-08 15:20:55 +03:00
data fix(docker): run the server as uid 1000 2026-08-22 17:05:24 +03:00
deploy ci(docker): build the image in CI and pull it on the server 2026-08-26 22:05:21 +03:00
internal fix(tooling): write the public Expo config into the export 2026-09-08 15:20:55 +03:00
packages feat(tooling): resolve the runtime version with expo-updates 2026-08-31 10:00:21 +03:00
scripts fix(tooling): wait three minutes for the registry to serve a package 2026-09-08 17:17:02 +03:00
web fix(frontend): move the sign in button to the top right 2026-08-31 01:32:28 +03:00
.dockerignore refactor(docker): drop the dashboard build stage 2026-08-26 20:07:36 +03:00
.env.example chore(backend): remove the Node server 2026-08-26 18:01:21 +03:00
.gitignore ci(tooling): drop the CLI checks job 2026-08-27 22:23:14 +03:00
AGENTS.md docs(docs): point the contributor setup at the Go CLI 2026-08-28 12:38:17 +03:00
CLAUDE.md chore: fix up the docs and agent files 2026-08-21 17:12:46 +03:00
CONTRIBUTING.md docs(docs): point the contributor setup at the Go CLI 2026-08-28 12:38:17 +03:00
docker-compose.yml fix(docker): bind the published port to localhost 2026-08-25 12:46:00 +03:00
Dockerfile refactor(docker): drop the dashboard build stage 2026-08-26 20:07:36 +03:00
go.mod feat(tooling): port the CLI to Go 2026-08-27 22:21:55 +03:00
go.sum feat(tooling): port the CLI to Go 2026-08-27 22:21:55 +03:00
LICENSE chore: add the MIT license and prepare the CLI for npm 2026-08-22 16:08:06 +03:00
README.md fix(backend): serve the app config as extra.expoClient in the manifest 2026-09-08 15:20:32 +03:00
spec.md chore: rename the project to bareed 2026-08-22 15:40:19 +03:00
TODO.md Update TODO.md 2026-08-28 14:56:35 +03:00

Bareed

An easy to use self-hosted OTA update server for Expo applications.

Features

  • ⚡️ Multiple Apps on One Server: Manage multiple Expo apps
  • 🪾 Runtime Versions: Manage multiple runtimes for apps.
  • ⌛️ Retention Policy: Automatically keep only the latest N updates
  • 🪶 Lightweight: One Go binary and a SQLite file, in a 30 MB container

How to use

On Your Laptop:

  1. Create a .env file from .env.example:
    cp .env.example .env
    
  2. Set a private ADMIN_PASSWORD and JWT_SECRET there. The server does not start until both have a value:
    openssl rand -hex 32
    
  3. Start the app with Docker Compose:
    docker-compose up -d
    
  4. Access the dashboard at http://localhost:4000.

In Your Expo Client

  1. Install expo-updates:
    npx expo install expo-updates
    
  2. Point the app at the server in app.json or app.config.js:
    {
      "expo": {
        "runtimeVersion": "1.0.0",
        "updates": {
          "url": "http://your-server-ip:4000/api/expo/manifest/your-app-slug"
        }
      }
    }
    

The runtimeVersion must match a runtime version that you created for that application. If either one does not match, the server answers 404 and the app keeps the bundle that it has.

Upload an Update

Run this in your Expo project:

npx @uwayss/bareed

It exports the app, zips it, and uploads it. The first run asks for your server address and admin password, then keeps them in ~/.config/bareed/config.json.

Add --login to save them without an upload, or --logout to delete them.

Upload From CI

--yes asks nothing. Set the server address and the password in the environment:

- run: npx @uwayss/bareed --yes --app your-app-slug --runtime 1.0.0
  working-directory: ./app
  env:
    BAREED_URL: https://your-server
    BAREED_PASSWORD: ${{ secrets.BAREED_PASSWORD }}

The password stays in the environment. Nothing is written to ~/.config/bareed/config.json.

Without --app or --runtime, the CLI reads your Expo config. A runtimeVersion policy is resolved with expo-updates, so you only need --runtime when the two platforms resolve to different versions. --yes creates a runtime version that is not on the server yet. It stops at an application that is not there, so a typo in --app cannot create one.

Upload Without the CLI

Same as the CLI but manually:

  1. Export the bundle:
    npx expo export --platform ios --platform android
    
  2. Write your public Expo config into the export:
    npx expo config --type public --json > dist/expoConfig.json
    
  3. Zip the contents of the dist directory:
    cd dist && zip -r ../update.zip .
    
  4. Open the application in the dashboard.
  5. Select the runtime version.
  6. Upload update.zip.

metadata.json and expoConfig.json must sit at the root of the archive. The server reads the platform from the request, so one archive serves iOS and Android.

Constants.expoConfig comes from the update, so it reports the version of the last update and not of the installed binary.

The app downloads the update in the background and starts it on the next launch.

Test this with a release build. Debug builds don't use expo-updates

Back Up the Data

The state is the SQLite database in data/, the update files in data/assets/, and the ADMIN_PASSWORD and JWT_SECRET from your .env.

Copy the database while the server runs:

docker compose exec -T bareed bareed --backup /tmp/snapshot.db

The server reads the database through SQLite, so the copy holds every write and no half written page. Copying data/sqlite.db yourself does neither.

Contributing

Read CONTRIBUTING.md. It covers how to run the server from source, the checks that must pass, and how commits are written.