- Go 76.5%
- JavaScript 12.6%
- CSS 6%
- Shell 3.2%
- HTML 1.1%
- Other 0.6%
| .agents/skills/simple-english | ||
| .claude/skills | ||
| .github/workflows | ||
| cmd | ||
| data | ||
| deploy | ||
| internal | ||
| packages | ||
| scripts | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
| spec.md | ||
| TODO.md | ||
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:
- Create a
.envfile from.env.example:cp .env.example .env - Set a private
ADMIN_PASSWORDandJWT_SECRETthere. The server does not start until both have a value:openssl rand -hex 32 - Start the app with Docker Compose:
docker-compose up -d - Access the dashboard at
http://localhost:4000.
In Your Expo Client
- Install expo-updates:
npx expo install expo-updates - Point the app at the server in
app.jsonorapp.config.js:{ "expo": { "runtimeVersion": "1.0.0", "updates": { "url": "http://your-server-ip:4000/api/expo/manifest/your-app-slug" } } }
The
runtimeVersionmust 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:
- Export the bundle:
npx expo export --platform ios --platform android - Write your public Expo config into the export:
npx expo config --type public --json > dist/expoConfig.json - Zip the contents of the
distdirectory:cd dist && zip -r ../update.zip . - Open the application in the dashboard.
- Select the runtime version.
- 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.