Dishd
Inspiration
The "where do you wanna eat?" group text — we've all spent 45 minutes negotiating dinner, lost to indecision and dueling Yelp tabs. We wanted Tinder's swipe-to-decide UX but for a group's collective taste, so picking a restaurant feels like a game instead of a chore.
What it does
One person spins up a session, drops a pin on the map, and gets a 4-digit code. Friends join from anywhere and everyone swipes yes/no on real nearby restaurants pulled from Google Places.
The instant every active member yes-swipes the same spot, an instant-match overlay fires with confetti.
If there's no unanimous match, you get a Top 3 leaderboard ranked by agreement %, each with a Gemini-powered personalized "why this fits you" note.
Solo mode is built in for when you're picking lunch alone.
How we built it
iOS App
- SwiftUI (iOS 16+)
- URLSessionWebSocketTask for realtime
- Keychain for tokens
- MapKit for pin + result map
- iMessage app extension so invites work as a blue bubble
Backend
- FastAPI (async)
- MongoDB Atlas via Beanie ODM
- JWT access + refresh tokens
- slowapi rate limiting
- Full security middleware:
- CSP
- HSTS
- size limits
- startup sanity checks
Realtime
Per-session WebSocket rooms broadcast:
member_joinedswipe_progressinstant_matchphase_changetop3_ready
AI Layer
Gemini 2.5 Flash generates:
- vibe blurbs for restaurant cards
- personalized fit narratives for each Top-3 pick
Cost Control
A 6-hour TTL cache over Google Places nearby-search queries keyed by rounded lat/lng, so a crowd in the same neighborhood hits Places once.
Challenges we ran into
Beanie + UUIDs
UUID fields are stored as BSON Binary subtype 4, so raw-dict queries with str(uuid) silently matched nothing. Cost us hours.
SwiftUI Navigation
Using @Environment(\.dismiss) inside a fullScreenCover + NavigationStack caused the nav stack to pop back to a stale swipe view that re-ran its .task, hit a 500, and landed on a mock list.
Fixed by routing every "close" action through a single onClose callback owned by the cover root.
Match-popup Race Conditions
The swipe ack and WS instant_match event both fired for the same yes-swipe, while three separate flows could request navigation to results.
Every navigation path now goes through one-shot guards.
Google Places Deprecation
The legacy Places API got deprecated mid-build and started returning REQUEST_DENIED.
We shipped:
- a 20s timeout
- mock fallback data
So the swipe stack is never empty during demos.
Async Generator Bug
Using await inside generator comprehensions returns an async generator that a sync for loop cannot iterate.
We hit this 500 twice before switching to explicit asyncio.gather for member fan-outs.
Accomplishments that we're proud of
- A genuinely multiplayer, real-time app that works across multiple devices — not a demo with hardcoded state.
- A production-shaped stack:
- containerized backend
- env-driven config
- per-route rate limits
- JWT refresh
- push notifications
- security headers
- graceful degradation when the LLM key is missing
- The iMessage extension — joining a session straight from a blue bubble feels like magic.
- A query cache that keeps Google Places spend bounded even when judges hammer it.
What we learned
ODMs Aren't Free
Mongo's BSON quirks — especially UUID Binary subtype 4 — can absolutely bite you in production.
SwiftUI Navigation is Complex
NavigationStack + fullScreenCover interactions become messy quickly.
Explicit callbacks beat @Environment(\.dismiss) every time.
WebSocket Event Design Matters
Deduplication-safe events save you from race-condition whack-a-mole.
AI Features Need Graceful Degradation
DishMatch boots fine without a Gemini key and falls back to rule-based narratives, so the core flow never depends on a network call to an LLM.
What's next for Dishd
Atlas Vector Search
Replace yes-count math with a vector-based group welfare function over Gemini embeddings.
Already isolated behind the MatchingService boundary, so it's effectively an env-var flip.
Sudden-Death Tie-Breaker
A 3-second round between the Top 3 picks — first to N taps wins.
Live Preference Chips
Flash real-time chips like:
"Sarah liked this too"
on cards your friends already swiped.
Calendar + Reservation Hooks
Drop a match directly into:
- iMessage
- Google Calendar
with reservation info pre-filled.
Multi-City + Group Memory
Learn each crew's standing preferences across sessions, so the second time you swipe with the same friends, the stack already knows.


Log in or sign up for Devpost to join the conversation.