Skip to content

The admin control plane: what exists, what does not, and the three decisions before screens ​

Part of car_sales — the dealership operating layer. 🌐 Design pulled from the Claude Design prototype project on 2026-08-23. 🧮 Schema read from the live migration set. Neither was assumed.

⚠ The headline, and it is the opposite of what a dealership-specific subscription system would assume

📘 The owner's instruction was "evaluate the existing subscription-management design against the dealership model rather than creating a separate or hardcoded dealership-specific system."

🧮 The entitlement half of what is being asked for already exists in the schema, and it was built for this exact admin panel. feature_grants carries eight scopes with precedence held as DATA, three axes, a limit_value, a limit_period and an on_exceed policy. The table's own comment says why the precedence is data rather than a CASE expression:

"the precedence is READABLE BY THE ADMIN PANEL, which is exactly what rendering the Access Control matrix requires — a UI cannot explain a precedence it has to hardcode."

⚠ So do not design a dealership subscription system. Design the screens that drive the one that is already there. QRS-863.

1 · What the design already contains ​

🌐 prototype/admin-panel/Subscriptions.dc.html — "Subscription Command Center", eight tabs, and it supersedes the thin Plans.dc.html:

TabWhat it already covers
PlansCRUD library; plan inspector with Pricing / Entitlements / Availability / Versions; cycles monthly / yearly / lifetime / usage / quarterly; editable per-feature entitlements; visibility public / private / hidden; targeting; plan inheritance; version history with rollback and scheduled release; lifecycle draft → review → published → paused → deprecated
Add-onsModular feature packs, per-seat / usage / feature, enable toggles
PromotionsCoupons, seasonal, referral, win-back, redemption caps
SubscribersWorkspace subscriptions; status active / trial / grace / past-due / canceled; per-subscription upgrade / downgrade / cancel / reactivate / recover; bulk ops
Custom deals⚠ org / workspace / user negotiated pricing — enterprise, volume, nonprofit, pilot, personalized. This is precisely the "Dealer A gets this, Dealer B gets that" requirement, already designed
InsightsConversion funnel, revenue by plan, MRR movement, churn
ApprovalsPricing-change and sensitive-op queue
AuditFilterable, exportable log

🌐 prototype/admin-panel/RBAC.dc.html — Access control: roles library with create / clone / inherit; per-role permission matrix across every module × 9 actions with inheritance, reusable groups and dependency/conflict validation; per-role Members; a permissions catalogue with an auto-scaling module registry and deny-by-default; time-bound assignments; approval queue; audit log.

🌐 And prototype/platform/controls.js — a platform control registry: every switch, threshold and weight as a record with id, group, label, help, type, default, risk, owner and audit trail. Nine groups, including a crm group that already holds sla.firstResponseHours, assign.mode and contact.maxPerWeek.

🔎 The design is materially stronger than the dealership plan had assumed, and three of its choices are worth naming

  1. A registry, not settings screens. "A settings screen is written once and then nobody can find anything in it." A new control is a row plus nothing else — no screen, no nav item.
  2. Risk is a first-class field. high means the control changes what the public sees, so the UI asks for confirmation and keeps the audit row forever.
  3. Deny-by-default with an auto-scaling module registry, so a new module appears in the matrix without a design change.

2 · What the schema actually has ​

🧮 Read from 20260808140000_v2_features_and_grants.sql.

ScopePrecedenceMeaning
platform10Everyone. Release-availability switches
archetype20All workspaces of one archetype
industry30The dairy-vs-boutique axis
plan40All subscribers to one plan
workspace_group50A cohort: enterprise bundle, beta group, custom segment
workspace60One outlet, or its subtree
workspace_member70One person within one workspace
user80One principal regardless of workspace — the consumer scope

Plus: axis in (applicability, entitlement, availability) · effect · limit_value · limit_period · on_exceed in (block_new, read_only, grace_period) · and a set_by precedence of platform_admin > org_admin > vendor > derived-default.

