# Dev Log: 2026-07-07

> Source: <https://dev.to/nasrulhazim/dev-log-2026-07-07-3jc2>
> Published: 2026-07-07 21:48:06+00:00

Four repos moved today. Here's the thread that ties most of them together: **one source of truth beats two.**

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.

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 |

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 always the same. Confirm against the read path, not the name.

A one-line fix, but a good reminder: a plan-tier flag (which tier unlocks a given capability) had drifted between the billing logic and the public pricing page. When entitlement rules live in two places, they *will* disagree. Keep the tier matrix in one config and read it everywhere.

Three of today's four changes are the same idea wearing different clothes: **remove the second source of truth.** Two entitlement models → one plan. Three authorization paths → one permission layer. Two pricing configs → one. Duplication in *rules* is worse than duplication in code, because the copies drift silently until someone gets billed wrong or let in where they shouldn't be.
