Blog
How We Built Our Own Review Loop: Patchrooms Inside Patchrooms
We didn't build a demo project to prove Patchrooms works. We pointed it at ourselves. The Patchrooms dashboard embeds the same SDK we ship to customers, filing into the same kind of project, through the same public ingest endpoint: no internal shortcut, no special-cased API. Every bug we hit while building the dashboard becomes a report in the dashboard, patch context our own agent reads back. This is the architecture, end to end, and why we think it's the right way to wire a human checkpoint into a loop you're actively building.
One embed, no special path
The Patchrooms dashboard is a React 19 SPA. Its root Layout.tsx (the component every route renders inside) embeds @patchrooms/sdk-web the same way a customer would embed it on a staging site. It points at SELF_PROJECT_KEY, the constant pr_self, and sets apiUrl: window.location.origin.
That last part matters more than it looks. window.location.origin means the widget is not pointed at some internal-only route. It's pointed at the exact same /ingest/* surface that every external customer's SDK talks to: open CORS, no cookies, authed by the project key header. We don't get a fast path. If /ingest/report has a bug, we hit it filing our own reports before any customer does.
The project it lands in, "Patchrooms Dashboard," isn't a fixture we hand-built either. It's created by the same seeding logic that runs for any admin org on server boot. The self project is just the one we happened to name after ourselves.
Why file bugs into your own product at all
The honest reason isn't "eat your own dog food" as a slogan. It's that a review checkpoint you don't personally rely on is a checkpoint you'll let rot. If reports from real customers were the only thing keeping /ingest/report honest, we'd find out about a broken upload path from a support ticket instead of from our own workflow breaking mid-build.
Wiring pr_self into Layout.tsx means every session of us building Patchrooms is also a session of us using Patchrooms. When a teammate hits a layout bug in the settings page, they click it, screenshot lands, comment goes in, and that report shows up in the same dashboard they're looking at, filed against the same project everyone on the team already has open. There's no separate bug tracker to context-switch into and no risk that the review layer is a thing we describe but don't run.
The same two-step upload, every time
When a report includes a screenshot or a voice note, the SDK doesn't attach the binary to the report request directly. It's a two-step flow: POST /ingest/blob first (multipart, memory-buffered, capped at 10 MB), which streams the file to our GCS bucket and creates a Blob document with reportId: null, unbound, not yet claimed by anything.
Only after that does the client POST /ingest/report with the blob ids referenced in its blocks. The server checks each blob actually belongs to the project and is still unbound, then does a compare-and-swap update to bind them to the new report atomically. If two requests race for the same blob, the loser's report gets rolled back and the client sees a 409.
This is the exact path a bug filed on the dashboard takes. Drag a screenshot into a report against pr_self and it goes through /ingest/blob, then /ingest/report, then the CAS bind (same code, same race handling, same rollback) as a screenshot filed on a customer's staging site.
The bug shows up before you've looked away
After a report is written, the ingest handler emits an event to an internal feedbackBus. An SSE stream (useFeedbackStream on the client side) is already subscribed, and it pushes the new report straight into the dashboard's feed.
That matters for dogfooding specifically because the person filing and the person triaging are frequently the same person, seconds apart. You click a broken button, type what's wrong, hit send, and the report is already sitting in the feed by the time you switch back to the dashboard tab. No poll interval to wait out, no manual refresh to confirm the write actually landed. If the SSE pipe were broken, we'd notice within one bug report, not within one customer complaint.
Surviving your own navigation
A dashboard is not a static staging page. It's client-side routed: you move from the project list to a project's feed to settings without a full page load, and Layout.tsx re-renders on every one of those transitions.
The SDK's init() is idempotent by design: calling it again tears the previous instance down and rebuilds it, rather than double-mounting a second widget or silently ignoring the second call. That's not a special case we added for pr_self: it's a general requirement for any SPA that re-parents the widget on navigation, and our own dashboard is the first and most frequent place that requirement gets exercised. If init weren't idempotent, we'd be looking at two floating widget buttons stacked on top of each other by lunchtime.
From click to patch, applied to ourselves
The point of Patchrooms isn't the report: it's what happens after. A report filed against pr_self has the same shape as any other: DOM selector, screenshot, URL, viewport, browser, console errors, the comment. "Copy agent feedback" turns that into the same Markdown prompt block (selector, goal, constraints, screenshot reference) that we tell customers to paste into Claude Code or Cursor.
We paste it into our own agent loop. A bug someone clicked on the settings page becomes patch context our own coding agent reads and fixes, the same review-checkpoint pattern we're asking you to put in front of yours. We're not claiming a growth number or a bug count here: this is an architecture story, not a metrics one. The point is narrower and more useful: the review loop we ship is the review loop we run, wired into the same product it's reviewing, through no path a customer couldn't also take.
FAQ
- What is pr_self?
- It's the project key for our own dashboard's Patchrooms project, seeded automatically on every server boot. Every install of Patchrooms creates a project the same way: pr_self is just the one we point at ourselves, alongside a self project named "Patchrooms Dashboard" in the org that owns it.
- Does the dashboard call a special internal API to file its own bugs?
- No. It calls the exact same public ingest endpoint any customer's site calls, with apiUrl set to window.location.origin instead of a third-party domain. There is no internal fast path. If the public endpoint breaks, we feel it filing our own bugs before anyone else does.
- How do screenshots and voice notes end up attached to a report?
- Same two-step flow as any report: the client POSTs the binary to /ingest/blob, which streams it to GCS and creates an unbound Blob doc, then POSTs /ingest/report with the blob id. The server checks the blob belongs to the project and is unbound, then binds it atomically. If the bind loses a race, the report rolls back and the client gets a 409 and retries.
- Why does the dashboard need a live feed instead of just refreshing?
- Because the person filing the bug and the person triaging it are often the same session, seconds apart. After a successful report, the ingest handler emits to an internal feedbackBus, and an SSE stream pushes the new report into the dashboard's feed in real time: no poll, no reload, no "did that actually save" moment.
- What does idempotent init buy you here specifically?
- The dashboard is a client-side-routed SPA. Every route change re-renders Layout.tsx, which re-runs SDK init. Because init tears down and rebuilds instead of double-mounting or silently no-op'ing, the widget survives navigation without leaking duplicate listeners or losing its queued drafts. That's a general SPA requirement, not something special-cased for pr_self.