Revio
Customer feedback from a tap on the counter — no app, no sign-up.

- My role
- Solo developer — architecture, API, background jobs, dashboard and deployment.
- Status
- Live
The problem
A café or a hairdresser finds out a visit went badly when the one-star review is already public. Asking at the counter rarely works either: the honest answer is the one people will not give to your face, and every alternative — find the listing, sign in, write something — loses almost everyone before the first tap.
What I built
A multi-tenant SaaS. Each business gets NFC cards for its counter and tables; the customer holds a phone to one, rates the visit in a few seconds and can leave a note, with nothing installed and no account created. Behind the tap is a .NET 8 API in a clean architecture — CQRS through MediatR, EF Core over MySQL, Redis on the hot path and Hangfire pulling Google Business Profile reviews in overnight — and a React dashboard where scores, notes and Google reviews meet, split by table, till and staff member.
What it looks like

Three steps, no setup — the whole product in one screen. 
Google reviews land in the same panel — read-only, and revocable from Google's own settings.
Architecture
Physical layer
Each card encodes a URL carrying its own business and placement, so a score arrives already knowing which table it came from.
- NFC tags
- QR fallback
Scan endpoint
The hot path. Card and business are read from cache, the scan is logged out of band, and the customer is redirected — this is the one request in the system that is allowed to be slow for nobody.
- ASP.NET Core
- Redis
API
Domain, Application, Infrastructure and Api as separate projects with dependencies pointing inward only; the domain project references no packages at all.
- .NET 8
- MediatR (CQRS)
- FluentValidation
- EF Core 8
Background jobs
Overnight review sync, queued as one job per business so a single failing account cannot block the rest, and idempotent on Google's own review id.
- Hangfire
- Google Business Profile API
Dashboard
Score distribution, busy hours and recurring complaints, with Google's reviews beside the business's own.
- React
- Vite
- TanStack Query
- Recharts
Decisions & trade-offs
The star filter is an ordering, not a gate
Routing only happy customers to Google is called review gating, and Google's own policy forbids selectively soliciting positive reviews — a business caught doing it can lose its reviews outright. So the rating decides which screen comes first, not which doors exist: someone who rates one star still sees the Google option, they just see the "tell the business directly" form before it. The legitimate way to have fewer bad reviews is to fix the visit while the customer is still in the room.
One platform Google account, added as a manager
The first design asked each business to paste its own authorisation into the panel. Google's access tokens expire after an hour, so the nightly sync broke by the next morning; working with refresh tokens instead needs a client id and secret per business, which is not something you can ask a hairdresser to generate. Now the owner adds the platform's support address as a manager on their own profile — no token, no password, revocable from Google's settings in one click.
Tenant isolation as a query filter, not a WHERE clause
Every tenant-scoped entity carries a global query filter, and the check that a location belongs to the asking tenant runs on the server rather than in the panel. Filtering in application code is one forgotten clause away from showing a café another café's reviews; hiding the option in the UI only stops the people who use the UI.
Compute first, ask a model second
Review insights are calculated deterministically wherever the arithmetic is enough, and only the parts that need language go to a model — Gemini first, Claude as a fallback when Gemini fails, under a daily cap, and the feature degrades quietly when no key is configured. A summary that costs money and can hallucinate should be the exception, not the pipeline.
Outcome
An unhappy customer reaches the owner before they reach Google — while they are still standing in the shop, and in time for someone to do something about it.
Measured
- Automated tests
- 878 across 111 files
- Packages referenced by the domain layer
- 0