Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
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 | 1 | Every tab/section + suggested defaults |
| 01-feature-keys.json | 1 | Machine-readable registry |
| 02-plans-permissions-current.md | 2 | How plans/entitlements worked before |
| 02-extension-points.json | 2 | Files/symbols to extend |
03-permission-contract.md / .json |
3 | Normative contract |
| 04-backend-model.md | 4 | Schema + billing service |
| 05-api.md / 05-openapi-fragment.yaml | 5 | Admin + tenant HTTP surface |
06-defaults-matrix.md / .json |
6 | Default matrix per plan |
| 07-custom-enable-all.md | 7 | Custom create + enable/disable-all |
| 08-admin-ui.md | 8 | /admin/billing → Permissions tab |
| 09-dashboard-gating.md | 9 | User nav/route/action gating |
| 10-enforcement.md | 10 | API AssertFeature wiring + tests |
How an admin configures it
- Sign in as platform admin → Platform billing (
/admin/billing). - Open the Permissions tab (
PlanPermissionsPanel). - Per plan: select a package → toggle feature keys → Enable all / Disable all.
- Global: flip a section master (e.g. disable
marketingfor all plans) via section controls /PUT /api/admin/feature-gates/sections/{section}. - Tenant UI reads
featuresfrom/api/auth/me(credits) and/api/billing/capabilities. - Sensitive API mutations call
billing.AssertFeature/AssertProcessingFeaturesand 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
.\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:
- Platform admin → Permissions → disable
marketingglobally → tenantGET /api/billing/capabilitieslists marketing keys indisabled_features. - Free company →
POST /api/processing/jobswithprocessing_type=title→ 402feature_disabled. - Create custom plan (omit features) → all
resolved_featurestrue; 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)
- Not every route is
AssertFeature'd — list/read and many store/feed mutations are still UI-gated only. - OpenAPI — session fragment in
05-openapi-fragment.yaml; public/api/v1/openapi.yamlunchanged (optional merge). - Numeric marketing limits remain documentation-only (no feature-key counters).
- Agent 9 UI — confirm live that nav hides after capabilities load; clear admin stub mode once APIs return 200.
- EPREL —
capability.eprelON for Free in matrix vs historical product copy; align separately.
Remaining blockers
See 10-enforcement.md.