# Plan permissions — system index Per-plan dashboard feature toggles, global section master switches, defaults for the public ladder, custom enable-all, admin UI, dashboard gating, and **API enforcement**. **Runtime rule:** ``` effective(feature) = plan_allows(feature) AND global_section_enabled(section(feature)) AND global_feature_enabled(feature) ``` Meters (`max_products`, credit wallet, `can_use_ai`) stay as today and compose with feature keys. --- ## Doc map (agents 1–10) | Doc | Owner | What it covers | |-----|-------|----------------| | [01-dashboard-feature-catalog.md](./01-dashboard-feature-catalog.md) | 1 | Every tab/section + suggested defaults | | [01-feature-keys.json](./01-feature-keys.json) | 1 | Machine-readable registry | | [02-plans-permissions-current.md](./02-plans-permissions-current.md) | 2 | How plans/entitlements worked before | | [02-extension-points.json](./02-extension-points.json) | 2 | Files/symbols to extend | | [03-permission-contract.md](./03-permission-contract.md) / [`.json`](./03-permission-contract.json) | 3 | Normative contract | | [04-backend-model.md](./04-backend-model.md) | 4 | Schema + `billing` service | | [05-api.md](./05-api.md) / [05-openapi-fragment.yaml](./05-openapi-fragment.yaml) | 5 | Admin + tenant HTTP surface | | [06-defaults-matrix.md](./06-defaults-matrix.md) / [`.json`](./06-defaults-matrix.json) | 6 | Default matrix per plan | | [07-custom-enable-all.md](./07-custom-enable-all.md) | 7 | Custom create + enable/disable-all | | [08-admin-ui.md](./08-admin-ui.md) | 8 | `/admin/billing` → Permissions tab | | [09-dashboard-gating.md](./09-dashboard-gating.md) | 9 | User nav/route/action gating | | [10-enforcement.md](./10-enforcement.md) | 10 | API AssertFeature wiring + tests | --- ## How an admin configures it 1. Sign in as **platform admin** → **Platform billing** (`/admin/billing`). 2. Open the **Permissions** tab (`PlanPermissionsPanel`). 3. **Per plan:** select a package → toggle feature keys → **Enable all** / **Disable all**. 4. **Global:** flip a section master (e.g. disable `marketing` for **all** plans) via section controls / `PUT /api/admin/feature-gates/sections/{section}`. 5. Tenant UI reads `features` from `/api/auth/me` (credits) and `/api/billing/capabilities`. 6. Sensitive API mutations call `billing.AssertFeature` / `AssertProcessingFeatures` and return **402** `{ "error":"feature_disabled", "code":"plan_gate", "feature":"…", "upgrade_url":"/pricing" }`. ### API cheat sheet | Action | Endpoint | |--------|----------| | Tenant capabilities | `GET /api/billing/capabilities` | | Credits + features | `GET /api/billing/credits` / `GET /api/auth/me` | | Plan feature editor | `GET\|PUT /api/admin/plans/{id}/features` | | Enable / disable all on a plan | `POST …/features/enable-all` \| `disable-all` | | Global gates | `GET\|PUT /api/admin/feature-gates` | | Section for all plans | `PUT /api/admin/feature-gates/sections/{section}` | Auth: session + company for tenant; session + **platform admin** for admin mutations. CSRF on dashboard POSTs. --- ## Code map | Layer | Path | |-------|------| | Migration | `apps/api/sql/schema/026_plan_features.sql` | | Catalog | `apps/api/internal/billing/feature_catalog.go` | | Resolve + CRUD | `apps/api/internal/billing/plan_features.go`, `features_api.go` | | Custom enable-all | `apps/api/internal/billing/custom_package_features.go` | | Seed | `EnsureDefaultFeatureSeeds` (from `EnsureDefaultPlans`) | | Enforcement | `apps/api/internal/billing/feature_enforcement.go` + call sites | | HTTP | `apps/api/internal/httpapi/plan_features_handlers.go`, `plan_gate.go` | | Admin UI | `PlanPermissionsPanel.svelte`, `admin-plan-permissions.ts` | | Dashboard gate | `plan-capabilities.ts`, `PlanRouteGuard.svelte`, `Nav.svelte` | --- ## Defaults (summary) | Package | Behavior | |---------|----------| | **Free** | Core catalog/feeds/stores ON; AI / API keys / live email / campaign AI / SEO AI / BYOK OFF | | **Starter** | AI + API keys ON; BYOK OFF | | **Growth / Business** | All registry keys ON | | **Custom** (`is_custom` or non-public name, incl. Enterprise) | All ON (create materializes full true map) | | **Global gates** | Missing row ⇒ enabled | --- ## Verify locally ```powershell .\scripts\migrate.ps1 # applies 026_plan_features.sql cd apps\api go test ./internal/billing/ -count=1 -run "DefaultPlanFeatures|ResolveEffective|FeatureKey|Custom|Assert" go test ./internal/httpapi/ -count=1 -run "PlanFeatures|PlanGate" ``` Manual: 1. Platform admin → Permissions → disable `marketing` globally → tenant `GET /api/billing/capabilities` lists marketing keys in `disabled_features`. 2. Free company → `POST /api/processing/jobs` with `processing_type=title` → 402 `feature_disabled`. 3. Create custom plan (omit features) → all `resolved_features` true; enable-all / disable-all round-trip. --- ## Post-rollout verify (2026-08-05) | Check | Result | |-------|--------| | Migrate path (Windows/Laragon) | `.\scripts\migrate.ps1` (Makefile `migrate` needs bash) | | `026_plan_features.sql` | Applied — goose version **26** (BOM stripped so goose could parse the file) | | `go test ./internal/billing/ -count=1` | **PASS** (`ok` ~0.1s) | | Key files present | Admin `PlanPermissionsPanel.svelte` + `admin-plan-permissions.ts`; dashboard `plan-capabilities.ts`, `PlanRouteGuard.svelte`, `Nav.svelte`; API `plan_features_handlers.go`, `plan_gate.go`, `features_api.go` | ### Remaining blockers (from agent 10) 1. Not every route is `AssertFeature`'d — list/read and many store/feed mutations are still UI-gated only. 2. OpenAPI — session fragment in `05-openapi-fragment.yaml`; public `/api/v1/openapi.yaml` unchanged (optional merge). 3. Numeric marketing limits remain documentation-only (no feature-key counters). 4. Agent 9 UI — confirm live that nav hides after capabilities load; clear admin stub mode once APIs return 200. 5. EPREL — `capability.eprel` ON for Free in matrix vs historical product copy; align separately. ## Remaining blockers See [10-enforcement.md](./10-enforcement.md#remaining-blockers).