Good coffee near me,is the wrong question.
A café discovery app that scores every room against a 7-dimension taste profile, what you’re there for, and how far you’re willing to walk in a Calgary winter, instead of a single star rating. Underneath it, a City Card loyalty program that partner cafés run themselves, from their own portal — receipts in, stamps out.
Seven axes. A café gets scored on all of them, same as a person.
A person heading in for three hours of deep work and a person meeting a first date have almost nothing in common except that both walked into a café. Every user and every café gets the same 7-dimension profile, plus a separate categorical answer for primary drink, because “sweet vs. bold” and “coffee vs. matcha” are different questions a single axis would blur together.
↔ strong, bold
↔ lively room
↔ cosy, maximal
↔ linger for hours
↔ rotating seasonal
↔ won’t flinch at $9+
↔ serious pastry case
One quiz, four screens that actually use it.
Take the quiz once. It feeds a matched “Today” feed, the context picker on every café’s own page, and the café’s own portal for reviewing receipts — four screens off one taste vector, not four separate flows bolted together.
Reconstructed layouts, not literal screenshots — the app has no marketing screenshots yet.
What a 92% match is structurally not allowed to include.
Paid or partner status can change whether a café gets promoted; it can never change its match score. The scoring function’s input type has no field for partner status or price paid, so there’s nothing for it to read — the guarantee is enforced by the type system, not by convention.
Cafés can pay to be listed, featured, or entered into the City Card. They can never pay for a better Match %. Every café gets the same honest number, partner or not — copied verbatim from the app’s own marketing copy, because I wanted the case study to hold itself to it.
A missing input drops out of the blend entirely, renormalized — a café with no known coordinates isn’t scored as 0m or ∞ away, proximity just isn’t part of its score yet. A test in score.test.ts pins the no-partner-field guarantee down so it can’t regress silently.
Receipt in, stamp out, five before a reward.
A matched café from Today or Discover, saved and visited in person.
Capped at 3 uploads a day per member — a receipt, not a claim, is the unit of trust.
Reviewed from the café’s own portal, not the consumer-facing admin tools.
Five stamps unlock a reward, activated and redeemed in person.
Two passes through my own code before a pilot user touches it.
Pass one covered auth, database, application, and infrastructure broadly. Pass two went back through the row-level security policies and the analytics write path line by line and caught three issues pass one missed, one of them critical. I’m keeping the findings table, not polishing it out.