A convoy app for a group of friends driving in separate cars on a long road trip. Two features, nothing else:
- Live map — everyone sees everyone else's position in real time, so if the cars get separated they can still follow the same route. Each phone sets its own name and picks a colour from a palette of eight; both ride along with the position packets, so everybody's map agrees without a server-side roster.
- Walkie-talkie — push-to-talk voice between all cars, played through the car audio (Bluetooth or CarPlay/Android Auto), whether or not the phone is on silent and whether or not the app is foregrounded — or even in the recent apps list at all.
Reference points: TenTen for the walkie-talkie, Bump for the map.
Push-to-talk is half-duplex because it has to be, not because it is nicer. Any always-hot microphone forces a Bluetooth link from A2DP (stereo, good) to HFP (mono, narrowband, "phone call music") for as long as the mic is open — for the entire drive. Keeping the mic closed until somebody holds the button is the only way the music sounds normal.
Almost every non-obvious decision in this repo follows from that. They are written down in
docs/DESIGN.md, which is worth reading before changing anything under
src/lib/audio.ts or modules/expo-ptt.
- Expo (managed workflow + config plugins), React Native, TypeScript
- Development builds, not Expo Go — Expo Go cannot load custom native code
- LiveKit, self-hosted, for both the audio room (Opus over WebRTC) and position broadcast over the data channel, which deliberately avoids standing up a second realtime backend
expo-location+expo-task-managerfor background locationreact-native-maps- A small Node token server issuing LiveKit JWTs and sending APNs PushToTalk wakeups
App.tsx, index.ts entry point; registerGlobals and the audio-session regime decision
app.json the single source of truth for entitlements, background modes,
Info.plist keys and Android permissions
app.config.ts overlays the two values that differ per machine
src/
config.ts one hardcoded room, one hardcoded channel, one shared secret
lib/audio.ts the audio configuration, which is the design
lib/room.ts LiveKit room creation and connection
lib/location.ts background location task
lib/positions.ts the position packet format
lib/palette.ts the eight convoy colours
ptt/usePushToTalk.ts binds the OS push-to-talk session to the LiveKit mic track
state/ConvoyProvider everything the two screens need
modules/expo-ptt/ the native module: PTChannelManager on iOS, a foreground service on
Android, one JS API over both
server/ LiveKit JWTs + APNs PushToTalk sender
infra/ docker-compose for the SFU, Caddy and the token server
docs/ RUNBOOK, DESIGN, SETUP, SERVER, ROADMAP
docs/RUNBOOK.md is the one to follow first — fresh clone to both phones running, with the server on your Mac and no TLS, domain or APNs needed on day one.
cp .env.example .env # LiveKit URL, token server URL, shared secret
npm install
npx expo prebuild --clean --platform android
npx expo run:android # a physical device — the emulator has no Bluetoothdocs/SETUP.md covers the repo rules that make the Windows/Mac split work, and the Push to Talk entitlement including the license-agreement trap that blocks it.
The server is in docs/SERVER.md — including why the TLS certificate must not be a Cloudflare Origin CA one.
Where the project actually stands, phase by phase, is in docs/ROADMAP.md.
Alpha, for one trip with a handful of known people. No invites, no accounts, no onboarding, no group management — the room is hardcoded and the auth is a shared secret. That is deliberate.