The constraint that shaped everything
RYTA had a legacy .NET version with real users and listings. The new version had to ship fast, change daily during the launch weeks, and reach people on Android, on the web and inside the MAX messenger — without waiting for app-store review every time a button moved. That rules out a classic native app. It also rules out Capacitor-style tooling with a hosted update service, because the update path had to be ours.
One bundle, three shells
The application is a vanilla JavaScript PWA with a MapLibre GL map on PMTiles from our own geo stack. The same bundle is:
- seeded into the Android APK from assets and run in a thin Kotlin WebView shell,
- served as a PWA at web.ryta.online,
- mirrored as a bot and a mini-app inside the MAX messenger, live in production since 27 August 2026.
The app code follows a core template model: application code never edits core files; core improvements are pulled in by a manifest-driven script, so the shell and the update engine are shared with other apps built on the same template.
The OTA engine
On start the shell checks the backend for a newer bundle on its channel, downloads it, verifies the SHA-256 hash, and swaps it in atomically with rollback if the new bundle fails to boot. Channels allow staged rollouts; a minimum-native-version gate keeps a bundle from running on a shell that lacks a needed capability. Over 300 bundles have been published this way.
Identity without passwords
A user proves a phone number by calling an 8-800 number. The Rostelecom virtual PBX sends an HMAC-signed webhook to the backend, which matches the caller to the pending verification request. No SMS codes, no passwords; one person can have many devices. Subscriptions are keyed by the verified phone.
Backend and money
FastAPI with SQLAlchemy on PostgreSQL, MinIO for photos, FCM push built natively and scoped to direct messages to avoid fan-out storms, topic chat rooms plus DMs. Paid services go through T-Bank acquiring (Init plus a signed notification webhook) against an admin-managed price catalogue. A SvelteKit admin panel covers users, listings, billing, push, client errors, themes, taxonomies and a custom icon picker that converts Android vector XML to SVG.
A client-side diagnostics journal exports errors straight into the admin; release scripts detect environment drift; a redroid container provides a virtual Android device for testing; a script generates the Play Store screenshots, captions and feature graphic.
Numbers
About 36 600 lines in 116 files across backend, PWA, Android shell, admin and the MAX bot; users and listings migrated from the .NET system; running on a self-managed Docker/Caddy host with a private registry.
What I would do differently
Build the diagnostics journal before the first beta, not after the first bug report from a phone I could not see. And decide the channel policy (who gets which bundle) up front; retrofitting it onto users already in the wild is awkward.