Vybe
A private, Android-first social app: a live friends map with consent-based location sharing, private photo sharing and end-to-end encrypted chat.
- Status
- Beta
APK 0.1.5 published 22 Oct 2026 with push; E2EE chat and push not yet confirmed on a physical phone
- Role
- Owner and developer: product decisions, architecture, the Android release builds and the server deployment.
- Built with
- Expo SDK 58, React Native, TypeScript, Kotlin, libsignal, Express 5, Prisma 7, PostgreSQL 17, Firebase Cloud Messaging, Docker, Caddy

The problem
Location-sharing apps tend to share too much with too many people. Vybe keeps the circle to accepted friends, makes every share an explicit choice, and encrypts chat so the server only ever stores ciphertext.
How it works
- The Android app (Expo dev build, expo-router, TanStack Query) talks to a single Express 5 API over HTTPS and an authenticated WebSocket for realtime fan-out.
- Location runs as a foreground service with a 120 s target. The server decides who may see a point in one place, authz.ts, and purges locations after 24 hours.
- Chat uses Signal's own libsignal through a Kotlin native module: PQXDH session setup and the Double Ratchet. Private keys never leave the device; history sits in SQLCipher.
- The API, PostgreSQL 17 and media run in Docker Compose behind Caddy with automatic TLS on an Oracle Cloud VM. Over-the-air updates are code-signed.
Decisions that mattered
- libsignal instead of custom crypto
- A reviewed, maintained Signal implementation with PQXDH and the Double Ratchet. It makes the app AGPL-3.0.
- One encrypted device per account
- Multi-device Signal needs sender keys and device lists; getting it wrong breaks the security model.
- Store-and-forward only for chat
- Ciphertext is deleted on acknowledgement, and read receipts are encrypted control messages, so the server never learns read state.
- No Redis, no PostGIS
- One API process on one VM. Queries are 'latest point for these friends', never 'users near X'.
Problems I had to solve
Chat failed only in release builds
Every send failed in processBundle. Replaying a friend's real key bundle with desktop libsignal worked, so the keys were fine. The R8 mapping showed a libsignal callback type renamed by minification; adding keep rules fixed it.
Sessions looked up with ourselves
libsignal's SessionCipher and SessionBuilder take the local and remote addresses in opposite orders. The module passed them the same way to both. Fixed and verified in a full two-phone round trip.
Windows path limits broke the native build
React Native codegen paths exceeded 260 characters. Long paths plus a newer SDK CMake got the first signed release APK built on Windows.
What is verified, and what isn't
| Part | State |
|---|---|
| 79 server integration tests against real Postgres 17 (auth, friends, location, photos, E2EE via the API, realtime, stories, admin) | Verified |
| API live over HTTPS with a Let's Encrypt certificate; only Caddy exposes ports | Verified |
| Signed release APKs 0.1.0 to 0.1.5 and a code-signed OTA channel | Built, not yet verified |
| E2EE chat, camera and background location on a physical phone | Built, not yet verified |
| Push notifications through Firebase (server key accepted by Google; not yet confirmed on a phone) | Built, not yet verified |
| Google sign-in (needs an OAuth client) | Blocked |
| Encrypted backups (need an age key pair) | Blocked |
Taken from the project's own status documents.
Still unfinished
- Device testing of chat, camera, background location cadence and Keystore storage
- Confirm push on a phone, and set up Google sign-in
- Legal review of the privacy policy and terms before a public launch
- Multi-device encryption (deliberately out of scope for v1)