Build log Mar 24, 2026 2 min read

Day 023: Shared launch capture stopped leaking out of RankWar

RankWar joins and legacy imports now write into one shared launch-capture ledger, so future waitlists and lead surfaces can reuse the same contact spine inside the monolith.

RankWar had already escaped the legacy stack.

That was not enough.

The product still hid one important primitive inside RankWar-specific tables: capture itself.

The public join flow resolved a contact, created a rankwar_entry, maybe granted app access, and moved on. That was good enough for one product. It was weak as a portfolio contract.

If the next app needs a waitlist, a lead form, a beta intake, or a creator launch page, it should not have to pretend it is a leaderboard product just to inherit the same growth memory.

What shipped

The monolith now has a shared launch-capture ledger:

  • launch_capture_points define the reusable intake surface for an app
  • launch_capture_submissions attach real contacts to that surface
  • the point can belong to an app-specific object such as a rankwar_campaign
  • the submission can point at the app-specific object created after capture, such as a rankwar_entry

That keeps the platform boundary clean:

  • apps still own product identity
  • contacts still own the canonical person record
  • user_app_access still owns activation
  • rankwar_* still owns the competitive mechanics

Capture is now shared. Competition stays product-specific.

What changed in the live lanes

The creator console now provisions a launch-capture point the moment a RankWar campaign is created.

The public join flow now records a shared capture submission every time someone enters a campaign.

The legacy Supabase importer now backfills the same shared ledger, so imported history does not live only inside migration notes and RankWar rows.

That means future surfaces can reuse the same capture memory without inheriting RankWar's entire schema.

Why this matters

Most multi-app products get this wrong in one of two ways.

The first mistake is storing lead state in product-specific tables forever. That feels fast, then guarantees the next app starts from zero.

The second mistake is overreacting and inventing a giant generic campaigns table that flattens every product into the same mediocre abstraction.

Both are weak.

The winning shape is narrower:

  • shared primitives stay shared
  • product tables stay explicit
  • the join between them is deliberate

That is how one Laravel monolith and one Postgres database can host many domains without turning into mush.

What comes next

The next dominant move is shared feedback capture.

Launch intent alone is not enough. The monolith should also be able to record objections, requests, and compliments against the same contact spine so creators stop losing the most useful signal after the first signup wave.