TLDR: Your app can look completely done and be a silent failure machine. A WIN audit — asking every screen whether it handles loading, empty, error, partial, and stale states — will show you exactly where.

the setup

I'd been building an internal webinar event manager hard for about a week and a half.

It's the internal event project manager that replaced our old Notion V3 framework for an ecommerce business's webinar team. Next.js 16 + Supabase (our Postgres backend) + Tailwind v4 + shadcn components. Features stacking fast: keyboard navigation, a Cmd-K command palette, inline task editing, cross-entity search, a PWA install prompt.

It looked done. Genuinely done.

the wall I almost ignored

Right before handing it to the team, I ran a WIN audit.

The idea is simple: walk every screen and ask whether it handles all five states a user might actually land in — loading, empty, error, partial, and stale. Every one. Not just the happy path.

This sounds like paperwork. It is not paperwork.

what I found

The first finding stopped me completely.

We had sonner (the toast notification library) mounted in the layout. Ready. Wired. Waiting to fire.

There were ZERO toast() calls anywhere in the app.

Every server action that failed? Silent. Every useSWR (our data-fetching hook) error field? Ignored. Every Supabase database error coming back from a React Server Component? Swallowed — never surfaced, never logged, never shown.

The app looked fine on the happy path. Click the right buttons in the right order on a fast network? Works great!

But the moment something went sideways — a failed DB write, a bad network hop, an empty result — the user was completely on their own. No toast. No shake. No error state. Just… silence.

That's not "a little rough." That is ALL CAPS BAD. People would be editing tasks, submitting forms, navigating — and they'd have no idea whether anything actually worked.

the fix

Seven commits. Shipped overnight, all green, all auto-deployed to Vercel before the team saw a thing.

The core move was a small contract: src/lib/validation.ts (using zod, a validation library) + a typed ActionResult — either ok() or fail("user-facing message"). Every server action now validates its inputs first, actually checks the Supabase write response (chaining .select() to surface any database-level errors), and returns structured success or failure.

Then ActionForm and runAction wired that contract to a real toast on failure. Finally — the Toaster had calls to answer.

We layered in optimistic rollback on task title edits, SWR error states throughout, RSC throw-on-error with gated error boundaries, explicit loading spinners, and a proper not-found.tsx.

Vitest: 23 → 54 passing. ESLint: 0 errors, 0 warnings. Build: green.

why it matters to me

Every single bug I found was invisible on the happy path. I would have shipped a polished-looking app that silently swallowed failures and gave users zero feedback.

The WIN audit forces you to build the boring half — what the screen looks like when something ISN'T happening. Loading state. Empty state. Error state. Rollback.

That's the work that separates a prototype from something you can actually hand to people.

I almost skipped it. I almost just called it done.

Really glad I didn't.