<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jason Michael</title>
    <description>The latest articles on DEV Community by Jason Michael (@builtbyjason).</description>
    <link>https://dev.to/builtbyjason</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066352%2Fcbb30880-2b1c-47a8-b6ec-793ffb57873f.jpg</url>
      <title>DEV Community: Jason Michael</title>
      <link>https://dev.to/builtbyjason</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/builtbyjason"/>
    <language>en</language>
    <item>
      <title>Sereinly: Building a "Nothing Persists" Product on Top of a Backend That Necessarily Persists Some Things</title>
      <dc:creator>Jason Michael</dc:creator>
      <pubDate>Thu, 13 Aug 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/builtbyjason/sereinly-building-a-nothing-persists-product-on-top-of-a-backend-that-necessarily-persists-some-2401</link>
      <guid>https://dev.to/builtbyjason/sereinly-building-a-nothing-persists-product-on-top-of-a-backend-that-necessarily-persists-some-2401</guid>
      <description>&lt;p&gt;&lt;em&gt;The Nuance in "We Don't Store Your Messages" That's Worth Being Precise About&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sereinly was marketed around a privacy promise: what you paste in doesn't get stored. I want to be precise about what that actually meant architecturally, because "we don't store anything" is a claim that's easy to overstate if you're not careful about which layer of the system you're talking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What definitely is stored
&lt;/h2&gt;

&lt;p&gt;The product needed accounts, subscriptions, and usage limits to function as a paid SaaS. Supabase-backed auth sessions, a &lt;code&gt;profiles&lt;/code&gt; table tracking plan type and validity, and a separate usage-tracking layer (for things like remaining session minutes on a metered plan) all persist by necessity — a subscription product can't function without knowing who's paying for what and how much they've used.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the "no storage" claim was actually about
&lt;/h2&gt;

&lt;p&gt;The claim was scoped to message content specifically — what you paste into Ely, Sera, or Milo to get help with. The architecture routed AI processing through server-side API routes rather than a client-side service calling the AI provider directly (an earlier client-side implementation was explicitly deprecated in favor of this, for security reasons).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where my confidence gets honest
&lt;/h2&gt;

&lt;p&gt;Here's where I want to be careful rather than assert more than I can verify: I can confirm the system separates account/usage data (which does persist) from the AI-processing pathway (routed server-side), and I can confirm there's a dedicated &lt;code&gt;usage&lt;/code&gt; tracking layer that appears scoped to metering (minutes, plan status) rather than content. What I haven't directly traced is the full request lifecycle inside the server-side API routes to confirm message content is never written to a log, a database row, or a request log anywhere along that path, even transiently.&lt;/p&gt;

&lt;p&gt;That's a meaningful gap, and I think it matters to name it rather than repeat a marketing claim with more technical confidence than I've actually verified. "We don't store your messages" is a strong, specific claim, and if I were auditing this system before making that claim publicly again, I'd want to trace the full server-side path end to end, check request/error logging configuration, and confirm no AI provider we're calling itself retains inputs on their end by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this distinction matters generally
&lt;/h2&gt;

&lt;p&gt;I think this is a common gap in how privacy claims get made across the industry generally, not specific to this one product: "we don't store your data" often means "we don't store it in the primary application database," while logging infrastructure, error tracking tools, and third-party API providers can each retain data through separate paths that a simple "no storage" claim doesn't actually cover. A precise privacy claim needs to account for every layer data passes through, not just the one the product team is thinking about when they write the marketing copy.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Vows: A Tiered Module System for White-Labeling Wedding Sites</title>
      <dc:creator>Jason Michael</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/builtbyjason/vows-a-tiered-module-system-for-white-labeling-wedding-sites-18pe</link>
      <guid>https://dev.to/builtbyjason/vows-a-tiered-module-system-for-white-labeling-wedding-sites-18pe</guid>
      <description>&lt;p&gt;&lt;em&gt;Why Each Pricing Tier Is a Separate Code Module, Not a Feature Flag&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Vows by Surihana offers multiple product tiers — I've seen "elegant," "luxury," "premium," and "squad" as distinct modules in the codebase. I want to talk through the architectural choice of building tiers as separate modules rather than a single codebase with feature flags gating access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two approaches
&lt;/h2&gt;

&lt;p&gt;The common pattern for tiered products is one codebase, with feature flags or subscription checks gating which UI and logic paths a given user can access. It's simpler to maintain in one sense — one deploy, one set of components, conditional rendering based on tier.&lt;/p&gt;

