Skip to content

Convention — admin/production coexistence

What it is

The rule for adding an admin-management surface to an entity that already has a production/runtime surface, so the two coexist without colliding. The canonical case is Skill: the runtime app already consumed skills (allocation, demand, employee skills) long before MIQ-133 added a Skill admin CRUD — the admin track had to slot in alongside the existing production endpoints, not replace or disturb them.

When to use it

When you're adding CRUD/management for an entity whose data is already read or enforced elsewhere in the running system — especially seeded reference data the runtime depends on.

The pattern

1. Separate the admin track from the production track.

  • A distinct controller and route — admin endpoints live under /api/admin/<entity> (e.g. SkillsAdminController at /api/admin/skills), separate from the production controller (SkillsController).
  • A distinct React Query cache namespace — the admin hooks pass a different resourceKey (e.g. useSkillsAdmin uses resourceKey: 'skills-admin') so admin mutations don't stomp the production read cache. See Helpers catalog.
  • Distinct RBAC permissions — admin actions gate on <entity>.config / .view; the production surface keeps its own existing permissions.

2. Protect the seeded rows the runtime relies on. Reference data is often seeded as system rows (IsSystem = true) that runtime logic assumes exist. Admin edit/delete of those rows is rejected with SystemRowProtectedException<T> (a 409 <ENTITY>_PROTECTED) so an administrator can't pull a row out from under the running system. See Exceptions.

3. Respect existing references on delete. Because the production side may reference the row, delete is blocked by the FK-count guard (<Entity>ReferencedException, 409) — the admin track can't orphan production data.

The result: the admin layer is additive. It manages the long tail (create new non-system rows, edit descriptions, deactivate unused rows) while the seeded, referenced rows the runtime needs stay immutable.

Gotchas / constraints

  • System row ≠ global row. A system row (IsSystem = true) is structurally immutable to keep production stable. A global row (business_unit_id IS NULL) is shared reference data that a BU may extend. They're different ideas — don't conflate them (see Multi-tenancy).
  • Don't reuse the production controller for admin writes. Keep the admin track in its own controller/route so its permissions, validation, and cache behaviour stay isolated.
  • Partition the FE cache. If admin and production both query the same entity, give the admin hooks their own resourceKey, or an admin mutation will invalidate (or be invalidated by) the production cache unexpectedly.

A note on the "Path A" label. Some sprint material refers to admin-track layering with "Path" labels; the supporting fact sheet (AdminTrack_FactSheet.md) actually uses "Path 0/1/2" for the setup-order of provisioning (system prerequisites → BU scaffold → operational data), which is a related but distinct idea. The coexistence convention documented here is the concrete, code-verified one: a separate admin track that coexists with production via separate controllers/routes/cache-keys plus system-row and FK protection.

Build status

Available — implemented for Skill admin (MIQ-133 Phase 2) coexisting with the production skills surface; the system-row/FK guards are live across the lookup entities.