John Aleman
All projects

FlightNotes

Guest feedback for every pour. Breweries, wineries and bars put a QR code on the table, and guests rate what they are drinking.

Live
FlightNotes landing page with the headline 'Guest feedback for every pour' beside a sample tasting flight card
The problem
Feedback on a tasting flight is rarely recorded, so a business has little to go on about which pours guests liked.
Who it’s for
Two users at once: guests holding a glass and a phone, and the owners of breweries, wineries, distilleries and bars.
What I built
A QR-to-rating guest flow with no app or account, plus a business dashboard for menus, analytics, winners and opt-ins.
Key decision
Guests stay anonymous by default. A tasting is bound to a cookie, and the server stores only a hash of its token.

The problem

Back to contents

In a tasting room, a guest’s opinion of each pour is the most useful thing the business could learn, and it is rarely written down. FlightNotes starts from one premise: a guest should not need an app or an account to give it.

The business side has the opposite problem: it needs that feedback to add up to something it can act on, such as which pour is winning this week, where guests drop off and who agreed to hear from them. It needs that without the product turning guests into a marketing list they never agreed to join.

Product experience

What the product does for the people using it, and why it works that way.

Back to contents
  1. Scan, rate, done. No app, no account.

    A guest scans the table card and lands on the menu. Ratings, favorites and notes work immediately. The tasting survives a reload or a quick trip back to the menu, because it is held by a cookie scoped to that menu. Asking for an email is optional and comes only at the end, to save the flight to a journal.

    Guest rating a flight on a phone. Each beverage has five stars, a favorite heart and a note for the business
  2. One pour or a full flight, same product

    Many venues want feedback on a single featured pour, and flight-only wording made that feel unsupported. The format is derived from the item count rather than stored: one beverage reads as a Featured Pour, with no 1/1 progress counter, and two or more read as a Flight. Nothing migrates when a menu grows.

  3. Printed QR codes never need reprinting

    A code encodes a short permanent path that redirects to the current menu. Renaming the menu, changing its beverages or even changing its URL leaves every printed card working, and revoking a code is a single row update.

    Tasting menu editor for the 'Brewer's Picks' flight. Reorderable beverages sit beside a QR code panel with SVG, PNG, print and share options
  4. Telling guests before their notes became visible

    Notes started out private. When owners asked to read them, the field was relabeled “Add a note” with a line saying the business can read it, and notes written under the old promise stay hidden. A column defaults to private, and only notes written after the change are shared.

  5. Analytics owners can read at a glance

    Date ranges are presets in the store’s own timezone, compared with the previous period. Beverages need a minimum number of ratings before they are ranked, so one enthusiastic guest cannot crown a winner. Charts are hand-built SVG with colors checked for contrast and color-vision deficiency, a keyboard-accessible line chart and a table view.

    Analytics page with tastings started, finish rate, average rating and QR scans, above a daily activity line chart

Engineering decisions

Each one as the decision, the reasoning, the trade-off accepted and the outcome.

Back to contents

Anonymous, cookie-bound tasting sessions

Decision
Starting a tasting issues a random token in an HttpOnly cookie named for that menu. The server stores only its HMAC, and guest operations are scoped by menu rather than by session id.
Why
Guests should not need an account to taste. Sessions must not leak between people at the same table or across menus, and a forged, missing or foreign token should look the same as no token.
Trade-off
Clearing cookies or switching browsers starts a new tasting. That is acceptable for an in-venue flow, and the emailed tasting record is the durable copy.
Outcome
No personal data is collected to taste, and the API exposes no identifiers worth guessing.

Server Components call services; the browser uses GraphQL

Decision
Server Components call application services directly with the request’s viewer. Client Components go through one GraphQL Yoga endpoint. Both paths run the same authorization inside the services.
Why
Calling the app’s own GraphQL endpoint over HTTP from the server would duplicate auth plumbing and add a network hop to every render.
Trade-off
There are two entry points to keep consistent, so authorization has to live in services and never in resolvers or components.
Outcome
Server rendering has no internal HTTP round trips, and there is one mutation surface. Feature boundaries were later enforced with purpose-built service functions whose names state their authorization contract.

