My wife and I have a problem (no, not THAT kind of problem).
It's the same problem every couple has: nobody can decide where to eat. Or what movie to watch. Or what show to binge next.
The conversation follows a depressingly predictable script — "I don't care, what do you want?" repeated ad infinitum until someone either picks something out of frustration or you just stay home.
So I built an app to solve it. WhaTo lets a group of up to 8 people join a session with a 4-letter code, swipe through options (restaurants, movies, or TV shows), and find out what they agree on. Like Tinder, but for dinner.
The Stack
- Framework: React Native with Expo (cross-platform iOS, Android, web)
- Real-time sync: Firebase Realtime Database
- API proxy: Cloudflare Worker (routes calls to Yelp, TMDB, Google Places)
- Animations: React Native Gesture Handler + Reanimated
- Testing: Jest + React Native Testing Library + Maestro (E2E)
The Interesting Problems
Real-Time Sync for 8 Concurrent Users
The core requirement was that everyone swipes simultaneously and sees results the instant the last person finishes. Firebase Realtime Database handles presence tracking and swipe state broadcast, but the matching algorithm runs client-side. Each client independently computes matches as swipe data arrives from other users. The server just broadcasts state changes.
This was a deliberate choice. Running the matching algorithm server-side would add a round-trip penalty on every swipe completion, and the algorithm itself is lightweight — it's just set intersection. The tradeoff is that every client computes the same result independently, which is redundant work, but the latency improvement is worth it. Results appear instantly as the last person finishes swiping, with no perceptible delay.
Session management was its own challenge. Sessions auto-expire after 24 hours via Firebase TTL rules. The 4-letter codes need to be unique within the active session window but recyclable after expiry — I didn't want to slowly exhaust the namespace.
Gesture Handling and Card Physics
This was the rabbit hole I didn't expect. React Native Gesture Handler + Reanimated handle the swipe animations, but getting the card physics to "feel right" took more iteration than any other feature.
The problem is that Tinder has trained everyone's muscle memory for how a swipe card should behave — the acceleration curve, the rotation on drag, the snap-back animation on an incomplete swipe, the way the card flies off screen on completion. If any of those are slightly off, the whole experience feels wrong, even if the user can't articulate why.
I ended up studying Tinder's actual animation curves by screen-recording the app and stepping through the footage frame by frame. Probably overkill, but the result is that WhaTo's swipe feels natural to anyone who's used a dating app.
Keeping API Keys Out of the Client
The app pulls restaurant data from Yelp, movie/show data from TMDB, and maps from Google Places. Shipping those API keys in the client binary is a non-starter — anyone with a decompiler gets your keys.
The solution is a Cloudflare Worker that acts as an API proxy. The client calls the Worker, the Worker calls the external API with the real key, and the response gets passed through. The Worker also handles rate limiting and request validation, so even if someone figures out the Worker endpoint, they can't abuse the upstream APIs through it.
What I'd Do Differently
If I were starting over, I'd skip Firebase Realtime Database and use something with better offline support. Firebase RTDB works fine when everyone has a connection, but handling the edge case where someone's phone drops to airplane mode mid-session and reconnects later is awkward. Firestore would've been a better choice for this, but by the time I realized it, migrating wasn't worth the effort.
I'd also invest more in E2E testing earlier. I added Maestro late in the process.
Try It
Free, no ads, no account required. Sessions auto-expire after 24 hours and I don't store your data beyond that.
Feedback welcome — especially if you've tried to solve this problem before and have opinions about what works and what doesn't.

Top comments (0)