Music streaming with an Android app
AhoyMusic
My own music streaming service with a native Android app, my daily driver for two months now.
My daily driver for 2 months
10 TRACKS · 85 MB
Good evening
Liked Songs
A recreation of the real app with made-up tracks and no sound.
Core skills
- Mobile and native
- Backend and APIs
- Real-time systems
- Infrastructure and ops
- Reliability
- Testing and verification
Tech stack
- Expo
- React Native
- Reanimated
- expo-audio
- Kotlin
- Media3
- Expo Modules API
- MediaStore
- NestJS
- MongoDB
- Redis
- BullMQ
- ffmpeg
- Linux
- pm2
- nginx
- Let's Encrypt
The hardest part
The lock screen stuck on the first song
What looks frozen can be a loop running too fast.
The whole story
What it looked like
The music kept playing normally, but the notification on the lock screen showed the first song's title forever.
What was really going on
An endless loop: every notification restarted the background service, and every restart sent another notification. Android simply threw the flood away.
The fix
The service now only announces itself on an explicit action. Proven by tapping the real notification on an emulator.
- 846
- tests passed, app 548 and API 298
- ~24k
- lines of code, plus ~10.7k lines of tests
- ~1,000
- lines of Kotlin in 4 files
- 2 months
- in daily use
In plain words
Instead of paying for a music subscription, my music runs on my own server. The app plays it online or offline, in the background and with lock-screen controls, like the big services do. I use it every day.
Simply to save costs. And because a music player is a nasty stress test. Audio, the Android lifecycle and offline sync forgive nothing, and if something breaks, I notice it myself every day.
How it works
Step 1 of 5
Import
A BullMQ worker outside the API handles tracks one by one, so a stuck job can't block requests.
What was built
- A player with a queue, shuffle, repeat, background playback and lock-screen controls
- A native Kotlin module with a Media3 session that decodes nothing and mirrors the queue held in JS
- Offline mode: per-track downloads, sign-in without a network, library snapshots, cleanup of partial files
- Audio streaming with HTTP Range (206 and 416), so seeking works through nginx too
- An import worker with BullMQ outside the API, per-track progress, duplicate detection and file upload
- Separate libraries per account, a deploy script with preflight checks, nightly backups with a documented restore
How those skills were used
- Mobile and native
- Kotlin, foreground services, Media3 sessions, runtime permissions and config plugins, plus debugging with adb and logcat
- Backend and APIs
- NestJS with MongoDB and no ORM, Redis and BullMQ, 32 HTTP routes
- Real-time systems
- Range requests, proxy buffering and async status ticks that must not arrive out of order
- Infrastructure and ops
- Linux, pm2, nginx, Let's Encrypt, logrotate, a disk guard and backups that warn about empty dumps
- Reliability
- Silent wrong states become clear refusals, for example a deploy over an old install path
- Testing and verification
- 846 unit tests, and I checked the native part on a real device and an emulator
My role
Concept, architecture, directing the agents and verification. Claude Code wrote most of the code, inside an agent workflow I developed myself.
See the Context Observer project