Chapter 10 — Feedback → tickets → shipped: closing the loop while they remember
The single most valuable object in this company is a bug report from a real user. It is worth more than a roadmap session (it's evidence, not opinion) and more than an analytics dashboard (it comes with intent and context). Everything in this chapter exists to shorten the distance between user notices problem and user hears it's fixed — because a loop that closes while they still remember filing it converts a critic into a collaborator.
flowchart TD
USER["User hits a problem
(or has an idea)"] --> WIDGET["Floating 'Send feedback' widget
message + screenshot OR screen recording ≤15MB
anonymous OK (optional reply-to email)"]
WIDGET --> API["POST /api/v1/support/feedback
(IP rate-limited)"]
API --> STORE["Store + upload attachment to S3
(comment survives even if upload fails)"]
API --> OUTBOX["Queue outbox email → founder
Reply-To = the reporter"]
OUTBOX --> WORKER["Worker dispatches via SES"]
WORKER --> INBOX["Founder inbox
(one glance, no login needed)"]
STORE --> CONSOLE["Feedback console /feedback
New → Triaged → Resolved / Archived"]
INBOX --> TRIAGE{"Daily triage
(part of the 20-min cadence)"}
TRIAGE -->|bug| REPRO["Reproduce locally
(docker + seeded DB)"]
TRIAGE -->|idea| BACKLOG["Backlog / roadmap"]
REPRO --> FIX["Fix + test
commit with the feedback id"]
FIX --> DEPLOY["Push to main → pipeline → live"]
DEPLOY --> RESOLVE["Mark Resolved in console"]
RESOLVE --> REPLY["Reply to reporter
(Reply-To makes this one click)"]
REPLY --> USER
BACKLOG -.->|scheduled| FIX
The widget: lower the friction to zero
A floating button on every page. No login required — anonymous users leave an optional reply-to email (and many do). The message field asks for anything: bugs, confusion, ideas. The attachment field accepts screenshots and screen recordings (MP4/WebM/MOV, ≤ 15 MB) — recordings after we learned that users describing a broken flow in words produce ambiguous prose, while thirty seconds of screen video is a repro script. Two small decisions that matter:
- The comment is the payload; the attachment is enrichment. If object storage hiccups mid-submit, the feedback still saves and the loss is merely noted in the logs — never lose the words because the picture failed.
- The page URL and user-agent ride along automatically. "It's broken on /payments" arrives saying exactly which /payments.
The notification: reply-able by construction
Submission queues an outbox email (Chapter 03's transactional pattern)
to the founder with everything inline and — the detail that took a
bounce-storm to learn — Reply-To set to the reporter's address.
Staff hitting "reply" answers the user, not a dead mailbox. The email
also carries a deep link straight to the console entry.
The console: a tiny ticket system, honestly scoped
A single page with four tabs — New → Triaged → Resolved / Archived
— attachments rendered inline (with an auth-header blob fetch, because
<img src> can't carry tokens), recordings playable in a <video>
player, and a status workflow with timestamps. That's the entire ticket
system. No SLAs, no assignee matrices, no Jira — a workflow is only as
wide as the team that maintains it, and this one is maintained in the
same codebase as the product (fix-adjacent, therefore fix-adjacent in
the diff).
Case study: one bug, report to resolved
The best way to show the loop is a real one. A user reported: "On clicking Edit invoice button, new invoice page is opened. Not able to edit invoices." Same morning, a second report from the same user: double-clicking Create made duplicate invoices.
Repro (docker + seeded DB): both reproduced instantly. Diagnosis
went two layers deep — the visible bug was Angular route-input timing
(the editor read its id before the router bound it), but reproduction
exposed something worse underneath: the invoice repository loaded
entities without line items, so line updates 404'd and — the
corruption class — adding a line recomputed totals against an empty
set, silently wiping the invoice's math. One user's confused click had
uncovered a data-integrity hole. Fix: an effect for the route
input, and Include(LineItems) at the repository level (which also
fixed quote conversion losing its lines). Ship: tests, commit
referencing the feedback, push — the Chapter 07 pipeline had it live
same-day. Close: status → Resolved; the reply (one click, thanks to
Reply-To) told the user exactly what had been wrong and that their
report caught a second bug they never saw. That user has filed more
feedback than anyone — because the loop visibly worked.
Why this beats analytics
Analytics tells you that something is wrong ("drop-off on step 3"). Feedback tells you why ("there's no currency picker on this form" — a real report that same week). Instrument funnels, yes; but read your inbox like a scientist. At small scale, ten verbose humans outperform any dashboard you can build.
Recap. Zero-friction capture with recordings, reply-able notifications, a ticket workflow exactly as wide as the team, and a loop fast enough that the reporter experiences the fix. The feedback loop isn't support — it's your highest-signal product process.
Next: Chapter 11 — Reach: SEO, AI crawlers, and being the machine-readable answer.