&lt;p&gt;Vows instead structures each tier as its own module — for example, the "elegant" tier has its own &lt;code&gt;invitation-engine&lt;/code&gt;, &lt;code&gt;rsvp-system&lt;/code&gt;, &lt;code&gt;guest-links&lt;/code&gt;, &lt;code&gt;event-display&lt;/code&gt;, and &lt;code&gt;sender-profiles&lt;/code&gt; files, distinct from whatever the other tiers implement for the same concerns. "Squad" — the wedding-party coordination tier — has its own dedicated &lt;code&gt;squad-system&lt;/code&gt; module entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why separate modules, given the added overhead
&lt;/h2&gt;

&lt;p&gt;I can't claim certainty about every reason behind this specific architectural choice without more direct confirmation, but based on the observable structure, a few real advantages stand out. Separate modules mean a change to the luxury tier's RSVP flow can't accidentally break the elegant tier's — there's no shared conditional logic where a tier-specific edge case has to be reasoned about against every other tier simultaneously. It also means a tier can be retired, or majorly redesigned, without touching code that other tiers depend on.&lt;/p&gt;

&lt;p&gt;The real cost is duplication — if all four tiers need a bug fix to, say, how RSVP counts are calculated, that fix potentially needs to be applied four separate times rather than once. This is a genuine tradeoff, and I think it's the right one specifically for a product where the tiers are meant to feel meaningfully different from each other (a "squad" coordination tool is a different problem from a "luxury" tier's presentation logic) rather than the same core product with cosmetic differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I'd choose the other approach
&lt;/h2&gt;

&lt;p&gt;If the tiers here were primarily differentiated by things like storage limits or a handful of gated UI elements — the more typical "free vs. pro" pattern — I think a single codebase with feature flags would be the better call; the products aren't different enough to justify separate modules. The module-per-tier approach earns its complexity specifically when the tiers represent genuinely different feature sets and user needs, not just different access levels to the same feature set.&lt;/p&gt;

&lt;p&gt;Curious how others have drawn this line — at what point does tier differentiation become significant enough to justify separate modules over flags within one codebase?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Finding Astro: Rate-Limiting Abuse Reports Without Silencing Real Ones</title>
      <dc:creator>Jason Michael</dc:creator>
      <pubDate>Tue, 11 Aug 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/builtbyjason/finding-astro-rate-limiting-abuse-reports-without-silencing-real-ones-3c62</link>
      <guid>https://dev.to/builtbyjason/finding-astro-rate-limiting-abuse-reports-without-silencing-real-ones-3c62</guid>
      <description>&lt;p&gt;&lt;em&gt;Four Layers to Stop a Report System From Becoming a Weapon&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Any platform that lets users flag other users for review creates a dual-use tool: it protects genuine victims, and it can be weaponized by bad actors against people doing nothing wrong. I want to walk through the specific layered defenses Finding Astro uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: account age gating
&lt;/h2&gt;

&lt;p&gt;Accounts under 30 days old cannot file abuse or conflict reports — reports from new accounts are quarantined for manual review instead of processed automatically. This targets a specific attack pattern: creating a fresh account purely to file a retaliatory report, then discarding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: velocity limiting
&lt;/h2&gt;

&lt;p&gt;More than 3 reports in a single hour from the same source triggers an automatic, temporary block. This is a straightforward rate limit, but it's calibrated around a real behavioral assumption: a genuine user filing multiple legitimate reports in an hour is rare; a bad actor spamming reports to overwhelm a target is the more common cause of that pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: geographic clustering detection
&lt;/h2&gt;

&lt;p&gt;More than 3 reports within 500 meters of each other within a week gets flagged as possible coordinated harassment, rather than treated as three independent, unrelated concerns. This catches a pattern the first two layers miss entirely: multiple different accounts, each individually below the velocity threshold, coordinating against the same target or location — a pattern that's invisible if you only look at each report in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: a credibility score with asymmetric weighting
&lt;/h2&gt;

&lt;p&gt;Every user carries a credibility score starting at 100. A report an NGO later confirms as false costs the reporter 15 points. A report confirmed accurate adds 5. The penalty for a bad-faith report is 3x the reward for a good one.&lt;/p&gt;

&lt;p&gt;That asymmetry is deliberate, not an oversight. If the penalty and reward were equal, a bad actor filing 10 false reports and having 1 confirmed true would net out roughly even. Weighting the penalty heavier makes bad-faith reporting a losing strategy over any meaningful volume, without requiring every single report to be manually reviewed before action is possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why four layers instead of one strong one
&lt;/h2&gt;

