DEV Community

Cover image for How I built a SaaS in a weekend with Claude + Next.js — the honest breakdown
Info Inlet
Info Inlet

Posted on

How I built a SaaS in a weekend with Claude + Next.js — the honest breakdown

Terrifying silent webhook data-loss bug

I built the first working version of my SaaS in a weekend with Claude and Next.js. That sentence is true, and it's also the least interesting thing I can tell you about the weekend.

The genre this headline belongs to is mostly a lie — not the "I did it" part, the implied part. "Weekend SaaS" posts sell you the fantasy that the hard thing is production: that if you could just type the code fast enough, you'd have a business by Monday. So they show you the scaffold, the pretty landing page, the Stripe button, and they stop exactly where the real work starts. The typing was never the hard part. The machine made that obvious by doing most of it while I watched.

So this is the other breakdown. The honest one. It's a story about one clean, green, perfectly-typed diff the model handed me on Sunday afternoon that would have quietly eaten a paying customer's money — and the one change to how I work that caught it. That change is the whole product now. The weekend just taught me what it was.

What actually happened, hour by hour, with no flexing

Let me get the "breakdown" people clicked for out of the way, because it's genuinely short.

Friday night was scaffolding, and it was stupid fast. create-next-app, the App Router, Tailwind, a Postgres instance, auth wired through a provider, the schema for the core objects. Claude wrote most of it and I steered. The stuff that used to be a Saturday of yak-shaving — config, boilerplate, the fourteenth time I've set up the same middleware — compressed into a couple of hours of me describing and the model producing. This part of the hype is real. Setup is solved.

Saturday was the actual feature: the one thing the product does that's worth paying for. Here the rhythm changed. I'd describe a slice, the model would produce a confident, plausible, well-structured implementation, and I'd spend most of my time reading it rather than writing it. Not reviewing in the "LGTM" sense — reading it the way you read a diff from a very fast junior who is never, ever unsure of themselves. Most of it was good. Some of it was good-looking and wrong, and the ratio is the entire story of building this way.

Sunday was supposed to be payments and polish. It became the reason this post exists.

The diff that looked perfect

Sunday afternoon I asked for the webhook handler — the thing that receives the payment event and updates the account. The model gave me back exactly what you'd want to see. Typed end to end. Idiomatic. Error handling in the right places. It even added a comment explaining the retry semantics, which is the kind of thoughtful touch that makes you trust the author. It compiled, it passed the test it had helpfully written for itself, and it read clean.

It acknowledged the webhook before it persisted the row.

If you've never been burned by that line, it looks like nothing. The handler returns its 200 OK to the payment provider, and then writes the record to the database. 99.9% of the time those two things happen microseconds apart and you never notice. But the provider retries on anything that isn't a fast 200, and the write can fail or lag — and on the one bad night where the ack goes out and the save doesn't land, the provider sees success, never retries, and you have a customer who paid you with no record that they did. Locked out of the thing they just bought, nothing in the logs to say they were ever there.

I know this bug in my body. I shipped it years ago, on a different product, with no AI anywhere near it — a clean write path that acked before it persisted, and a phone call I can still feel from the customer it locked out. So I recognized it on sight. That's the only reason I caught it. The model didn't flag it, because nothing about the code looks wrong — it looks like the work of someone careful. The bug isn't in the syntax. It's in the ordering, and the ordering reads fine.

Here's the part that should bother you more than it bothered me in the moment: two lines reordered. That's the fix. The most dangerous diff of the weekend and the cheapest to repair — if, and only if, someone in the loop was actively asking "what happens on the retry?" instead of admiring the clean code.

The thing I changed — and it's not "review the AI's output more carefully"

The obvious lesson is "read what the model writes." True, useless. I did read it. I caught this one because I'd personally been set on fire by it once; I will not catch the next one that burned somebody else. "Be more careful" is not a system. It's a hope, and hope doesn't scale past the bugs you happen to have scar tissue for.

The real lesson is structural, and it's embarrassingly simple:

The thing that writes the code cannot be the thing that blesses the code.

When I ask one model to produce a diff and tell me if it's safe, I'm asking the author to grade their own homework, in the same confident voice they use for everything. It wrote the ack-before-persist handler and it would have told me the handler was correct, because from the inside of "I just produced this," it is correct — it compiles, it matches the request, it looks like care. Self-assessment is exactly the thing an eager author is worst at.

