# 02 — Current inventory: admin, roles, plans (A1/legacy), support **Agent:** 2/20 · **Mode:** read-only inventory (docs only) **Tools:** codehelper `project_context` (short) → `kickoff` → `investigate`/`query`/`context`; Read/Grep/Glob fallback where workspace file tools were unavailable. **Desired target (from parent brief / sibling agents 3–6):** | Persona / package | Desired access | |-------------------|----------------| | **Legacy plan (A1 + legacy-marked)** | Limited product nav only (Dashboard, Products, Feeds, Export Feeds, Categories, Attributes, Standard Fields, Billing, Settings). **No** Background Tasks / `processing.monitor`, stores, marketing extras | | **Platform admin + developer** | Full `/admin` + full ops capabilities | | **support_staff** | Support ticket queue / assign / reply only — **not** billing/plan mutation | --- ## 1. Executive summary Today the platform has a **binary** staff gate (`users.is_platform_admin`) and **company membership** roles (`admin` | `member`, plus API-key role `api`). There is **no** `staff_role` / `support_staff` / `developer` enum in code or schema (`is_legacy`, `staff_role`, `support_staff`, `developer` → **zero** matches). **A1** is modeled as a **client deal / custom package** (non-public plan name → `IsCustomPackage` → default **all features ON**), not as a limited “legacy nav” profile. Demo tenant **Local Demo Co** (ex–A1 Slovenija) is on public **Enterprise** (`is_custom=true`), which also resolves to all-ON features. Support desk exists (tenant + admin APIs, assignee field, agent replies) but **admin support routes require full platform admin**. No CSAT/rating fields found under `apps/api/internal/support`. --- ## 2. Role model (what exists) ### 2.1 Platform staff — single boolean | Symbol / field | Path | Notes | |----------------|------|-------| | `User.IsPlatformAdmin` | `apps/api/internal/auth/service.go` (`User`) | JSON `is_platform_admin` | | `Service.IsPlatformAdmin` | `apps/api/internal/auth/invites.go:209` | DB: `SELECT is_platform_admin FROM users WHERE id = $1 AND is_active` | | `RequirePlatformAdmin` | `apps/api/internal/httpapi/middleware.go:245` | Session + DB check; **not** a client claim | | `checkPlatformAdmin` | `apps/api/internal/httpapi/middleware.go:262` | Test hook `testPlatformAdmin` or `Auth.IsPlatformAdmin` | | `requirePlatformAdmin` | `apps/web/src/lib/admin-gate.ts:8` | Client gate via `GET /api/auth/me` | | `authSession.isPlatformAdmin` | `apps/web/src/lib/auth-session.svelte.ts` | UI session mirror | | `applyPlatformAdmins` | `apps/api/cmd/migrator/admins.go:13` | Legacy MySQL `admin_users` → `users.is_platform_admin` | | `seed-demo` | `apps/api/cmd/seed-demo/main.go` | Forces demo user `is_platform_admin = true` | **Missing vs desired:** `admin` / `developer` / `support_staff` staff roles; APIs to list/assign staff roles; middleware that allows support-only routes without full platform admin. ### 2.2 Company membership roles | Concept | Where | Values | |---------|-------|--------| | Membership `role` | `memberships.role` (auth/company handlers) | `admin`, `member` (invite normalize rejects `owner`) | | `CompanyAdminAllowed` | `middleware.go:43` | `admin` \|\| `api` | | `requireCompanyAdmin` | `middleware.go:48` | 403 `"admin required"` | | `allowCompanyAdminOrPlatform` | `middleware.go:58` | Company admin **or** platform admin (cutover: all-member tenants) | | Team promote/demote | `company_handlers.go`, `company_member_role_test.go` | Last-admin guards | Orthogonal to plan features (also noted in `docs/plan-permissions/02-plans-permissions-current.md` § membership). ### 2.3 Support message “agent” role (not staff RBAC) Ticket messages use `author_role` ∈ `user` | `agent` | `system` (`apps/web/src/lib/support/admin-api.ts`). `ReplyAsAgent` (`support/tickets.go:325`) writes `author_role='agent'`. That is **message authorship**, not a user staff role. --- ## 3. Admin panel — web routes All under `apps/web/src/routes/admin/`. Pages call `requirePlatformAdmin()` (except layout which only shows chrome when `me.user.is_platform_admin`). | Route | File | In `AdminNav`? | Purpose | |-------|------|----------------|---------| | `/admin` | `+page.svelte` | Overview | Analytics summary + tool cards | | `/admin/users` | `users/+page.svelte` | Yes | User list, set-password emails, **dev** set-password / impersonate | | `/admin/analytics` | `analytics/+page.svelte` | Yes | Tokens / credits / jobs | | `/admin/billing` | `billing/+page.svelte` | Yes | Plans, assign, credits, cycles + **PlanPermissionsPanel** | | `/admin/support` | `support/+page.svelte` | Yes | Ticket queue | | `/admin/support/[id]` | `support/[id]/+page.svelte` | (via Support) | Ticket detail / reply | | `/admin/stuck-products` | `stuck-products/+page.svelte` | Yes | Stuck jobs | | `/admin/settings` | `settings/+page.svelte` | Yes | Platform settings | | `/admin/tasks-cleanup` | `tasks-cleanup/+page.svelte` | No (overview card only) | Same stuck cleanup | | `/admin/logs` | `logs/+page.svelte` | No (deep link; nav comment: no logs API) | Placeholder | | `/admin/bootstrap` | `bootstrap/+page.svelte` | No | Check admin flag; cannot create admins via API | | `/admin/migrate-organizations` | `migrate-organizations/+page.svelte` | No | Migration helper UI | **Nav chrome:** `AdminNav` — `apps/web/src/lib/components/AdminNav.svelte` (`menuItems`). **Layout wiring:** `apps/web/src/routes/+layout.svelte` — `showAdminNav = Boolean(me?.user?.is_platform_admin)`; main app `Nav` gets `showAdmin` for “Platform admin” link (`adminOnly: true`). --- ## 4. Admin panel — API routes Mounted in `Server.Router` — `apps/api/internal/httpapi/server.go:294–325`: ``` /api/admin/* → RequireSession + RequirePlatformAdmin ``` | Method | Path | Handler (approx) | |--------|------|------------------| | GET | `/users` | `handleAdminListUsers` | | POST | `/users/{id}/dev-password` | `handleAdminDevSetPassword` (non-prod) | | POST | `/users/{id}/impersonate` | `handleAdminDevImpersonate` (non-prod) | | GET | `/companies` | `handleAdminListCompanies` | | GET | `/readiness` | `handleAdminReadiness` | | GET | `/analytics` | `handleAdminAnalytics` | | GET | `/jobs` | `handleAdminListJobs` | | POST | `/jobs/stuck-cleanup` | `handleAdminStuckCleanup` | | GET/PUT | `/settings` | platform settings | | POST | `/settings/mail/test` | mail probe | | GET/POST | `/plans` | list / upsert | | GET/PUT | `/plans/{planID}/features` | plan feature overrides | | POST | `/plans/{planID}/features/enable-all` | convenience | | POST | `/plans/{planID}/features/disable-all` | convenience | | GET/PUT | `/feature-gates` | global gates | | PUT | `/feature-gates/sections/{section}` | section toggle | | POST | `/plans/assign` | assign plan to company | | POST | `/credits` | add credits | | POST | `/billing/run-cycles` | renewals | | POST | `/emails/set-password` | cutover emails | | GET | `/support/tickets` | admin queue | | GET | `/support/tickets/{id}` | admin get | | POST | `/support/tickets/{id}/messages` | `ReplyAsAgent` | | PATCH | `/support/tickets/{id}` | `UpdateAdmin` (status/priority/assignee) | **Dev-only staff tooling** (not a “developer” role): `admin_dev_handlers.go` — blocked when `Config.IsProduction()`. --- ## 5. PlanPermissionsPanel & plan features | Piece | Path / symbol | |-------|----------------| | UI panel | `apps/web/src/lib/components/admin/PlanPermissionsPanel.svelte` | | Host page | `/admin/billing` Permissions tab — `billing/+page.svelte` | | Client API | `apps/web/src/lib/admin-plan-permissions.ts` (`listAdminPlansWithFeatures`, `loadFeatureGates`, enable/disable-all, …) | | Catalog | `apps/web/src/lib/plan-feature-catalog.ts` (`isDefaultPublicPlanName`, sections) | | Prior design doc | `docs/plan-permissions/08-admin-ui.md` | **Package kind badges** (panel `$derived packageKind`): - `default` — public ladder name + `!is_custom` - `ladder_custom` — public ladder + `is_custom` (Enterprise) - `custom` — `is_custom` - `deal` — non-ladder name without treating as custom badge path Stub mode when feature APIs return 404/501 (`isPlanPermissionsApiUnavailable`). Backend resolve path: `DefaultPlanFeatures` → `PlanAllowsFeature` → `ResolveEffectiveFeatures` (`apps/api/internal/billing/plan_features.go`). Custom packages default **all catalog keys ON**. --- ## 6. Plans: `is_custom`, public ladder, A1 / “legacy” ### 6.1 Public vs client deals | Symbol | Path | Behavior | |--------|------|----------| | `IsPublicProductPlan` | `billing/service.go:485` | `free\|starter\|growth\|business\|enterprise` only | | `ListPublicPlans` | `billing/service.go:600` | Marketing / self-serve; **excludes** client deals | | Admin `ListPlans` | via `/api/admin/plans` | **All** plan rows (incl. A1, Merkur, …) | | `EnsureDefaultPlans` | `service.go:496` | Syncs public ladder only; named deals untouched | Comments explicitly name **A1**, Merkur trial, legacy Basic/Professional as non-public client deals. ### 6.2 `IsCustomPackage` / `is_custom` | Symbol | Path | Rule | |--------|------|------| | `IsCustomPackage(name, isCustom)` | `billing/custom_package_features.go:19` | `isCustom \|\| !IsPublicProductPlan(name)` | | `prepareCustomPackageCreateFeatures` | same file `:49` | Non-public names force `IsCustom=true`; empty features → all-ON map | | Enterprise seed | `defaultPublicPlans()` | Public name **with** `IsCustom: true` | **Tests:** `custom_package_features_test.go` — `"A1", false, true` (name alone ⇒ custom treatment). ### 6.3 Company A1 / Local Demo Co | Fact | Source | |------|--------| | Legacy MySQL company | `A1 Slovenija` / `97e1a309-3d23-4aa2-b518-8e8d7afdfec7` | | Postgres id | `ee246275-dec0-4446-9e83-58d0c16c258a` | | Display name after seed | **Local Demo Co** (`docs/demo-user.md`, `docs/migrate-from-descrybe-new.md`) | | Demo plan | **Enterprise** (`is_custom=true`, 1M credits) — **not** a plan named `A1` | | Migrated teammate emails | `…@legacy.local` (dev password / impersonate) | **Implication:** Even if a DB row named `A1` exists as a client plan, feature defaults are **enable-all**, opposite of desired **legacy limited nav**. There is **no** `is_legacy` column or legacy feature profile in billing today. ### 6.4 Product nav gating (tenant UI) `Nav.svelte` filters items via `planCapabilities.can(featureKey)`. `processing.monitor` (Background Tasks / Processing) is a normal feature key — currently ON for custom/Enterprise. Desired legacy matrix would turn it **OFF** (and hide stores/marketing extras). --- ## 7. Support — tenant + admin ### 7.1 Tenant (company-scoped) | Surface | Path | |---------|------| | UI | `/support`, `/support/new`, `/support/[ticketId]` under `apps/web/src/routes/support/` | | API | `server.go:472+` — `GET/POST /api/support/tickets`, messages, notifications | | Handlers | `handleListSupportTickets`, `Create`, `Get`, `Reply` — session + **RequireCompany** | | Service | `ListForUser` / `Create` / `GetForUser` / `ReplyAsUser` | Nav item: `/support` gated by feature `support.center`. ### 7.2 Admin / staff queue | Surface | Path | |---------|------| | UI | `/admin/support`, `/admin/support/[id]` | | Client | `apps/web/src/lib/support/admin-api.ts` | | API | `/api/admin/support/tickets*` (platform admin only) | | Service | `ListAdmin`, `GetAdmin`, `ReplyAsAgent`, `UpdateAdmin` | | Schema | `apps/api/sql/schema/025_support_center.sql` (indexed; includes `assignee_admin_user_id`) | **Assignee:** `UpdateAdmin` / PATCH body `assignee_admin_user_id` / `clear_assignee`. No separate “support staff inbox” or claim-only policy — any platform admin sees the full queue. **Ratings / CSAT:** not present in support package types/handlers (gap for agent 11+). --- ## 8. Gaps vs desired state | Desired | Current | Gap | |---------|---------|-----| | Legacy limited nav (A1 / legacy plans) | Custom/non-public → **all features ON**; demo A1 tenant on **Enterprise** all-ON | Need legacy profile / `is_legacy` / seed matrix; map A1 (+ similar) to sparse features; turn off `processing.monitor`, stores, marketing extras | | Admin + developer full admin | Only `is_platform_admin` boolean | Need staff role enum; map admin+developer → full `/api/admin`; optionally keep prod-safe “dev tools” separate from role | | support_staff ticket access only | Support admin APIs behind **same** `RequirePlatformAdmin` as billing | Need least-privilege middleware + AdminNav subset (Support only); block plans/settings/credits | | Role chips / assign staff from orgs UI | Users list shows `is_platform_admin` badge only | No assign APIs; bootstrap page cannot create admins | | Support assign + staff inbox + CSAT | Assignee field exists; no staff role, no CSAT | Extend UpdateAdmin + UI; add rating schema | | Differentiate legacy vs `is_custom` | `is_custom` / deal name ⇒ enable-all | Contract must separate **custom deal packaging** from **legacy feature profile** | --- ## 9. Extension points (summary) Prefer extending existing gates rather than parallel systems: 1. **Staff RBAC** — extend `checkPlatformAdmin` / `RequirePlatformAdmin` (or sibling `RequireStaffRole`) reading a new `users.staff_role` (or replace boolean carefully). 2. **Legacy features** — extend `DefaultPlanFeatures` / `IsCustomPackage` / seed (`default_plan_features_seed.go`) with a `legacy` profile; detection: plan name patterns (`A1`, …) and/or `is_legacy` flag. 3. **Admin nav** — `AdminNav.menuItems` filtered by staff capability; support_staff → Support (+ maybe Overview read-only if needed). 4. **Support** — keep `/api/admin/support/*` but authorize `admin|developer|support_staff`; optionally filter `ListAdmin` by assignee for least privilege. 5. **PlanPermissionsPanel** — already the UI to inspect/set matrices; add Legacy badge + apply-legacy-profile action once backend exists. Machine-readable companion: [`02-extension-points.json`](./02-extension-points.json). --- ## 10. Related docs - `docs/plan-permissions/02-plans-permissions-current.md` — plans / features / roles orthogonality - `docs/plan-permissions/08-admin-ui.md` — PlanPermissionsPanel contract - `docs/demo-user.md` — Local Demo Co / A1 migration - `docs/migrate-from-descrybe-new.md` — A1-only migrator scope - Sibling agents will add `03-roles-matrix`, `04-contract`, `05-legacy-seed`, `06-staff-roles`, …