&lt;p&gt;Each layer catches a different attack pattern, and none of them alone would be sufficient. Account-age gating does nothing against an established account. Velocity limiting does nothing against a slow, patient bad actor filing one report a day. Geographic clustering does nothing against a single-account, single-report false accusation. The credibility score does nothing to prevent the first report from causing damage — it only shapes incentives after the fact.&lt;/p&gt;

&lt;p&gt;Layering catches more of the pattern space than any single mechanism would, but it also means four separate systems to reason about, maintain, and tune — a real complexity cost against a real abuse-prevention benefit.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Finding Astro: Geo-Validating a Return Event Without Trusting Self-Reported Location</title>
      <dc:creator>Jason Michael</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/builtbyjason/finding-astro-geo-validating-a-return-event-without-trusting-self-reported-location-3pml</link>
      <guid>https://dev.to/builtbyjason/finding-astro-geo-validating-a-return-event-without-trusting-self-reported-location-3pml</guid>
      <description>&lt;p&gt;&lt;em&gt;Catching Illegal Animal Relocation With a 1.5km Distance Check — and Why the Threshold Isn't Arbitrary&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Under India's ABC Rules 2023, sterilized street dogs must be returned to their original capture location — moving them elsewhere is illegal, and historically very hard to prove or catch. I want to walk through the actual validation logic Finding Astro uses to flag suspicious returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;Every capture event is logged with GPS coordinates at the moment it happens. Every corresponding return event is checked against that original location. If the return point is more than 1.5 kilometers from the capture point, the system flags the case as &lt;code&gt;geo_validated = false&lt;/code&gt; and alerts the NGO overseeing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 1.5km, not 0km
&lt;/h2&gt;

&lt;p&gt;A zero-tolerance threshold — flag any distance mismatch at all — would generate constant false positives. GPS accuracy on consumer mobile devices realistically varies by tens to low-hundreds of meters depending on conditions (urban canyon effects, device quality, indoor/outdoor transitions during logging). A rescuer might reasonably capture a dog at one end of a street and release it at the other end of the same territory — a legitimate, non-violation movement of a few hundred meters that a stricter threshold would incorrectly flag.&lt;/p&gt;

&lt;p&gt;1.5km is calibrated to be well beyond realistic GPS noise and well beyond a dog's own reasonable territory range, while still being tight enough to catch genuine relocation — moving an animal to a different neighborhood or a different part of the city entirely, which is the actual pattern the rule exists to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the flag does and doesn't do
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;geo_validated = false&lt;/code&gt; flag doesn't automatically accuse anyone of wrongdoing — GPS error, a phone with no signal at the moment of logging, or a legitimate edge case could all produce a false flag. What it does is surface the case for human review by the overseeing NGO, rather than either silently accepting all returns as valid or silently blocking anything outside the threshold. The system is designed to make illegitimate patterns visible, not to autonomously adjudicate them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it looks like it should
&lt;/h2&gt;

&lt;p&gt;Before this kind of system existed, "was this dog actually returned to its territory" was effectively an unverifiable claim — you had only the rescuer's or authority's word for it. Making it a checkable data point, even an imperfect one, changes the incentive structure: a pattern that was previously invisible and unprovable becomes visible and reviewable.&lt;/p&gt;

&lt;p&gt;I think a lot of accountability problems in under-digitized systems come down to exactly this: not bad actors specifically, but the complete absence of any record that could distinguish a bad actor from a good one. Sometimes the fix isn't a sophisticated model — it's just making a previously unrecorded fact recordable and checkable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Bonds: Encoding a Trust Model as Two Measurable Signals Instead of a Subjective Score</title>
      <dc:creator>Jason Michael</dc:creator>
      <pubDate>Sun, 09 Aug 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/builtbyjason/bonds-encoding-a-trust-model-as-two-measurable-signals-instead-of-a-subjective-score-66l</link>
      <guid>https://dev.to/builtbyjason/bonds-encoding-a-trust-model-as-two-measurable-signals-instead-of-a-subjective-score-66l</guid>
      <description>&lt;p&gt;&lt;em&gt;We Gate Dating App Features Behind Message Count + Time, Not a "Trust Score"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A lot of trust/safety systems in consumer apps rely on some form of opaque scoring — a number the platform computes from signals the user can't fully see or verify. For Bonds' progression system, I wanted something different: gates defined entirely by two numbers both users can, in principle, see and understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;