So I stopped doing that. Now the model that generates the diff is never the one that signs off on it. There's an author pass whose job is to produce — fast, cheap, endless, genuinely great at the thing that got easy. Then there's a separate skeptic pass whose entire job is to try to break that diff rather than admire it — prompted not for "is this good" but for "here is a diff, find the input that makes it lose money." Asked that way, about that handler, the retry case is the first thing it reaches for. And then there's a human on the merge button — me — who owns the call and can see the blast radius neither pass can, because I'm the one who knows a locked-out customer is a phone call and not a log line.

Author, skeptic, human. The author has infinite throughput and I've learned to measure it by almost nothing. The skeptic produces zero code and it's the most valuable seat in the system. That's not a philosophy I read somewhere — it's the shape the weekend beat into me, when the author handed me a beautiful diff that would have cost me a customer and had no idea it had done anything wrong.

What a weekend actually buys you (and what it doesn't)

Since I'm being honest about the rest of it: a weekend gets you a prototype that proves the core loop works. It does not get you a business. By Sunday night I had a thing that did the one valuable action end to end, for me, on my machine, with my data. That's real and it's worth something — it's the difference between an idea and an artifact you can put in front of a person.

What the weekend did not produce, and what no weekend produces: the error states, the empty states, the "what happens when the input is garbage," the billing edge cases, the abuse vectors, the onboarding, the second user who doesn't think like you, the thing that stays up at 2am. That's not weeks of work, it's months, and it's most of the actual product. The machine compressed the part that was already the easy part and left the hard part exactly as hard as it always was — arguably harder, because now there's more confident, plausible code to be wrong in.

So when you read "I built a SaaS in a weekend," translate it to "I built a convincing prototype of the core loop in a weekend, and the machine made the typing cheap enough that I could spend the whole time on whether the thing was right instead of whether it existed." That's the actual gift. It's smaller than the headline and much more useful.

Why that weekend is the entire architecture of what I build now

That near-miss on Sunday wasn't a footnote to the project. It became the project.

The thing I'm building, xenition, is an agent platform, and the whole industry right now wants to cheer for the part that produces — look how much it ships, look how clean, look how fast. But a generator is just doing the skill that stopped being scarce, at a scale no human can match, and then signing off on its own work in the same even voice whether the diff is a masterpiece or the handler that eats a payment. So the entire shape of the product is the thing my weekend taught me: never let the author bless its own output. An author that generates, a skeptic whose only job is to attack what the author made, and a human who owns the merge. The abundant thing, the scarce thing, and the person accountable for the call.

I didn't set out that weekend to find a product architecture. I set out to see how far Claude and Next.js could get me by Monday. The answer was: all the way to a working prototype, and one reorder-two-lines bug away from shipping a disaster that looked exactly like success. Which turned out to be the most valuable thing the weekend could have shown me.

Top comments (17)

Collapse
 
sgaggjhkjh profile image
Arjun Sharma •

That webhook story — acking the 200 before the write, fixed by two lines reordered — is the scariest-cleanest bug anecdote I've read in a while, and the author/skeptic/human split is one I'm stealing for my own reviews.

Collapse
 
infoinlet1 profile image
Info Inlet •

Steal away — that's what it's for 😄 The part that surprised me most: the skeptic pass adds almost no friction once it's a separate step. The hard bit was psychological — admitting the model that just impressed me shouldn't also be the one I trust to bless it. If you try it, that one question — "what happens on a retry?" — punches way above its weight. Would love to hear how it lands in your reviews.

Collapse
 
sgaggjhkjh profile image
Arjun Sharma •

The retry question goes on my review checklist today — I'll report back the first time it catches something the skeptic pass missed.

Thread Thread
 
infoinlet1 profile image
Info Inlet •

Love it — a checklist line is exactly the right home for it, because it survives the days you're tired and inclined to trust the clean diff. That's the whole point of making it structural instead of "be careful."

One small tip from running it a while: when it does catch something, write down the one-line question that caught it, not just the bug. The checklist grows by questions — "what happens on a retry?", "what happens when this is called twice?", "what happens when the input is empty?" — and each one you add is a class of bug you never have to rely on scar tissue for again.

Looking forward to the report-back. 🙏

Thread Thread
 
sgaggjhkjh profile image
Arjun Sharma •

Capturing the question rather than the bug is a great framing. It grows the checklist into a set of invariants instead of an incident log, which is exactly what keeps it useful when the team changes.

Collapse
 
mudassirworks profile image
Mudassir Khan •

the ack before persist ordering is the one that bites everybody once. we got hit by the flip side right after fixing it: the provider retried during a slow write and we processed the same payment event twice, so a unique constraint on the event id ended up doing more work than the handler logic.

and yeah the model cannot see that class of bug because the diff reads fine, it only shows up in a failure trace.

did your skeptic pass get the failure scenarios as input, or just the diff?

Collapse
 
infoinlet1 profile image
Info Inlet •

You and Julian landed on the exact same spot from different scars — the unique constraint on the event ID quietly doing more than the handler logic. That's two independent "it bit us once" votes in one thread, which is about as strong a signal as this stuff gets. The lost write and the double-process are the two faces of the same retry, and only one of them is fixed by reordering.

To your direct question — honestly, that weekend the skeptic got just the diff. There were no failure scenarios to feed it, because the whole finish line lived in my head; that's precisely why catching it still depended on my own scar tissue rather than the system. Which is the weakness a couple of other folks in this thread pushed on too.

Where I've moved since: the skeptic works far better with the failure scenarios as explicit input — but written before the build, as the acceptance condition for the stage ("a failed write leaves the provider retrying and every customer ends with exactly one record"). Then the skeptic isn't free-associating about what might go wrong; it's checking the diff against a claim that must hold, and "what about the retry that fires twice?" falls straight out of "exactly one record." Diff-only is the weak version. Diff-plus-a-written-invariant is the one that would've caught this without me having been burned first.

And your last point is the one I'd underline for everybody: it only shows up in a failure trace, never in the diff — which is exactly why the author can't be the one to bless it. Great question. 🙏

Collapse
 
parsaaftabi profile image
Parsa Aftabi •

the unique constraint ending up as the real handler is the part that gets me. once the write is slow enough to trigger a retry, that one insert is the entire gate.

Collapse
 
infoinlet1 profile image
Info Inlet •

Right — and the part that makes it elegant is that the constraint is the only gate that holds no matter how the handler misbehaves. The handler can reorder, retry, run twice concurrently, get refactored by someone who's never heard of this thread — and the second insert still slams into the constraint and fails. Every other guardrail is a promise the code has to keep on every path; the unique index is a promise the database keeps for you, once.

The bit worth saying out loud: under a slow write, those retries can land concurrently, so two inserts race for the same event ID at the same instant. That's exactly the case handler-level "check then write" logic loses — both reads see nothing, both proceed. The constraint is the one thing that still holds under the race, because the DB serializes it for you. One winner, one conflict you catch and no-op.

So yeah — once you're in retry territory, that single insert is the handler. The rest is bookkeeping around it. Good eye. 🙏

Thread Thread
 
parsaaftabi profile image
Parsa Aftabi •

check-then-write is where I see this break most in reviews. two reads come back empty, both proceed, and the code looks correct the whole time.

Collapse
 
contentclips_st profile image
ContentClips •

The honest breakdown framing is refreshing. My 2 cents from shipping client deliverables on tight turnarounds: the bottleneck in a weekend build is almost never the code - it is the transition moments: environment setup, deployment config, docs. Claude handles code generation brilliantly, but I have learned to budget the remaining 20% of the time for the plumbing that makes a thing actually shippable: CI, a real landing page, searchable docs. Buyers judge "shipped" by what they can click, not by what runs on localhost. Nice writeup.

Collapse
 
infoinlet1 profile image
Info Inlet •

"Buyers judge 'shipped' by what they can click, not by what runs on localhost" — that's a keeper, and it's the honest counterweight to my whole post. I spent 2,000 words on correctness because that's the bug that scared me, but you're pointing at the thing that actually kills most weekend projects: they never cross the gap from "runs on my machine" to "a stranger can touch it."

And I think the two points are the same point wearing different clothes. The plumbing — CI, deploy config, a real landing page, searchable docs — is exactly the stuff that's unglamorous, has no clean diff to admire, and so is the first thing the "I built a SaaS in a weekend" genre quietly skips. The ack-before-persist bug and the missing CI pipeline are both the hard part hiding behind the part that got easy. Code generation got cheap; everything around the code did not.

The one thing I'd add to your 20% budget from the correctness side: the plumbing is also where you install the skeptic cheaply. CI is the natural home for "run the adversarial check on every diff" — so the same pass that makes it clickable can be the pass that makes it right. Fold them together and the 20% buys you both.

Appreciate the field notes from actual client turnarounds — that's the view the weekend-flex posts never show. 🙏

Collapse
 
contentclips_st profile image
ContentClips •

Folding them together is the right instinct, with one guard: the adversarial pass and the deploy check need to run against the same artifact, otherwise you rebuild the classic 'green CI, broken deploy' split one level up — build the bundle once and feed both passes from that single artifact. Second point: make the skeptic emit a report artifact instead of a bare pass/fail. A pass/fail answer goes stale in a week and someone eventually deletes the job for slowing the pipeline; a report that shows what it challenged and why is what survives the budget review. And budget a slice of the 20% for the skeptic's own feedback loop — every false positive is a direct tax on the pipeline's credibility, and unpriced credibility taxes are how these checks get quietly turned off.

Collapse
 
ascendra_ventures_92c21f2 profile image
Ascendra Ventures •

The ack-before-persist handler is a perfect example, because nothing about it looks wrong. It compiles, it has a thoughtful comment, it passes the test the same model wrote, and it still loses a paying customer on the one bad night.

"The thing that writes the code cannot be the thing that blesses the code" is the line I would keep. The author, skeptic and human split works for client work too, with one addition: the skeptic needs something fixed to test against. If the finish line for the payment stage is written before the build, for example a failed write leaves the provider retrying and every paying customer has a record, the skeptic pass becomes a check against that list rather than an open hunt.

Your list of what a weekend does not buy, the empty states, the billing edge cases, the second user, is what founders need to read before asking how long the rest will take. How do you prompt the skeptic so it does not simply agree with the author?

Collapse
 
infoinlet1 profile image
Info Inlet •

You've put your finger on the thing that makes the skeptic actually work versus theater, so let me take both parts.

The spec point is exactly right, and it's the upgrade I'd make to the post if I rewrote it. An open-ended "find the bug" skeptic is weaker than one handed a finish line written before the build — "a failed write leaves the provider retrying and every paying customer ends with a record." Now the skeptic isn't hunting in the dark; it's checking the diff against a claim that must hold. Your framing — write the acceptance condition for the payment stage first, then the skeptic verifies against it — is better than what I did that weekend, where the finish line lived only in my head. Written-down beats remembered, every time.

On your direct question — how to stop the skeptic just agreeing — three things that moved the needle for me:

  1. Change the job, not the tone. Don't ask "is this correct?"k "here is a diff that is assumed to have a bug that losesmoney; produce the input sequence that triggers it." The presupposition does the work. It's no longer allowed to conclude "looks good"; it has to produce a failing case or explicitly argue why none exists.
  2. Separate the context. The skeptic shouldn't see the author's reasoning or reassuring comments — those are precisely what lull you. Give it the diff and the spec, nothing else. A fresh pass with no investment ig to call it broken.
  3. Make it adversarial by role, not by adjective. "Be critical" gets you a cosmetic nitpick. Framing it as a named failure mode — "you are the retry that fires 3 seconds after the ack; walk the handler" — gets versary beats abstract skepticism.

The honest limit: even prompted perfectly it won't catch evern stays on the merge button. But the gap between "is thisgood?" and "find the input that makes it lose money" is most of the value, and it's free.

And yes — that "what a weekend doesn't buy" list is the one I wish founders read before the "how long for the rest?" question. The rest is the product. Thanks for the sharp comment. 🙏

Collapse
 
julianneagu profile image
Julian Neagu •

The duplicate payment angle matters just as much as the lost payment. I’d make the event ID unique at the DB level too. Better to let the database enforce it than trust the handler to get every retry right.

Collapse
 
infoinlet1 profile image
Info Inlet •

This is the correct instinct and it's the deeper fix — thank you for pushing it one level down. My post stopped at "persist before you ack," but that only solves the lost write. You're pointing at the other half: once the provider retries, you process the same event twice, and now you've granted the entitlement twice or double-counted the payment. Reordering two lines does nothing for that.

And your framing of where to enforce it is the real lesson. A unique constraint on the event ID makes idempotency a property of the schema, not a promise the handler has to keep perfectly on every path. The handler can be sloppy, the retry can fire five times, a future refactor can reorder things again — and the second insert just fails on the constraint instead of silently duplicating. You catch the conflict and no-op. The invariant lives in the one place that can't be talked out of it.

Which, pleasingly, is the same lesson as the whole post from a different angle: don't trust the confident author — in this case the handler — to get every case right. Push the guarantee somewhere that physically cannot be charmed into agreeing. The DB constraint is the skeptic that never sleeps.

Great addition. 🙏