🔎 Worked example: the owner's Dealer A and Dealer B, with no code change and no new table

RequirementHow it is expressed
Dealer A: 10 Setu Cardsone row — feature_key='setu_cards', scope=workspace, axis=entitlement, limit_value=10, on_exceed='block_new'
Dealer A: AI disabledone row — scope=workspace, axis=entitlement, effect='deny'
Dealer B: AI enabled, higher messagingtwo rows at workspace scope with a larger limit_value
Both on the same planthe plan-scope rows (precedence 40) still apply to everything not overridden at 60
"This employee only"workspace_member scope, precedence 70
Who may set itset_by — platform admin outranks org admin outranks vendor

⚠ That is the whole of the custom-subscription requirement, and it is already in the database. The set_by chain is also exactly the three-layer cascade in the owner's §9.

3 · The gap table ​

🧮 Verified, not inferred. is_admin() has zero definitions; roles, permissions, role_assignments, platform_controls and any subscription record do not exist.

#CapabilityStateSeverity
1Per-tenant feature entitlements + limits + overrides🟢 EXISTS — 8 scopes, 3 axes, precedence as data—
2Plan catalogue🟢 platform_plans exists—
3Subscription RECORD — which workspace is on which plan, with status and renewal🔴 MISSING. ⚠ The design's Subscribers and Custom deals tabs have no table behind themBlocking
4RBAC — roles, permissions, assignments🔴 0% built. The entire persona halfBlocking
5⚠ Role SCOPE (subtree)🔴 Missing from the SCHEMA and from the RBAC design. A matrix of module × action cannot express "Sales Manager over outlets 1-3 but not 4"Blocking, and invisible
6Multiple roles per user — resolution semantics🔴 Undefined. The design has per-role Members, which permits it structurally and says nothing about conflictsBlocking
7Usage ledger — consumption against limit_value🔴 MISSING. The cap is expressible; current usage is not, so "increase their allowance" cannot be shownHigh
8Per-tenant CONTROL overrides🔴 controls.js is platform-wide with no tenant dimension — its own lift note is platform_controls (id, value_json, updated_by, updated_at). ⚠ But crm.sla.firstResponseHours is a per-dealer policyHigh
9Role templates per customer type🔴 Not a concept anywhere. Without it a dealer admin builds roles from a blank matrix, which nobody will do on a phone callHigh
10Screen-level access as its own axis⚠ Recommend AGAINST — see §4.3Decision
11Plan targeting taxonomy⚠ The design targets geographic + segment/category; the platform models industry × archetype. Two taxonomies for one questionMedium
12The admin panel itself🔴 Design-only. 🧮 apps/mobile/src/tiers/admin/ holds two READMEs and zero code; apps/web has no /admin route and no auth at allContext
13Subscription lifecycle vs collection⚠ trial / grace / past-due are designed; 📘 the platform subscription has no collection path built at allMedium

4 · The three decisions that must be made before any screen is drawn ​

4.1 · Multi-role resolution: UNION of allows, UNION of scopes ​

📘 The owner's case: "one person could be both Sales Representative and Team Lead."

Recommended, and it must be written down before the matrix is designed

Permissions UNION across a user's roles. Scopes UNION too. A deny at a more specific set_by beats a grant at a less specific one — never a blanket deny inside the role layer.

Why
Union is what the user expectsAdding a responsibility should add ability, never remove it
Intersection breaks the common caseA rep-plus-team-lead would lose their own pipeline the moment the TL role omitted it
A role-level deny is a trapWith union semantics one role's deny would silently disable another's grant, and the admin sees two ticks and one broken screen
Deny belongs on the set_by axis🧮 feature_grants already ranks platform_admin > org_admin > vendor. Suspensions and compliance blocks live there, where precedence is explicit

