Incident War Room: an incident-response tool where the approval gate and the AI can't be faked out A developer built Incident War Room, a real-time incident-response app for on-call teams using Next.js 16 and Sanity as the backend, with an escalation approval gate enforced in a Server Action rather than the UI so a single responder cannot push a SEV1 through alone. The project, created for the dev.to × Sanity "Path Two" hackathon with Claude Code, uses Sanity Workflows and the App SDK for live updates and includes an LLM-generated root-cause feature that is instructed to answer "unknown" rather than guess. This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange https://dev.to/challenges/sanity-2026-09-16 Incident War Room is an incident-response app for on-call teams. When something breaks at 2 AM, everything about the incident lives on one page: It's built with Next.js 16 App Router and Sanity as the whole backend. Live app: https://incident-war-room-nine.vercel.app https://incident-war-room-nine.vercel.app Try the gate yourself all passwords are demo1234 : alice@example.com prefilled , open the sample incident and click dana@example.com , open the same incident and approve. It flips to SEV1 / escalated and a status page entry appears. bob@example.com an engineer : you can't approve at all. A real-time incident-management app built for the dev.to × Sanity "Path Two" hackathon https://dev.to/challenges/sanity-2026-09-16 . Next.js 16 on the front end, Sanity as the backend — using Sanity Workflows and the Sanity App SDK for real functionality, not as checkbox features. Live demo: https://incident-war-room-nine.vercel.app https://incident-war-room-nine.vercel.app Sanity project: am9ihg1w / dataset production Login: seeded test accounts in docs/testing-credentials.md listen API, no polling. escalationApproval request needing sign-off from src/app/incidents/ id /actions.ts , not just hidden in the UI: the approver is always the logged-in responder, must be an on-call lead, and can't approve twice, so one person can't push a SEV1 through alone. pending → approved/rejected as a Kanban board in Studio sanity-plugin-workflow . Once… The schema is documented in docs/schema.md and the design in docs/architecture.md . I built the whole thing with Claude Code . Before any code, I had it write planning docs docs/overview.md , docs/architecture.md , docs/schema.md so each later prompt had a fixed target: a schema, the rules for the escalation gate, and what "real" use of Workflows and the App SDK meant. My rule for both bonus features was that a judge should be able to try to break them. Where the model got stuck, and how I corrected it: AGENTS.md that tells the agent to read the docs bundled in node modules/next before writing code, because APIs changed. That's how it got things like middleware being renamed to proxy right, instead of writing last year's Next.js from memory. status: "approved" directly. The Kanban board stayed, to track where each escalation stands, but the real rule moved into a Server Action approveEscalation that is the only code path allowed to finish the transition. runbook schema existed in Studio, but nothing in the app read it, and the page title was still "Create Next App". Asking the agent "what's unused or unfinished?" found more than asking it to "add a feature". ifRevisionId so two approvals at the same moment can't both count as "the first". author as a bare reference and showed "Unknown responder"; events the server added escalation, resolution didn't show up because the client kept its first list; and the page showed two different clocks UTC from the server on Vercel, local time from the browser . All fixed, all found by driving the live site with Playwright, not by reading code. Reaching past the Studio: pending → approved / rejected , shown as a Kanban board in Studio. Once the Server Action counts two approvals, it flips the incident to SEV1 / escalated, creates a statusPageEntry , adds a timeline event and can POST to an external status page webhook. @sanity/sdk-react . One GROQ query gives live mean time to resolution, open incidents by severity and pending approvals. It started read-only, which felt like decoration, so it can now approve escalations too, using the openai/gpt-oss-120b for a root cause and up to three action items, told to say "unknown" instead of guessing. A custom Studio input RootCauseInput.tsx adds a "Regenerate with AI" button, so an editor can ask for another draft at any time. am9ihg1w production Schema types: incident , timelineEvent , responder , escalationApproval , statusPageEntry , postmortem , runbook . Two modeling decisions I'd defend: timelineEvent documents that reference the incident, two posts in the same second never conflict, and the app subscribes to them with incident. ref . postmortem.timelineSnapshot as plain objects, so later edits to the live timeline can't quietly change what the postmortem says happened.