Rebounder Tech Blog

Written by the people who actually run these systems in production.

fail-closed Guard Was Missing From Two Other Write Paths

Published About 5 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

Right after adding a fail-closed start_month guard to direct booking, kimiteras-portal found it missing from group distribution and invite issuance, which can't be recalled once sent.

The short version

kimiteras-portal’s ad slots (placement_loops) carry an invariant: a booking must never go through while start_month — the anchor for the whole contract period — is unset. A fail-closed guard had already been added to the direct booking path (bookLoopSlotCore) to fix exactly this. The very next review found that two entirely separate write paths, group distribution and invite issuance, had no such guard at all. Every write path that has to uphold the same invariant is a place the fix can be forgotten to copy.

Why direct booking alone wasn’t enough

There is more than one way to create a contracts row from placement_loops. Adding a fail-closed guard to the public-catalog direct booking path (bookLoopSlotCore) only protects the path that runs through that guard. If another function can create the same contracts row from a different entry point, that entry point is left unchecked. The follow-up review turned up exactly two of those.

Gap 1: group distribution was the easiest of all to trigger, and the customer could do it themselves

group-booking.ts’s assignLoopUnitsToMember creates a contract from a slot already allotted to a group. Before the fix, it still let the listing period (period) go through as null:

// Before the fix (sha 4f71bfae, lines 370-385)
const range = periodRange(startYm, effectiveTerm);
// ...
period_start: range ? range.start : null,
period_end: range ? range.end : null,

The ternary lets a null range pass straight through, creating the contract with period_start/period_end left null — the same shape of defect that had just been fixed on direct booking. But the group-distribution path had two conditions stacked on top that made it even harder to catch:

  • A group’s allotted slots are normally unlisted, so neither the public catalog’s filter nor the save-time guard reaches them
  • It isn’t staff who runs this — it’s the group’s own contact person, a customer, callable by anyone with access via distributeUnitsAction on /group

In other words, a customer could create a period-NULL contract purely through their own actions — a more likely path to the bug than anything an internal check would catch. The fix added the same fail-closed validation used in direct booking: refuse the distribution when the period can’t be resolved, and also delete the placement row already reserved, to roll back the reservation.

// After the fix (sha 10aab915, lines 373-387)
if (!range) {
  await admin.from("placements").delete().eq("id", placementId as string);
  return {
    ok: false,
    error: "This slot has no listing start month set, so it cannot be distributed. Please contact the operator.",
  };
}

Leaving the reservation in place while refusing to create the contract would have left an orphaned slot — reserved, but never usable by anyone. Rolling back the reservation at the same time as the refusal prevents that orphaning too.

Gap 2: invite issuance was too late to catch after the fact

loop-invite.ts’s issueLoopInvite issues invite links for referral partners and groups. Before the fix, it only checked whether the slot had an owner, and never validated start_month at all.

// Before the fix (sha 10aab915, lines 66-78)
const { data: loop } = await admin
  .from("placement_loops")
  .select("id, label, bought_out_by_company_id")
  .eq("id", params.loopId)
  .maybeSingle();
// ...
if (!loop.bought_out_by_company_id)
  return { ok: false, error: "..." };
// no start_month check here

Invite links get printed as QR codes or emailed out to the outside world. The existing safety net rejects the booking at final submission — but for whoever received the invite, that means filling in their company name, address, representative, and agreeing to the terms, only to hit a dead end at the very last step. And because the invite itself is already outside the system by the time it’s issued, there’s no recalling it after the fact. The fix validates start_month’s format at issuance time and refuses to issue the invite at all if it’s invalid.

// After the fix (sha f0c52ff, lines 67-87)
if (!isValidStartMonth(loop.start_month as string | null))
  return {
    ok: false,
    error: "This slot has no listing start month set, so an invite link cannot be issued. Please set a start month for the slot first (leaving it unset means the invited party would be rejected only after finishing their submission).",
  };

The same invariant, missed every time a write path gets added

Direct booking, group distribution, and invite issuance look nothing alike and are called from nowhere near each other. But the invariant they all have to respect is identical: never create a contract while start_month is unset. The relief of having fixed one path is exactly what lets the others slip past. This time it took two follow-up reviews — the same two people plus one new reviewer — to catch both gaps, which also means: without those extra eyes, neither would have been found. Every time a change adds a new write path, it’s worth asking whether the same invariant needs to be checked somewhere else too.

Summary

“Fixed the direct booking guard, so it’s done” was too early a call. In a system where more than one path can write the same data, fixing one path leaves the others unchecked by default. Paths like group distribution — runnable by the customer, unreachable by the public-catalog guard — or invite issuance — unrecallable after the fact — were exactly the ones that deserved a closer look before direct booking, not after. Fixing a feature with this shape means treating “find every other path that writes the same data” as part of the fix itself, not an afterthought.

Frequently asked questions

Q1The direct booking path had a guard. Why wasn't that enough?

placement_loops's contracts rows have more than one write path. A guard on the direct booking path (bookLoopSlotCore) only protects that path. Two other functions, group distribution (assignLoopUnitsToMember) and invite issuance (issueLoopInvite), could still create contracts unchecked.

Q2Why was the group distribution path especially easy to miss?

A group's allotted slots are normally unlisted, so neither the catalog filter nor the save-time guard reaches them. And it's the group's own contact person, a customer, who runs it via distributeUnitsAction on /group. It was the easiest way a period-NULL contract could get created.

Q3Couldn't the invite issuance guard have waited?

No. Invite links get printed as QR codes or emailed out, so once issued they're gone — no recalling them. The existing safety net rejects at final submission, which for an invited party means filling in company details first, only to hit a dead end. Issuance itself had to be blocked.

Q4What did the fix look like?

The same fail-closed check used in direct booking was added to both. Group distribution now refuses to distribute when the period can't be resolved, and deletes the reserved placement row to roll it back. Invite issuance refuses to issue unless start_month is valid. Both notify Slack on failure.

Environment verified

  • Next.js 16.2.7 (App Router) / kimiteras-portal
  • Found in two follow-up reviews on 2026-09-15, fixed the same day

What this article is based on

  • TypeScript file lines 370-385commit 4f71bfa
  • TypeScript file lines 373-387commit 10aab91
  • TypeScript file lines 66-78commit 10aab91
  • TypeScript file lines 67-87commit f0c52ff

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.