# 05 — Plan permissions API (agent 5/10) Session/dashboard HTTP surface for plan feature permissions. Aligns with `03-permission-contract.md`. Public `/api/v1` OpenAPI is unchanged (API-key product API); schemas below are the session contract for admin UI + dashboard gating. Backend storage + resolve live in `billing` per `04-backend-model.md` / contract §2 / §6 (`plans.features` JSONB + `platform_feature_gates`). Migration: `apps/api/sql/schema/026_plan_features.sql`. Apply with `make migrate` / `.\scripts\migrate.ps1` before relying on writes. Handlers call: `CapabilitiesForCompany`, `GetPlanFeatures`, `SetPlanFeatures`, `EnableAllPlanFeatures`, `GetFeatureGates`, `SetFeatureGates`, `SetSectionGate` (and agent-4 aliases `EnableAllForPlan` / `SetGlobalFeature` / `AssertFeature`). --- ## Endpoint list + auth | Method | Path | Auth | Purpose | |--------|------|------|---------| | `GET` | `/api/billing/capabilities` | Session + company | Effective features = plan ∩ global | | `GET` | `/api/billing/credits` | Session + company | Existing credits overview **plus** `features`, `disabled_features`, `feature_etag` | | `GET` | `/api/auth/me` | Session | Embeds credits overview (same feature fields when credits load) | | `GET` | `/api/admin/plans` | Session + **platform admin** | Plans include `features` (overrides) + `resolved_features` (plan_allows) | | `POST` | `/api/admin/plans` | Session + **platform admin** | Upsert plan; optional `features` replaces overrides when present | | `GET` | `/api/admin/plans/{planID}/features` | Session + **platform admin** | Plan feature editor payload | | `PUT` | `/api/admin/plans/{planID}/features` | Session + **platform admin** | Replace overrides `{ "features": { "key": true } }` | | `POST` | `/api/admin/plans/{planID}/features/enable-all` | Session + **platform admin** | Set every registry key `true` (custom packages) | | `GET` | `/api/admin/feature-gates` | Session + **platform admin** | Global section + feature master switches | | `PUT` | `/api/admin/feature-gates` | Session + **platform admin** | Partial upsert `{ "sections": {...}, "features": {...} }` | | `PUT` | `/api/admin/feature-gates/sections/{section}` | Session + **platform admin** | Enable/disable one section for **ALL** plans | Mutations require CSRF (dashboard session stack) + platform admin for `/api/admin/*`. Unknown feature keys / sections → `400` `{ "error": "…" }` via `billing.ClientError`. --- ## Response shapes ### `GET /api/billing/capabilities` ```json { "plan_id": 3, "plan_name": "Starter", "is_custom": false, "has_active_plan": true, "features": { "catalog.products": true, "settings.api_keys": true }, "sections": { "catalog": true, "marketing": true }, "disabled_features": ["integrations.ai.byok"], "feature_etag": "sha256:…", "entitlements": { "plan_name": "Starter", "is_free_plan": false, "is_paid_plan": true, "can_use_ai": true, "can_use_eprel": true } } ``` ### `GET|PUT /api/admin/plans/{planID}/features` ```json { "plan_id": 3, "plan_name": "Starter", "is_custom": false, "features": { "settings.api_keys": true }, "resolved_features": { "catalog.products": true, "integrations.ai.byok": false } } ``` - `features` = stored sparse overrides (`{}` if none) - `resolved_features` = full registry via `plan_allows` only (globals ignored) ### `GET|PUT /api/admin/feature-gates` ```json { "sections": { "shell": true, "marketing": false }, "features": { "capability.byok": false } } ``` Missing gate rows default **enabled** (migration-safe). ### `PUT /api/admin/feature-gates/sections/{section}` ```json { "enabled": false } ``` Returns full `FeatureGatesView`. --- ## OpenAPI (session fragment) See `docs/plan-permissions/05-openapi-fragment.yaml` for OpenAPI 3.1 path/components suitable for admin/dashboard clients. Not mounted on `/api/v1/openapi.yaml`. --- ## Files touched (agent 5) | File | Intent | |------|--------| | `apps/api/internal/httpapi/plan_features_handlers.go` | Handlers | | `apps/api/internal/httpapi/server.go` | Route mount | | `apps/api/internal/billing/plan_features.go` | Service + DTOs (shared with agent 4) | | `apps/api/internal/billing/feature_catalog.go` | Registry from `01-feature-keys.json` | | `apps/api/internal/billing/service.go` | `Plan`/`CreditsOverview`/`ListPlans`/`UpsertPlan` | | `apps/api/internal/billing/client_errors.go` | Client-facing feature errors | | `apps/api/sql/schema/026_plan_features.sql` | Additive migration | --- ## Manual verify 1. `.\scripts\migrate.ps1` (or `make migrate`) 2. Platform admin: `GET /api/admin/feature-gates` → all sections true 3. `PUT /api/admin/feature-gates/sections/marketing` `{ "enabled": false }` 4. Tenant session: `GET /api/billing/capabilities` → marketing keys in `disabled_features` 5. `POST /api/admin/plans/{id}/features/enable-all` then `GET` → all `resolved_features` true 6. Non-admin `PUT /api/admin/feature-gates` → 403