⚠ The matrix UI must therefore show EFFECTIVE permission for a selected user, not just per-role ticks. A permission matrix that cannot answer "what can Rajesh actually do" is the single most likely support failure in this panel.

4.2 · Role scope is a first-class column, not a filter ​

This is the gap most likely to be missed, because the RBAC design was drawn for a PLATFORM admin and inherits no hierarchy

📘 ADR-0023 D4: roles assign to a SUBTREE. A Sales Manager over three outlets is one assignment, not three — and a per-workspace role row is how a revoked role survives in a forgotten duplicate.

So role_assignments needs (user_id, role_id, scope_workspace_id, includes_subtree, valid_from, valid_until) — and the design's time-bound assignments already anticipate the last two.

⚠ And the resolution must use the materialized path, not a recursive walk: a subtree predicate on every row of leads is the query that gets slow first (architecture validation §3.2).

4.3 · ⚠ Screen access must be DERIVED, never a third toggle ​

The owner asks for screen-level control. I recommend against it as an independent axis, and the reason is a defect class this repo has already paid for

📘 If a screen exists because a module is entitled and a role has a read action on it, then screen visibility is a function of two things that are already configurable. Adding an independent screen matrix creates two places to say the same thing — the duplicate-source-of-truth class ([QRS-249] (/dev-tracker/tracker)), and the failure is nasty: a screen ticked ON while its module is off, and nobody can tell which control is lying.

Screen visible ⇔ module entitled AND role has read on it. One derivation, no third matrix.

What the owner actually needs is still delivered:

The askHow it is met
"This dealer should not see Analytics"Deny the module at workspace scope. Every screen in it disappears
"Only the GM sees people performance"The role has no read on that module
"Hide this nav item but keep the deep link working"⚠ Navigation prominence is presentation, not permission. A nav_hidden flag is fine; a permission that 404s an entitled user is a bug
"Enable Receptionist but not Team Lead"Provision the role template or do not. Personas are role templates (§5)

🔎 And it keeps the panel explicable. An operator asked "why can't they see this screen?" has exactly two places to look instead of three.

5 · Personas are ROLE TEMPLATES, and that is the missing concept ​

📘 The owner is right that the dealership hierarchy is a reference model, not a structure. GM → CRM Manager → Team Lead → Sales Rep at one dealer; Dealer Principal → Sales Manager → Sales Executive at another; GM → Sales Rep at a third.

The model that supports all three without a code branch

text
CUSTOMER TYPE  (industry × archetype — already modelled)
      ↓ suggests
ROLE TEMPLATES  ← the missing table: a named permission bundle per customer type
      ↓ instantiated at provisioning, then freely edited
