💼🔌📊
If you have been living under a rock, every B2B SaaS still bills per seat. The pricing page says "$29 per user per month" and everyone nods. Spoiler: the schema behind that line is where most teams quietly lose their minds.
The usual mess looks like one bloated
tenants table with seat_limit, plan_name, and a column called notes that someone is using as a kill switch. Don't do that.
The Money Pattern: four tables, one job each. Plans hold the catalogue. Subscriptions link tenant to plan. Overrides handle the "just give them 50 seats, Aiden said it was fine" cases. Usage_daily is the rollup your UI reads.
Behold: a single view stitches it together. Override beats plan, plan beats default, NULL never wins.
Plot twist: the firehose stays separate. Raw usage_events writes go through a SECURITY DEFINER RPC so RLS never blocks a logged-in user from counting their own action, but they also can't forge a tenant_id. The dashboard reads usage_daily, never the firehose.
The catch: nightly cron must roll usage_events into usage_daily or the firehose eats your storage budget alive. Use pg_cron or a Supabase scheduled function. Either works. Skipping it does not.
The Verdict: four tables, one view, one RPC. No spreadsheets, no plan_name column with 12 values that should have been an enum three years ago. Per-seat pricing is not hard. The schema sprawl is.