Weekly winners as a pure, rule-driven function

Decision
Winners are computed by a pure calculator over rating aggregates. Weeks are counted in the store’s timezone, there is a minimum-ratings threshold, and ties are broken in a fixed order. The rules live in config, not in SQL or components.
Why
A winner badge is public and a business will dispute it, so the rules must be explicit, testable without a database and identical everywhere they are shown.
Trade-off
Results are cached as snapshots stamped with a rule version, so changing a rule means recomputing them. Ratings are never discarded.
Outcome
The current week is shown as a leader, never as a winner, until it ends. Lifetime resets are owner-only, require typing a confirmation word that the server re-checks, and delete nothing.

Consent is a read model, not a checkbox

Decision
An email given to save a tasting is transactional. A business sees a contact only when that guest’s marketing consent is currently true, with the consent wording and time stored alongside it.
Why
A guest who wanted their own tasting record did not agree to be marketed to, and withdrawal has to take effect everywhere at once.
Trade-off
Owners see fewer contacts than the number of emails collected. The product chose that over a larger, less trustworthy list.
Outcome
The dashboard metric and the opt-in list read the same model, and CSV export neutralizes formula injection.

A cached menu shell with streamed per-guest parts

Decision
The public menu, its branding and the weekly winner badge render from tagged cache entries. The guest’s call to action, the staff preview banner and view tracking stream in behind their own Suspense boundaries.
Why
Reading the guest’s cookie at the top of the page made the whole route request-time, so its prerendered shell was a spinner.
Trade-off
A new weekly winner can take up to one revalidation window, five minutes, to appear, because a winner has no write event to invalidate on.
Outcome
Guests get the menu from cache immediately. The call-to-action skeleton matches the button’s size, so nothing shifts when it arrives.

Guest screens

The whole guest flow happens on a phone, after one scan.

Back to contents

Business dashboard

Back to contents

Walkthroughs

Back to contents
  • A guest scans, rates a flight and saves it

  • The business dashboard, from menus to winners

Architecture

A feature-modular monolith on the Next.js App Router: each feature owns its UI, GraphQL schema and resolvers, services and repositories, in one deployable app.

Back to contents
  • Feature changes stay within clear business boundaries.
  • Features depend on service contracts, not on persistence details.
  • Business rules and authorization stay the same across every entry point: Server Components, GraphQL and route handlers for callbacks and webhooks.
  • One deployment keeps operations simple.
FlightNotes architecture diagram showing Next.js App Router entry points—Server Components, Client Components through a GraphQL Yoga BFF, and route handlers—sharing application services across feature modules, repositories backed by Drizzle and Neon, and server-side provider adapters for external APIs

Outcomes and lessons

Back to contents

Shipped

  • Publicly launched at flight-notes.com with Free and Pro plans and Stripe subscriptions.
  • Guest flow: QR scan, menu, ratings, favorites, notes and an optional Flight Journal by magic link.
  • Business dashboard: tasting menus, a beverage catalog, analytics, weekly and lifetime winners, guest opt-ins and printable QR cards.

Validated

  • Over 70 architecture decision records capture the context, the decision and its consequences, including the ones that were later superseded.
  • Unit tests cover pure rules such as winners, aggregates and period math. Playwright suites run against local and staging environments.

Known limits and next steps

  • Guest notes are not moderated, so owners see guest text verbatim (rendered as text, never HTML).
  • The privacy policy has not yet had a legal review.

Lessons

  • Writing the decision record before the code made the trade-offs explicit, and gave later changes, like sharing notes, a promise to keep.
  • Next.js
  • GraphQL Yoga
  • TanStack Query
  • Drizzle ORM
  • Neon Postgres