Files
descrybe/docs/admin-roles-support/18-performance.md
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

3.0 KiB

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
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)