&lt;p&gt;Eight stages, each unlocked by a specific combination of mutual message count and elapsed time. Stage 2 (voice notes) needs 20 mutual messages and 24 hours. Stage 3 (an invite into a deeper "Focus" mode) needs 75 mutual messages and 3 days. Stage 5 (video sharing) needs 7 days specifically spent inside an active Focus period. Later stages layer in additional state (whether Focus or a shared "Couple Space" is active) alongside the message/time signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why two signals, not one
&lt;/h2&gt;

&lt;p&gt;A single signal is easy to game. If the only gate is "20 messages," a bad actor sends 20 rapid, low-effort messages in five minutes and unlocks the same access as two people who spent real time getting to know each other. If the only gate is "24 hours," time passing alone — with zero actual conversation — unlocks the same thing.&lt;/p&gt;

&lt;p&gt;Combining both raises the cost of gaming either one alone. You can't compress 24 real hours into less time, and you can't fake message count without actually typing messages, which itself takes non-trivial effort at 20-75 messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not a computed trust score instead
&lt;/h2&gt;

&lt;p&gt;I considered a single aggregate trust score — something that could also factor in report history, account age, response patterns, etc. I chose not to, for this specific system, because I wanted the gates to be legible: a user should be able to know exactly why a feature is locked and exactly what unlocks it, without wondering what an opaque internal number is currently sitting at.&lt;/p&gt;

&lt;p&gt;There's a real cost to this choice — a computed score could theoretically capture more nuance (report history, verified identity signals, etc.) than two raw numbers can. I think legibility was worth that cost for this specific feature, but I'd frame it as a genuine tradeoff, not a strictly superior approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest gap
&lt;/h2&gt;

&lt;p&gt;Stage 8 (video calls) requires Couple Space plus what the codebase documents as an "additional flag" beyond message count and time. I don't have full visibility into exactly what that check verifies, and I'd rather say that plainly than describe a mechanism with more confidence than I actually have.&lt;/p&gt;

&lt;p&gt;Would be interested in how others have approached the legible-gates-vs-computed-score tradeoff in trust/safety systems — particularly whether legibility held up as user bases scaled and bad-actor sophistication increased.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Pulza: Splitting a Monorepo Along a Compliance Boundary, Not Just a Feature Boundary</title>
      <dc:creator>Jason Michael</dc:creator>
      <pubDate>Sat, 08 Aug 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/builtbyjason/pulza-splitting-a-monorepo-along-a-compliance-boundary-not-just-a-feature-boundary-j6l</link>
      <guid>https://dev.to/builtbyjason/pulza-splitting-a-monorepo-along-a-compliance-boundary-not-just-a-feature-boundary-j6l</guid>
      <description>&lt;p&gt;&lt;em&gt;We Split Our Turborepo Apps by HIPAA Exposure, Not by Team Ownership&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most monorepo app-splitting advice centers on team ownership or deploy cadence. For Pulza, the deciding factor was different: which apps might touch protected health information (PHI), and which provably never do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure
&lt;/h2&gt;

&lt;p&gt;Pulza is a Turborepo + pnpm workspace monorepo. &lt;code&gt;apps/web&lt;/code&gt;, &lt;code&gt;apps/portal&lt;/code&gt;, and &lt;code&gt;apps/marketing&lt;/code&gt; are Next.js 15 apps on Vercel. &lt;code&gt;apps/workers&lt;/code&gt; is a separate Node/TS service running background jobs (via pg-boss over Postgres) on Railway. Database access goes through Drizzle ORM, with &lt;code&gt;packages/db&lt;/code&gt; as the single schema source of truth. Auth is Supabase Auth plus Row-Level Security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why workers live on a different platform entirely
&lt;/h2&gt;

&lt;p&gt;This wasn't a scaling decision — it's a compliance boundary decision, documented explicitly in the project's architecture notes as the "PHI boundary."&lt;/p&gt;

&lt;p&gt;Each infrastructure vendor has its own path to HIPAA compliance via a Business Associate Agreement (BAA), and each is priced and gated differently: Vercel's BAA is a Pro-plan add-on, Supabase requires a Team plan plus a separate HIPAA add-on, and Railway requires a committed Enterprise spend to unlock BAA coverage at all. None of these are active yet in Pulza's current build phase.&lt;/p&gt;

