Build1 publisher3 min readPublished
A webhook-only writer paywalled a customer who had already paid, because the browser redirect beat the event by seconds. The fix was making every write idempotent on Stripe's own identifier, not picking the right writer.
The Engineer · Build desk

build
One Self-Hosted Setup Cuts a $198 Platform Bill to a $24 Droplet1 publisher
build
With CRA out of React's docs, the new project default is a rendering decision1 publisher
build
106 design engineers report a €115,000 median. The salary sites are pricing a different job.1 publisher
build
Cold starts empty the module-level Map that the API used as its cache1 publisher
Compiled by The EngineerSomething wrong?How this is made
Here, last write wins is the correct policy, and the reason is narrow enough to write down as a precondition: each of the five writers puts down values it read from Stripe moments before writing, so whoever wrote last is also whoever asked the authoritative system last [13]. The row is one answer, fetched five different ways, not a merge of five opinions about the customer.
That precondition is where I would push on the design. A webhook handler is the one writer that does not naturally re-read, because the delivered event describes the object as of the event. VoiceDash's developer, writing on dev.to, says all five paths write values read from Stripe a moment earlier [13], which is the version of the pattern that holds. Copy it without that, let the handler trust its payload, and a retried delivery can install stale status on top of a row the read-time reconcile just refreshed. The unique key retires duplicate rows, not ordering.
The constraint doing the load-bearing work is one schema line: `stripeSubscriptionId` is `@unique` [11], so the upsert's `where` clause targets the row by the external system's identifier [12]. That is the whole difference between cheap redundancy and an incident. Insert keyed on your own cuid instead, and five writers give you five rows [14].
The client-side poll is the part I liked most. `/api/stripe/verify-session` answers `202` with `retry: true` when Stripe reports the checkout complete but no subscription attached yet, and the browser retries up to five times, two seconds apart [8]. That is a ten second budget [16]. The lag that caused the original lockout was a couple of seconds [3]. So the retry window covers the observed gap with margin, and it covers it in the browser, where the customer is already staring at the screen waiting to be let in.
Then the bill. `syncSubscriptionFromStripe` runs on render of the agency layout and lists the customer's subscriptions [10]. Every agency page view is a Stripe API round trip before the layout resolves. The writeup describes no TTL, no freshness check against `currentPeriodEnd`, and no short-circuit when the cached row is comfortably active, which is the first optimisation I would want and the one that is easiest to postpone until someone complains about page loads.
There is a second cache underneath, and it is graded. The workspace row carries a denormalized `plan` column that the limit checks read, so counting clients against a plan does not require a join to billing, while access is gated on the subscription row itself [15]. Staleness in the plan column costs a wrong limit. Staleness in the subscription row costs a lockout [2]. Different tolerances, different reads.
Transfer conditions are specific. Stripe hands you a stable unique id you cannot mint yourself, the cached state is a handful of scalar fields [11], and refetching is a cheap list call [10]. Trial extensions you grant by hand, or seat entitlements your own code computes, have no upstream to re-read. For those your table really is the record, and the tutorial advice of one canonical writer is right again.
Ranked by verification strength, evidence, and original report placement.
A read-time reconcile calls syncSubscriptionFromStripe on render of the agency layout: it lists the customer's subscriptions, picks the most recent one that is active, trialing, past due or unpaid, and refreshes the row.
VoiceDash is a small SaaS that gives voice AI agencies a white label portal for their own clients, built on Next.js on Vercel, Postgres behind Prisma, and Stripe for billing.
The first version of the billing code had exactly one writer of subscription state, the Stripe webhook, and it locked a paying customer out: after paying, the agency owner was redirected back into the app, the layout wrapping every agency page looked for a subscription row that was active or trialing, found none, and redirected them to the plan picker.
The webhook was not lost; it arrived a couple of seconds later than the browser redirect.
On preview deployments the webhook never arrived at all, because the endpoint registered with Stripe points at production.
The author's reframing: Stripe is where subscription state lives and his tables are a cache of it, which turns the question from why a write did not happen into when the cache is refilled and what to show while it is cold.
Publishers with included, body-backed reporting in this cluster.
1 article · September 6, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
The code is quoted, the incident is not
The mechanism at the centre of the story can be inspected: the Prisma model with its unique constraint, the shared upsert and the status filter that gates access are all reproduced in full. The events around them cannot. The lockout, the two-second gap between redirect and webhook, and the preview deployments that receive nothing rest on the author's recollection of his own logs, with no timestamps or error counts shown, and dev.to is the only place any of it appears.
One codebase, described by its author
The pattern is running in exactly one place, and the only person who reports it is the person who shipped it. Nobody else in our coverage says they use it, and the post gives no figures on how often the redundant writers actually fire or how many customers pass through the retry loop.
Modest, with one rule reaching past its example
The headline promise and the code agree: a unique constraint on Stripe's identifier really is what makes five writers harmless, and the constraint is on the page. Where the writing outruns what it can show is the closing rule that writes keyed on your own id are the dangerous case, which a single lockout in a single codebase does not establish. In the other direction the most portable idea gets the least room: when the reconcile cannot reach Stripe it logs and serves the database, choosing stale over locked out, and that trade is settled in a handful of sentences near the end.
Founder writing up his own product
VoiceDash's founder is describing VoiceDash on a platform where posts build a personal following, so the pull is toward architecture that reads as deliberate rather than improvised. Cutting against that, the piece opens by admitting he served a paywall to a customer holding a Stripe receipt, which is not the flattering way to tell this story. Stripe appears throughout purely as the vendor being paid, with nothing suggesting any relationship beyond that.
Solid on mechanics, thin on breadth
The specific technical claims hold up because they are internally consistent and partly quoted, so the ordering argument can be followed end to end. What keeps the number in the middle is reach: one author, one codebase, and no outside look at whether five writers stay harmless once subscriptions move between workspaces or Stripe starts throttling the per-render lookups.