Appearance
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:
| Tab | What it already covers |
|---|---|
| Plans | CRUD 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-ons | Modular feature packs, per-seat / usage / feature, enable toggles |
| Promotions | Coupons, seasonal, referral, win-back, redemption caps |
| Subscribers | Workspace 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 |
| Insights | Conversion funnel, revenue by plan, MRR movement, churn |
| Approvals | Pricing-change and sensitive-op queue |
| Audit | Filterable, 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
- 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.
- Risk is a first-class field.
highmeans the control changes what the public sees, so the UI asks for confirmation and keeps the audit row forever. - 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.
| Scope | Precedence | Meaning |
|---|---|---|
platform | 10 | Everyone. Release-availability switches |
archetype | 20 | All workspaces of one archetype |
industry | 30 | The dairy-vs-boutique axis |
plan | 40 | All subscribers to one plan |
workspace_group | 50 | A cohort: enterprise bundle, beta group, custom segment |
workspace | 60 | One outlet, or its subtree |
workspace_member | 70 | One person within one workspace |
user | 80 | One 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
| Requirement | How it is expressed |
|---|---|
| Dealer A: 10 Setu Cards | one row — feature_key='setu_cards', scope=workspace, axis=entitlement, limit_value=10, on_exceed='block_new' |
| Dealer A: AI disabled | one row — scope=workspace, axis=entitlement, effect='deny' |
| Dealer B: AI enabled, higher messaging | two rows at workspace scope with a larger limit_value |
| Both on the same plan | the plan-scope rows (precedence 40) still apply to everything not overridden at 60 |
| "This employee only" | workspace_member scope, precedence 70 |
| Who may set it | set_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.
| # | Capability | State | Severity |
|---|---|---|---|
| 1 | Per-tenant feature entitlements + limits + overrides | 🟢 EXISTS — 8 scopes, 3 axes, precedence as data | — |
| 2 | Plan catalogue | 🟢 platform_plans exists | — |
| 3 | Subscription RECORD — which workspace is on which plan, with status and renewal | 🔴 MISSING. ⚠ The design's Subscribers and Custom deals tabs have no table behind them | Blocking |
| 4 | RBAC — roles, permissions, assignments | 🔴 0% built. The entire persona half | Blocking |
| 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 |
| 6 | Multiple roles per user — resolution semantics | 🔴 Undefined. The design has per-role Members, which permits it structurally and says nothing about conflicts | Blocking |
| 7 | Usage ledger — consumption against limit_value | 🔴 MISSING. The cap is expressible; current usage is not, so "increase their allowance" cannot be shown | High |
| 8 | Per-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 policy | High |
| 9 | Role 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 call | High |
| 10 | Screen-level access as its own axis | ⚠ Recommend AGAINST — see §4.3 | Decision |
| 11 | Plan targeting taxonomy | ⚠ The design targets geographic + segment/category; the platform models industry × archetype. Two taxonomies for one question | Medium |
| 12 | The 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 all | Context |
| 13 | Subscription lifecycle vs collection | ⚠ trial / grace / past-due are designed; 📘 the platform subscription has no collection path built at all | Medium |
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
denyat a more specificset_bybeats a grant at a less specific one — never a blanket deny inside the role layer.
| Why | |
|---|---|
| Union is what the user expects | Adding a responsibility should add ability, never remove it |
| Intersection breaks the common case | A rep-plus-team-lead would lose their own pipeline the moment the TL role omitted it |
A role-level deny is a trap | With 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
readon it. One derivation, no third matrix.
What the owner actually needs is still delivered:
| The ask | How 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 matrix | Especially 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 constraint | Rename 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.
| Control | Really scoped to | Evidence in the registry itself |
|---|---|---|
marketplace.inactive.listed, reputation.weight.*, growth.sponsored.maxPerScreen, trust.paidRanking | Platform. One answer for everyone | perm: 'platform.configure', owner: 'Platform' |
comms.rate.marketing, comms.rate.utility | Platform — Meta sets them | Same |
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_controlsgains a nullablescope_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 item | Consequence |
|---|---|
| Org Admin › Access & limits | Renders usage against limit_value — ⚠ and the usage ledger does not exist yet (gap 7) |
| Org Admin › Roles & scopes | Needs role_assignments with a subtree scope, and must show effective permissions per user |
| Org Admin › Subscription & invoices | Needs the subscription record (gap 3) |
| Every gated screen | Reads 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
| Build | Why in this order | |
|---|---|---|
| 1 | Subscription record — workspace → plan, status, renewal | Gap 3. Nothing in the panel is demonstrable without it, and it is one small table |
| 2 | Usage ledger | Gap 7. Turns limit_value from a number into a screen |
| 3 | RBAC with subtree scope — roles, permissions, role_assignments | Gaps 4-6. 📘 Wave 2's largest dependency, and every persona view above Team Leader waits on it |
| 4 | Role templates | Gap 9. Cheap once RBAC exists, and it is what makes provisioning a 2-minute job |
| 5 | platform_controls with a nullable tenant scope | Gap 8. One table, one nullable column, one resolution rule |
| 6 | Plan targeting reconciled onto industry × archetype | Gap 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
| # | Problem | Why 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 TABS | What 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 |
| 3 | Two tabs means two implementations of one list | Filters, 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 tab | If 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:
| Signal | Source |
|---|---|
| Workspace count under the organisation | workspaces + materialized path |
| Seats in use | workspace_members |
| Org-owned vs member-owned | workspaces.ownership_model |
| Has a negotiated override | any feature_grants row at workspace or workspace_group scope |
| Contract present | the subscription record ⚠ (gap 3) |
| ACV band | the 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
| Section | Solo / SMB | Enterprise |
|---|---|---|
| Outlet breakdown | absent — there is one | present, and the main event |
| Contract and term | "monthly, cancel anytime" or an annual term | negotiated, with an approval trail |
| Diff from template | usually empty | the primary view |
| Roles provisioned | one or two | the full matrix with subtree scopes |
| Commercial | plan price | negotiated 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
| # | Step | What it writes | State |
|---|---|---|---|
| 1 | Select customer | resolves organization / workspace | 🟢 |
| 2 | Confirm segment | ⚠ derived, shown not chosen (§10) | 🟢 |
| 3 | Base plan or template | plan_key — the inherited layer at precedence 40 | 🟢 |
| 4 | Customise features | feature_grants rows at workspace scope (60), axis entitlement | 🟢 |
| 5 | Limits | limit_value + limit_period + on_exceed | 🟢 |
| 6 | Roles / personas | instantiate role templates | 🔴 gap 9 |
| 7 | Integrations | axis availability per integration | 🟢 |
| 8 | Paid add-ons | grant bundle + a billing line | 🟡 grants yes, billing line no |
| 9 | Pricing, contract term, renewal | the subscription record | 🔴 gap 3 — no table |
| 10 | Review | ⚠ the DIFF, not a summary | 🟢 |
| 11 | Activate | write + audit row + approval queue if pricing changed | 🟢 |
⚠ Two rules for this wizard, and the first one is what makes Review worth having
- 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.
- Every step must map to a write the schema can hold. ⚠ A wizard that can express an entitlement
feature_grantscannot 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.
| # | Step | What it writes |
|---|---|---|
| 1 | Name, category, description | add-on definition |
| 2 | Grant bundle — which features, which axis | the rows applied on activation |
| 3 | Pricing model — flat / per-seat / per-unit / metered | price definition |
| 4 | Usage limits and on_exceed | limit_value, limit_period, on_exceed |
| 5 | Eligibility — which industries, archetypes, plans, segments | ⚠ keyed on industry × archetype, not an invented "segment" taxonomy (gap 11) |
| 6 | Activation rules — immediate, next cycle, needs approval | lifecycle |
| 7 | Visibility — public, private, hidden | as plans already have |
| 8 | Publish | version + audit |
13 · What this adds to the gap list
| # | Gap | Severity |
|---|---|---|
| 14 | Derived segment view over workspace count, seats, ownership model, override presence, ACV | Medium — cheap, and it replaces a field that would go stale |
| 15 | ⚠ Provenance in the resolver's OUTPUT | High — the resolver must return which scope won, not just the value. Cheap now, a rewrite later |
| 16 | Template diff — resolved entitlements minus base plan | Medium. Powers both the deal view and the wizard's Review |
| 17 | Saved views — named, shareable, user-extensible list filters | Medium. The thing that actually replaces tabs |
| 18 | Add-on definition object — reusable grant bundle + price + eligibility | High. 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.
Related
- Screen blueprint — the dealership screens these entitlements gate
- Architecture validation — the wider schema assessment
- Persona feature map — the personas these role templates instantiate
- Commercial model — the plan templates, and why they are only templates