&lt;p&gt;Because Railway's BAA tier isn't funded, &lt;code&gt;apps/workers&lt;/code&gt; is deliberately restricted to non-PHI jobs only — a constraint enforced by discipline and documentation right now, not yet by a hard technical barrier, though that's worth flagging as a real limitation of the current approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff this creates
&lt;/h2&gt;

&lt;p&gt;Any background job that would need to touch a client record — a reminder about an upcoming session, say — can't run on &lt;code&gt;apps/workers&lt;/code&gt; yet, because that would mean PHI flowing through infrastructure that isn't compliance-covered. So those jobs either wait for the Railway tier to be funded, or need to be redesigned to avoid touching PHI directly (e.g., triggering a notification without the job itself reading the sensitive payload).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this is the right call, with a caveat
&lt;/h2&gt;

&lt;p&gt;Segmenting compliance-sensitive infrastructure from general-purpose infrastructure, before you have real compliance-sensitive data flowing through the system, is cheaper than retrofitting it under pressure later. I'd rather over-architect this boundary early than discover, post-launch, that a "small" background job has been quietly processing PHI on infrastructure that was never covered for it.&lt;/p&gt;

&lt;p&gt;The caveat: right now this boundary is enforced by team discipline and code review, not by an automated technical guardrail that would catch a violation before it ships. That's a real gap I'm aware of, not a solved problem — and I'd be interested in how others have automated enforcement of similar data-boundary rules within a monorepo, rather than relying on developers remembering the rule.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Korum: Detect-and-Refund vs. Prevent — A Concurrency Tradeoff</title>
      <dc:creator>Jason Michael</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:44:40 +0000</pubDate>
      <link>https://dev.to/builtbyjason/korum-detect-and-refund-vs-prevent-a-concurrency-tradeoff-1250</link>
      <guid>https://dev.to/builtbyjason/korum-detect-and-refund-vs-prevent-a-concurrency-tradeoff-1250</guid>
      <description>&lt;p&gt;&lt;em&gt;Why I Didn't Try to Make Double-Booking "Impossible" — I Made It Cheap to Reverse&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A common instinct when building any capacity-constrained booking system (event slots, appointment times, seats) is to reach for a hard database constraint that makes overbooking structurally impossible. I want to walk through why I didn't do that for Korum, and what I built instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Korum coordinates casual sports matches. A match has a fixed capacity. When it's full, further joins should fail cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "impossible" would actually require
&lt;/h2&gt;

&lt;p&gt;Making concurrent overbooking truly impossible under high contention generally means serializing writes at the database level — something like a unique constraint on a (match_id, slot_number) pair with slot numbers assigned atomically, or row-level locking on the match capacity row during the write. Both are legitimate approaches. Both also add real complexity: lock contention under load, or careful slot-assignment logic that has to be right in every code path that touches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built instead
&lt;/h2&gt;

&lt;p&gt;Application code performs a capacity check before creating a payment hold — a soft check, not a guarantee. The actual enforcement happens through database routines that confirm a paid participant atomically; if an overflow case slips through the soft check anyway, it's caught at confirmation time and the payment is marked for refund review, rather than silently accepted as a valid booking.&lt;/p&gt;

&lt;p&gt;In plain terms: overbooking isn't prevented from ever occurring — it's detected immediately after, and reversed, rather than left for someone to discover manually later (e.g., a captain finding 12 confirmed players for an 11-player match on Sunday morning).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this tradeoff, specifically
&lt;/h2&gt;

&lt;p&gt;The cost of a failure matters as much as the probability of it. In this domain, the failure mode of "briefly overbooked, then refunded" is low-cost: an apology, a refund, and a fix, versus the engineering cost of building and maintaining stricter locking for what's a genuinely rare race condition given realistic traffic patterns for a weekend-sports coordination app.&lt;/p&gt;

&lt;p&gt;I'd build this differently for something with a higher failure cost — a single-seat airline booking, say, where "sorry, we double-sold your seat" isn't an acceptable outcome even rarely. For Korum, detect-and-refund is the right tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: idempotent webhook handling
&lt;/h2&gt;

&lt;p&gt;Payment confirmations arrive via a Razorpay webhook, and webhooks aren't guaranteed exactly-once — network retries can cause the same event to fire more than once. The handler verifies incoming signatures and writes confirmations in a way designed to be safe under repetition: a duplicate delivery of the same event doesn't produce a duplicate effect.&lt;/p&gt;

&lt;p&gt;Curious how others have reasoned about prevention vs. detection-and-recovery tradeoffs for similar capacity-constrained systems — particularly where the "acceptable failure cost" line sits for you.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
