Dev Log: 2026-07-07
TL;DR Collapsed a billing model from à-la-carte features to plans-only, in four safe phases. Unified authorization across web, API, and MCP so all three obey one permission layer. Fixed a legacy Oracle password-sync writing to the wrong column. Four repos moved today. Here's the thread that ties most of them together: one source of truth beats two. Billing: plans-only Spent most of the day migrating a SaaS off per-feature à-la-carte subscriptions and onto plans-only entitlements. The interesting part isn't the model — it's doing it without a billing outage: seed plans, switch reads to plans, backfill every org, then delete the old machinery. Expand/contract, four deployable phases. Full write-up in the focused post. One permission layer for three surfaces An ops tool exposed the same actions three ways — web UI, API, and an MCP server for agent access. The bug: each surface checked authorization slightly differently, so an MCP tool could allow something the web UI blocked. The fix was to make the MCP tools gate on the same permission layer as everything else, so: web ─┐ API ─┼─► one permission check ─► allow / deny MCP ─┘ TL;DR: web ≡ API ≡ MCP — three doors, one lock. Also added a dedicated support-engineer role scoped for debugging without handing over the keys to everything, plus identity/diagnostics/SLA read tools so an agent can answer "why didn't this notification send?" without shell access. Before After Each surface authorizes its own way Single permission check, shared MCP tool could out-permission the UI MCP bound to the same guard No debug-scoped role support_engineer role, read-only diagnostics Legacy Oracle password sync Smaller but sharp: a password reset was writing to the wrong Oracle column and also touching a date_modified field it had no business updating. Routed the student reset to the correct password column and dropped the stray write. Lesson with legacy schemas — the column that looks right and the column the app actually reads from are not a