Xavia OTA¶
Self-hosted Xavia OTA server — an
expo-updates manifest host that ships JavaScript bundle updates to the
mobile app over-the-air. Native binaries are still built locally; most merges are
JS-only and reach installed apps through this server without a store round-trip.
Two instances¶
Xavia has no channel concept: an instance serves the latest bundle per
runtimeVersion. To separate internal-tester updates from production, dev and
prod run as two independent instances — separate buckets, separate Postgres,
separate upload keys — rather than one instance split by channel.
| Instance | Host | Published |
|---|---|---|
xavia-ota-dev |
ota-dev.tomoda.life |
every app-repo merge (internal testers) |
xavia-ota-prod |
ota.tomoda.life |
the controlled v* release tag |
The app bakes which host it targets at build time and only accepts an update
whose runtimeVersion matches (set in the app's app.config.js); that value
bumps only on a native change, which requires a fresh local binary. See the
app repo's docs/frontend/native/release.md.
Shape¶
Each instance is a self-contained manifest set under
k8s/envs/platform/xavia-ota-{dev,prod}/ — its own application.yaml (Argo app,
namespace platform) and manifests.yaml, discovered by the platform recurse.
| Piece | Where |
|---|---|
| Deployment + Service (port 3000) | k8s/envs/platform/xavia-ota-{dev,prod}/manifests.yaml |
| Dedicated Postgres (metadata) | same file — single-instance postgres:16-alpine + 5 Gi PVC per instance |
| Bundle storage | GCS buckets tomoda-ota-dev-<project> / tomoda-ota-prod-<project> (infrastructure/gcp/xavia_ota.tf), accessed via Workload Identity (one SA xavia-ota) |
| Secrets | ExternalSecret per instance projects PRIVATE_KEY_BASE_64, ADMIN_PASSWORD, UPLOAD_KEY, POSTGRES_PASSWORD from GCP Secret Manager |
| Ingress | ota-dev.tomoda.life / ota.tomoda.life (cert-manager letsencrypt-prod, ingressClassName: traefik) |
| Argo apps | xavia-ota-dev / xavia-ota-prod (auto-discovered by the platform recurse) |
Secrets: shared vs per-env¶
Both instances share the code-signing key and the admin password; the upload key and DB password are per-env.
| Secret | Scope | Why |
|---|---|---|
tomoda-ota-signing-key |
shared | One signing key → one public cert works in the app, whichever host it targets |
tomoda-ota-admin-password |
shared | Single dashboard login |
tomoda-ota-{dev,prod}-upload-key |
per-env | Dev-CI and prod-CI publish paths stay isolated |
tomoda-ota-{dev,prod}-db-password |
per-env | Each instance has its own Postgres |
First-run setup¶
The secrets are Terraform-generated — nothing is typed by hand:
terraform applymints the RSA code-signing keypair (tls_private_key+ a 20-year self-signed cert), a shared admin password, and per-env upload keys + DB passwords, and writes each to Secret Manager.- Copy the
ota_signing_certificate_pemoutput into the app repo atfrontend/certs/ota-public.pem(the app validates OTA manifests against it). One cert covers both instances. - Retrieve the admin password / upload keys from Secret Manager when you need to log into a dashboard or publish (
gcloud secrets versions access latest --secret=tomoda-ota-dev-upload-key). The DB passwords are internal-only.
Xavia authenticates to GCS via ADC (the Workload-Identity-bound SA), so only
GCP_BUCKET_NAME is set per instance — no key file. The image is pinned to
xaviaio/xavia-ota:2.0.3 (latest release); bump deliberately.