TENANT ROLES  (this dealer's own roles, renamed and reshaped at will)
      ↓ assigned, many-to-many, scoped to a subtree, optionally time-bound
USERS
Why templates rather than fixed personas
A dealer admin will never build a role from a blank matrixEspecially not on a support call. A template is the difference between a 2-minute provisioning and a 40-minute one
A template is a STARTING POINT, not a constraintRename Team Lead to Senior Consultant, merge two, delete one. The tenant's roles are theirs
It generalises past dealerships immediately📘 A yoga trainer's template set is owner alone; an enterprise's is nine roles. Same table, different rows — which is the owner's §6
It is the only place the reference hierarchy should exist📘 And never in app code: "never branch on archetype, industry or plan" is already lint-gated

6 · The control cascade ​

⚠ The boundary is a trust boundary, not a layout. 📘 ADR-0028: /admin and /org must never share a route group, and a bug on /org exposes one customer to another (QRS-842).

7 · Per-tenant vs platform controls — the split nobody has drawn ​

🧮 controls.js holds both kinds in one registry with no tenant dimension. Its own lift target is platform_controls (id, value_json, updated_by, updated_at) — no scope column.

ControlReally scoped toEvidence in the registry itself
marketplace.inactive.listed, reputation.weight.*, growth.sponsored.maxPerScreen, trust.paidRankingPlatform. One answer for everyoneperm: 'platform.configure', owner: 'Platform'
comms.rate.marketing, comms.rate.utilityPlatform — Meta sets themSame
crm.sla.firstResponseHours, crm.sla.followUpDays, crm.assign.mode, crm.score.enabled⚠ PER TENANT. One dealer wants 4 hours, another 24🔎 The registry already hints at it: these carry perm: 'crm.manage' and owner: 'Sales', not platform.configure
comms.quiet.*, crm.contact.maxPerWeek⚠ Platform floor, tenant may tighten🌐 "QR setu sends from ONE business number, so a person hit by three teams blocks the number for every merchant"

The recommendation, and it is one nullable column

platform_controls gains a nullable scope_workspace_id / scope_org_id. Resolution is tenant value → platform default. Controls declare whether a tenant may override, and in which direction.

⚠ The direction matters and is easy to miss: a tenant may make contact.maxPerWeek stricter, never looser, because the shared WhatsApp number means one tenant's looseness is every tenant's problem. A per-tenant override without a direction rule is a way for one customer to damage the others.

⚠ And do not put thresholds in feature_grants. limit_value is an entitlement cap — how much you may have. An SLA hour count is a policy. Conflating them would make "raise their card limit" and "change their follow-up clock" the same operation, and they have different owners.

8 · What this changes in the dealership blueprint ​

⚠ The tier labels in the screen blueprint are PLAN TEMPLATES, not structure

📘 Screen blueprint labels every screen [Core] / [Growth] / [Complete] / [Group] / [Add-on]. 🔎 Read those as "which plan template includes it by default", never as a hardcoded tier. Any of them is overridable per dealer by one feature_grants row at workspace scope.

So Foundation → Growth → Complete are named bundles of grants, and the ladder is a sales artefact rather than an architectural one. That is what the owner asked for, and the schema already permits it.

Blueprint itemConsequence
Org Admin › Access & limitsRenders usage against limit_value — ⚠ and the usage ledger does not exist yet (gap 7)
Org Admin › Roles & scopesNeeds role_assignments with a subtree scope, and must show effective permissions per user
Org Admin › Subscription & invoicesNeeds the subscription record (gap 3)
Every gated screenReads useFeature's three axes, never enabled alone. 📘 CLAUDE.md's fifth rule
Nothing dealership-specific⚠ No dealership subscription table, no dealership role enum, no dealership plan code. The vertical is rows

9 · Sequencing ​

BuildWhy in this order
1Subscription record — workspace → plan, status, renewalGap 3. Nothing in the panel is demonstrable without it, and it is one small table
2Usage ledgerGap 7. Turns limit_value from a number into a screen
3RBAC with subtree scope — roles, permissions, role_assignmentsGaps 4-6. 📘 Wave 2's largest dependency, and every persona view above Team Leader waits on it
4Role templatesGap 9. Cheap once RBAC exists, and it is what makes provisioning a 2-minute job
5platform_controls with a nullable tenant scopeGap 8. One table, one nullable column, one resolution rule
6Plan targeting reconciled onto industry × archetypeGap 11

⚠ And the honest statement about the panel itself

🧮 apps/mobile/src/tiers/admin/ holds two READMEs and zero code. apps/web has no /admin route and no auth at all. The Subscription Command Center and the RBAC matrix are design-only.

🔎 That is not an argument against them — it is the reason to validate the model now, while the cost of a schema decision is one migration rather than a rewrite. But nothing on this page should be read as a capability the platform has today.

10 · ⚠ Should Solo/SMB and Enterprise be separate tabs? My answer is no ​

📘 The owner asked for a product and UX take rather than implementation, so this section argues the case and then proposes a different structure.

The recommendation, stated first

One Subscribers list, SEGMENTED. Not two tabs. Separate the detail view by segment, never the list.

🔎 Tabs feel like the answer because they solve the symptom the owner correctly identified — an admin should never dig through a mixed list. But they solve it by creating four problems that get worse with scale, and the scale argument actually points the other way.

The four reasons tabs are the wrong instrument here ​

#ProblemWhy it matters more over time
1⚠ "Enterprise" is a shape of DEAL, not a type of customer📘 The platform models three user categories and industry × archetype. Commercially, "enterprise" is a fact pattern: several workspaces, several seats, a negotiated price, a contract. Those are filterable facts, not a taxonomy that should be frozen into navigation
2⚠⚠ A tab forces a binary the business will re-litigate, and reclassification MOVES RECORDS BETWEEN TABSWhat is a 2-outlet dealer on a negotiated price? A solo merchant on a custom deal because they are a design partner? Every borderline case becomes an argument, and the resolution relocates an account — the worst property a navigation can have. "Where did that account go?" becomes a support question about our own tool
3Two tabs means two implementations of one listFilters, sorts, columns, bulk ops, search, pagination — written twice and drifting apart. 📘 The duplicate-source-of-truth class applied to UI, and the enterprise copy gets the new column six months late
4🔎 Tabs do not solve the scale problem; they hide it in one tabIf there are 5,000 SMBs and 30 enterprise accounts, the SMB tab is still an unusable 5,000-row list. What actually solves scale is search, segmentation and saved views — and once you have those, the enterprise tab is redundant

And the owner's real requirement is met better without them

The stated requirement is "an Admin should be able to instantly distinguish customer type and understand the complete subscription configuration of any individual client without losing context."

🔎 That is a requirement about the RECORD being self-identifying, not about the lists being physically apart. A strong segment identity on every row and at the top of every detail view delivers it — and keeps working when a sixth segment appears.

What to build instead ​

text
Subscriptions
│
├── Overview            ← the OPERATIONAL queue, not vanity metrics
│     expiring in 30 days · past-due · approvals pending
│     limit breaches · deals awaiting activation
│
├── Subscribers         ← ONE list, segmented, with saved views
│     │
│     ├── saved views (user-extensible, the primary navigation)
│     │     Enterprise · active        SMB · expiring 30d
│     │     Custom deals · pending     Over limit
│     │     Dealerships                Trials ending
│     │
│     └── row identity: SEGMENT CHIP · outlets · seats · deal type · ACV
│
├── Plans / templates   ← PRODUCTIZED. Versioned, published, inherited
├── Add-ons             ← PRODUCTIZED. Reusable grant bundles + price + eligibility
├── Promotions
├── Insights
├── Approvals
└── Audit

⚠ Note what IS legitimately separate, and why it is a different axis from customer size

📘 The owner's §4 names it exactly: productized plans versus client-specific commercial agreements. That separation is by OBJECT TYPE, and object type is a legitimate navigation axis because the two have different lifecycles — a plan is versioned, published and inherited; a deal is negotiated, signed and expires.

🔎 So keep Plans, Add-ons and Custom deals apart. Do not add customer size as a second axis of tabs on top. ⚠ And "Custom deals" is better modelled as a filter on Subscribers (deal_type = custom) than as its own list, because a custom deal is a subscription — which is why the owner could not find the drill-down: it was in a different list from the account it belongs to.

The segment must be DERIVED, never a hand-set flag ​

A hand-set customer-type field goes stale the first week and then quietly lies

🧮 Every fact needed to classify an account already exists in the tenancy tree:

SignalSource
Workspace count under the organisationworkspaces + materialized path
Seats in useworkspace_members
Org-owned vs member-ownedworkspaces.ownership_model
Has a negotiated overrideany feature_grants row at workspace or workspace_group scope
Contract presentthe subscription record ⚠ (gap 3)
ACV bandthe subscription record ⚠ (gap 3)

Segment is a VIEW over those facts, not a column somebody maintains. It cannot drift, it needs no migration when the thresholds move, and an account that grows from 1 outlet to 4 reclassifies itself.

⚠ And show WHY: the chip should be explainable on hover — "Enterprise: 6 outlets, 41 seats, negotiated price." A classification an admin cannot interrogate is one they stop trusting.

11 · The client drill-down, and the two things the owner did not ask for ​

📘 The owner's outline is right. Two additions make it answer the question they actually posed — "what exactly did we sell this client, what are they entitled to, what are they using" — and both fall out of the schema for free.

➕ Addition 1: PROVENANCE on every entitlement row ​

This is the single most valuable thing this screen can do, and it is free

🧮 feature_grants stores precedence as data and carries set_by. So every effective entitlement can name where it came from:

text
AI actions        DISABLED     ← workspace scope (60), set by platform_admin, 12 Aug
                               overriding: plan "Growth" (40) which grants 2,000/month
Setu Cards        24 max       ← workspace scope (60), set by platform_admin, 12 Aug
                               overriding: plan "Growth" (40) which grants 30
WhatsApp          enabled      ← inherited from plan "Growth" (40)
Google Business   enabled      ← inherited from plan "Growth" (40)
Marketplace       n/a          ← availability axis: not shipped

⚠ Without provenance an admin cannot tell what was NEGOTIATED from what was INHERITED — which is precisely the confusion the owner is describing. A custom deal with 80 rows and no provenance is less readable than no screen at all.

➕ Addition 2: the DIFF view — show only what differs from the base template ​

A custom deal is a set of deltas. Show the deltas

🔎 An admin asking "what exactly did we sell ABC Motors" wants the 6 overrides, not the 80 inherited rows. So the deal's primary view is a diff against its base plan, with the full resolved table one click away.

And the same view is the wizard's Review step, which is what makes Review a check rather than theatre (§12).

The drill-down, specified ​

text
ABC Motors                                    [ENTERPRISE]  6 outlets · 41 seats
Custom Enterprise · derived from Growth       active · renews 31 Mar 2027

├── AT A GLANCE
│     ACV · contract term · renewal · owner · payment status · 6 overrides
│
├── WHAT DIFFERS FROM THE TEMPLATE          ← the primary view
│     6 rows, each with provenance and who set it
│
├── ENTITLEMENTS × USAGE                    ⚠ needs the usage ledger (gap 7)
│     Setu Cards          26 of 24      ⚠ OVER — on_exceed: block_new
│     Management logins   12 of 12         at limit
│     Storage             18 of 25 GB
│     AI actions          disabled
│     WhatsApp credits    1,240 of 5,000
│
├── DISABLED, AND WHY                       ⚠ three different reasons, never merged
│     applicability  — does not apply to this business at all
│     entitlement    — not on this plan            → upsell
│     availability   — not shipped yet              → not sellable
│
├── ROLES PROVISIONED
│     which templates were instantiated · who holds them · subtree scope
│
├── ADD-ONS ACTIVE
│     WhatsApp · Google Business · standees · AI — each with its own term and meter
│
├── OUTLET BREAKDOWN                        ← enterprise-only section
│     per outlet: plan, cards, seats, usage, its own overrides
│
├── COMMERCIAL
│     negotiated price · cycle · invoices · GST · approval trail
│
└── HISTORY
      every change, who made it, at which scope, and what it overrode

⚠ The three-reason split is not cosmetic and this screen is where it gets decided

📘 ADR-0021: effective = applicability AND entitlement AND availability. A single "disabled" list would make the admin panel lie to its own operator — an applicability row is not an upsell opportunity, an availability row is not sellable at any price, and only entitlement is a commercial conversation. 📘 CLAUDE.md's fifth rule turns on exactly this distinction.

✅ And this is where the DETAIL genuinely diverges by segment, which is the separation worth having

SectionSolo / SMBEnterprise
Outlet breakdownabsent — there is onepresent, and the main event
Contract and term"monthly, cancel anytime" or an annual termnegotiated, with an approval trail
Diff from templateusually emptythe primary view
Roles provisionedone or twothe full matrix with subtree scopes
Commercialplan pricenegotiated price, per-outlet, with history

🔎 Same list, same record, same URL — a detail view that renders the sections the account actually has. That delivers everything tabs promised, and an account that grows from solo to enterprise gains sections rather than moving lists.

12 · New Custom Deal and New Add-on, as real workflows ​

📘 The owner is right that these read as placeholders. Each step below names the write it performs, so the wizard cannot express something the schema cannot hold.

New Custom Deal — 7 of 9 steps are buildable today ​

#StepWhat it writesState
1Select customerresolves organization / workspace🟢
2Confirm segment⚠ derived, shown not chosen (§10)🟢
3Base plan or templateplan_key — the inherited layer at precedence 40🟢
4Customise featuresfeature_grants rows at workspace scope (60), axis entitlement🟢
5Limitslimit_value + limit_period + on_exceed🟢
6Roles / personasinstantiate role templates🔴 gap 9
7Integrationsaxis availability per integration🟢
8Paid add-onsgrant bundle + a billing line🟡 grants yes, billing line no
9Pricing, contract term, renewalthe subscription record🔴 gap 3 — no table
10Review⚠ the DIFF, not a summary🟢
11Activatewrite + audit row + approval queue if pricing changed🟢

⚠ Two rules for this wizard, and the first one is what makes Review worth having

  1. Review shows the diff against the base template. 🔎 A review step that restates what you just typed is theatre; one that says "these 6 things differ from Growth, and 2 of them need approval" is a check.
  2. Every step must map to a write the schema can hold. ⚠ A wizard that can express an entitlement feature_grants cannot store produces a deal the product will not honour — and the operator will not find out until the dealer complains.

New Add-on — it is a PRODUCT object, not a per-client one ​

🔎 The reframe that fixes where this button lives

An add-on is a reusable bundle of grants, plus a price, plus an eligibility rule. That makes it the same kind of object as a plan — versioned, published, inherited — and not the same kind as a custom deal.

⚠ So New Add-on belongs next to Plans, and applying one to a client is an action on the Subscriber, never a creation step. Placing it beside Custom deals is what made it read as a placeholder: a creation button in a list of instances has nothing coherent to create.

#StepWhat it writes
1Name, category, descriptionadd-on definition
2Grant bundle — which features, which axisthe rows applied on activation
3Pricing model — flat / per-seat / per-unit / meteredprice definition
4Usage limits and on_exceedlimit_value, limit_period, on_exceed
5Eligibility — which industries, archetypes, plans, segments⚠ keyed on industry × archetype, not an invented "segment" taxonomy (gap 11)
6Activation rules — immediate, next cycle, needs approvallifecycle
7Visibility — public, private, hiddenas plans already have
8Publishversion + audit

13 · What this adds to the gap list ​

#GapSeverity
14Derived segment view over workspace count, seats, ownership model, override presence, ACVMedium — cheap, and it replaces a field that would go stale
15⚠ Provenance in the resolver's OUTPUTHigh — the resolver must return which scope won, not just the value. Cheap now, a rewrite later
16Template diff — resolved entitlements minus base planMedium. Powers both the deal view and the wizard's Review
17Saved views — named, shareable, user-extensible list filtersMedium. The thing that actually replaces tabs
18Add-on definition object — reusable grant bundle + price + eligibilityHigh. New Add-on has nothing to write without it

⚠ Gap 15 is the one to act on before anything is designed

🔎 A resolver that returns only the effective value forces the admin panel to re-derive precedence in the client to explain itself — which is a second implementation of the precedence rule, in JavaScript, guaranteed to disagree with the database eventually. 📘 feature_grant_scopes was deliberately made data so the panel would not have to hardcode it; returning the winning scope with the value is the other half of that decision, and it has not been specified.