# 18 — Performance (capabilities, permission matrices, support queues) Date: 2026-08-05 Owner: agent 18/20 ## Scope Concrete query/index/cache fixes for: 1. **Capabilities resolution** (`GET /api/billing/capabilities`) 2. **Admin permission matrices** (`GET /api/admin/plans`, plan features, feature gates) 3. **Support queues** (user list + admin/staff inbox) No million-SKU product-list work (already covered in `docs/perf-notes.md`). ## Findings (before) | Path | Issue | Severity | |------|--------|----------| | `CapabilitiesForCompany` | Active plan lookup used only `company_plans(company_id)` | Medium | | `handleGetCapabilities` | Body had `feature_etag` but no HTTP `ETag` / 304 (OpenAPI already patterned) | Medium | | Admin `ListPlans` | Already one query + in-memory `resolved_features` (no N+1) | OK | | Admin matrices | No `Cache-Control` — risk of intermediary caching | Low | | `ListAdmin` count | Always joined `users` + `companies` even without search | Medium | | Support indexes | Missing assignee + COALESCE(activity) indexes for queue sorts | Medium | | Pagination | Handlers already use `ParseLimitOffset` (default 50, max 200) | OK | ## Changes ### 1. Indexes — goose `027_capabilities_support_perf.sql` - `company_plans_company_active_created_idx` — partial `(company_id, created_at DESC) WHERE is_active` - `support_tickets_status_activity_idx` — `(status, COALESCE(last_message_at, updated_at) DESC)` - `support_tickets_assignee_activity_idx` — staff inbox partial on assignee - `support_tickets_user_activity_idx` — user ticket list activity sort Apply via existing migrate script after deploy. ### 2. Capabilities HTTP cache headers `handleGetCapabilities` now sets: - `Cache-Control: private, max-age=30, must-revalidate` - `ETag: "sha256:…"` via `billing.CapabilitiesResponseETag` (features **plus** plan id / name / remaining credits) - `304 Not Modified` when `If-None-Match` matches Body `feature_etag` is unchanged (feature-map only) for the web client. ### 3. Admin permission matrices - `ListPlans` already selects `features` once and resolves matrices in-process (no per-plan round-trip). - Added `Cache-Control: private, no-store` on: - `GET /api/admin/plans` - `GET /api/admin/plans/{id}/features` - `GET /api/admin/feature-gates` ### 4. Support queues - Count query skips user/company joins unless `search`/`q` is set. - Service-layer `clampListBounds` (default 50 / max 200) as defense in depth. - Admin list accepts `assignee_id` filter (uses new assignee index). - Handlers continue to use `ParseLimitOffset`. ## Tests - `apps/api/internal/billing/capabilities_etag_test.go` - `apps/api/internal/support/list_bounds_test.go` ```powershell cd apps/api go test ./internal/billing/ ./internal/support/ ./internal/httpapi/ -count=1 ``` ## Out of scope / deferred - In-process TTL cache for `GetFeatureGates` (tiny table; premature) - Keyset pagination for support (volume still small) - Changing product-list OFFSET pagination (see `docs/perf-notes.md` P0)