# 03 — Roles & legacy plan matrix (agent 3/20) **Status:** Design contract for admin roles + legacy cohort packaging. **Machine-readable twin:** [`03-roles-matrix.json`](./03-roles-matrix.json). **Feature keys:** align with [`docs/plan-permissions/01-feature-keys.json`](../plan-permissions/01-feature-keys.json). **Plan defaults (public ladder):** [`docs/plan-permissions/06-defaults-matrix.json`](../plan-permissions/06-defaults-matrix.json). --- ## PROBLEM Migrated / legacy tenants (example: **A1** → Local Demo Co catalog) must see a **narrow product surface** matching the legacy nav only — not the full v2 Operate/Setup marketing suite. Separately, platform roles need a clear permission ceiling that composes with plan feature keys. --- ## Effective resolution ``` effective(feature) = role_allows(role, feature) AND plan_allows(company.plan, feature) AND global_section_enabled(section(feature)) AND global_feature_enabled(feature) ``` - **Company end-users** (`legacy_user`, `standard_user`): plan matrix is the primary gate; role is the ceiling. - **Staff roles** (`admin`, `developer`, `support_staff`): role ceiling is wide; company plan still applies when acting *as* a tenant unless impersonation / platform override is explicit. - Platform admin routes (`/admin/*`) are **orthogonal** to dashboard `feature_key`s — gated by `users.is_platform_admin` (and future staff flags), not by plan JSON. **ASSUMPTION:** `legacy_user` / `standard_user` are product-cohort roles derived from plan name patterns (and/or an explicit `plans.cohort = legacy|standard` when implemented), not company membership `admin|member`. **ASSUMPTION:** A1 company / plan example maps to the **legacy** plan profile (see plan name patterns below). --- ## Legacy nav allow-list (ONLY) These are the **only** user-facing nav destinations for the legacy cohort. Everything else is OFF. | Nav label | Route | Parent feature key(s) | |-----------|-------|------------------------| | Dashboard | `/dashboard` | `dashboard.overview` | | Products | `/products` | `catalog.products` | | Feeds | `/feeds` | `feeds.list` (+ mapping) | | Export Feeds | `/export-feeds` | `feeds.export_feeds` | | Categories | `/categories` | `catalog.categories` | | Attributes | `/attributes` | `catalog.attributes` | | Standard Fields | `/standard-fields` | `catalog.standard_fields` | | Usage & Billing | `/billing` | `billing.overview` | | Settings | `/settings` | `settings.profile` (+ settings tabs) | ### Explicitly excluded from legacy | Surface | Reason | |---------|--------| | Background Tasks / Processing (`processing.monitor`) | Out of legacy nav | | Stores / Woo / Shopify | Out of scope | | Marketing campaigns, content calendar | Out of scope | | Brand kit / brand AI | Out of scope | | SEO | Out of scope | | Reviews | Out of scope | | AI integrations / Email sending | Integration extras beyond feeds | | Support center (tickets) | Support-heavy; not essential for legacy nav | | Structured descriptions / vector categories | Extra catalog tools, no legacy nav | | Platform admin chrome | Staff-only (`admin` / `developer`) | --- ## Legacy feature allow-list (ON keys) Canonical ON set for plan profile **`legacy`** (and role **`legacy_user`**). All other keys in `01-feature-keys.json` are **OFF**. ### Shell - `shell.navigation` - `shell.command_palette` - `shell.company_switcher` - `shell.tutorial` - `shell.account_menu` - `shell.billing_recovery_banner` OFF: `shell.support_notifications` ### Dashboard - `dashboard.overview` - `dashboard.stats` - `dashboard.quick_links` - `dashboard.recent_jobs` *(informational only; no Processing nav)* - `dashboard.news_feed` - `dashboard.activation_checklist` - `dashboard.migrated_checklist` - `dashboard.etl_gaps` - `dashboard.upgrade_banners` OFF: `dashboard.store_reconnect` ### Catalog - `catalog.products` (+ all product tabs) - `catalog.products.tab_processed` - `catalog.products.tab_needs_review` - `catalog.products.tab_error` - `catalog.products.tab_processing` - `catalog.products.tab_unprocessed` - `catalog.products.process_categories` - `catalog.products.process_attributes` - `catalog.products.process_ai_titles` - `catalog.products.process_ai_descriptions` - `catalog.products.enrichment_review` - `catalog.products.export_selection` - `catalog.products.upgrade_prompt` - `catalog.categories` - `catalog.categories.title_formula` - `catalog.categories.description_formula` - `catalog.attributes` - `catalog.attributes.bulk_import` - `catalog.standard_fields` - `catalog.standard_fields.groups` OFF: `catalog.structured_descriptions`, `catalog.vector_categories` ### Feeds (import + export + CSV uploads needed for feeds) - `feeds.list` - `feeds.add_url` - `feeds.add_csv` - `feeds.sync` - `feeds.mapping` - `feeds.mapping.select_item` - `feeds.mapping.map_fields` - `feeds.export_feeds` - `feeds.export_feeds.create` - `feeds.export_feeds.generate` - `feeds.uploads` ### Billing (Usage & Billing) - `billing.overview` - `billing.customer_portal` - `billing.quick_upgrade` - `billing.plans_compare` - `billing.checkout` ### Settings - `settings.profile` - `settings.company` - `settings.alerts` - `settings.api_keys` - `settings.team` - `settings.team_invite` ### Capabilities (core catalog/feeds only) - `capability.sku_cap` - `capability.ai_credits` - `capability.ai_processing` - `capability.eprel` - `capability.normalize_specs_fill` - `capability.feed_source_limit` - `capability.export_feed_limit` - `capability.storage_limit` - `capability.api_access` OFF: `capability.campaign_ai`, `capability.email_live_send`, `capability.brand_ai_apply`, `capability.seo_ai_rewrite`, `capability.byok` ### Entire sections OFF for legacy - **stores.*** — all keys - **processing.*** — `processing.monitor` - **marketing.*** — all keys - **integrations.*** — all keys - **support.*** — all keys --- ## Plan name patterns → profile | Pattern (case-insensitive) | Profile | Notes | |----------------------------|---------|-------| | exact `legacy` | `legacy` | Explicit legacy package | | `^a1(\b\|[\s_-])` / contains `a1 slovenija` | `legacy` | A1 company / plan example | | company display historically **A1 Slovenija** (MySQL `97e1a309-…`) | `legacy` | Even if renamed to Local Demo Co for demo | | exact `free` \| `starter` \| `growth` \| `business` \| `enterprise` | `standard` (+ ladder tier) | Public ladder | | `is_custom=true` and name does **not** match legacy patterns | `custom` | All-on defaults per plan-permissions contract | | unknown / empty | `standard` / Free | Fail closed to Free matrix | **A1 example = legacy.** Treat A1’s assigned plan (or a dedicated `Legacy` plan row seeded for that tenant) as the legacy profile. --- ## Roles | Role key | Who | Plan / feature source | Ceiling | |----------|-----|----------------------|---------| | `legacy_user` | End-user on a legacy-pattern company/plan | **legacy** matrix | Legacy allow-list only | | `standard_user` | End-user on public ladder | Plan matrix from `06-defaults-matrix` (Free→Enterprise) | Full catalog; plan denies Free/Starter AI/BYOK/etc. | | `admin` | Platform admin (`users.is_platform_admin`) | N/A for `/admin/*`; full dashboard keys ON | All feature keys + AdminNav | | `developer` | Engineering / ops with platform access | Same as admin + debug catalog extras always ON | All keys; intended for non-prod diagnostics | | `support_staff` | Support agents | Support + read-heavy tenant assist | Support keys ON; marketing/integrations mostly OFF; no destructive platform settings | ### Role notes #### `legacy_user` - Sees only the legacy nav allow-list. - AI titles/descriptions + credits remain ON (legacy customers processed catalog data). - No stores, processing monitor, marketing, brand, SEO, integrations extras, or support ticket UI. #### `standard_user` - Full v2 dashboard surface as allowed by their **public plan** (see `06-defaults-matrix.json`). - Role matrix in JSON is the **ceiling** (all registry keys `true`); Free/Starter denials still apply via plan. #### `admin` (platform) - `is_platform_admin = true`. - All dashboard feature keys ON. - Platform surfaces: Overview, Users, Analytics, Platform billing, Support, Stuck Products, Platform settings (`AdminNav.svelte`). - May impersonate users (non-prod / gated admin APIs). #### `developer` - Same feature ceiling as `admin`. - Explicitly keeps debug/extra catalog keys ON (`catalog.vector_categories`, `catalog.structured_descriptions`). - Expected to use platform admin + API tooling; not a customer-facing role. #### `support_staff` - Dashboard: shell + dashboard overview/stats + support.* ON. - Catalog/feeds/billing: **read-assist** — parent keys ON so staff can open customer context when impersonating; write-heavy marketing/integrations OFF. - Platform: Support queue (`/admin/support`), Users (limited), Stuck Products; **not** Platform settings or Platform billing write. - [UNCERTAIN] No dedicated `is_support_staff` column today — design assumes a future staff flag or group; until then map to a subset of platform-admin users. --- ## Role × section summary | Section | legacy_user | standard_user | admin | developer | support_staff | |---------|:-----------:|:-------------:|:-----:|:---------:|:-------------:| | shell (core) | ON | ON | ON | ON | ON | | shell.support_notifications | OFF | ON | ON | ON | ON | | dashboard (core) | ON | ON | ON | ON | ON | | dashboard.store_reconnect | OFF | ON | ON | ON | OFF | | catalog (products/categories/attributes/standard fields) | ON | ON | ON | ON | ON† | | catalog extras (structured/vector) | OFF | ON | ON | ON | OFF | | feeds (+ export + uploads) | ON | ON | ON | ON | ON† | | stores | OFF | ON | ON | ON | OFF | | processing.monitor | OFF | ON | ON | ON | ON† | | marketing | OFF | ON | ON | ON | OFF | | integrations | OFF | ON | ON | ON | OFF | | billing | ON | ON | ON | ON | ON† | | settings | ON | ON | ON | ON | ON† | | support | OFF | ON | ON | ON | ON | | capabilities (core AI/SKU/feeds) | ON | plan | ON | ON | ON† | | capabilities (campaign/email/brand/seo/byok) | OFF | plan | ON | ON | OFF | | `/admin/*` | OFF | OFF | ON | ON | partial | † support_staff: intended for assisted sessions / impersonation; not for self-serve marketing ops. --- ## COMPOSITION with company membership Existing company membership roles (`admin` \| `member`) stay orthogonal: | Concern | Gate | |---------|------| | Invite / team / API keys / Stripe portal | company `admin` (or platform admin) | | Which nav/features appear | plan profile + product role (`legacy_user` / `standard_user`) | | Platform console | `is_platform_admin` / staff role | Do **not** overload company membership `admin` with platform `admin`. --- ## IMPLEMENTATION HOOKS (non-binding) | Need | Likely home | |------|-------------| | Resolve legacy profile from plan name | `DefaultPlanFeatures` / plan name normalizer beside public ladder | | Seed Legacy plan row | `EnsureDefaultPlans` + assign to A1/demo migrated tenant | | Staff role flag | users column or staff group — [UNCERTAIN] until agent/schema decides | | Nav filter | existing `feature` on `Nav.svelte` items + ResolveFeatures | --- ## VERIFICATION checklist - [ ] Legacy matrix ON keys ⊆ `01-feature-keys.json` - [ ] No `processing.monitor`, `stores.*`, `marketing.*`, `integrations.*`, `support.*` ON for legacy - [ ] A1 / `legacy` name patterns resolve to legacy profile - [ ] `standard_user` ceiling does not bypass Free/Starter denials in `06-defaults-matrix` - [ ] Platform `admin` retains `/admin/*` independent of plan JSON