{ "agent": "04/20", "title": "Unified contract: plan features + staff roles + legacy", "status": "design_only", "version": "1.0.0", "coordinates_with": [ "docs/admin-roles-support/01-ux-research.md", "docs/admin-roles-support/02-current-inventory.md", "docs/admin-roles-support/03-roles-matrix.md", "docs/admin-roles-support/03-roles-matrix.json", "docs/plan-permissions/03-permission-contract.md", "docs/plan-permissions/03-permission-contract.json", "docs/plan-permissions/01-feature-keys.json", "docs/plan-permissions/06-defaults-matrix.json" ], "assumptions": [ "Product cohort (legacy/standard/custom) is a plan feature_profile, not a stored end-user RBAC role; legacy_user/standard_user are derived labels", "support_staff is least privilege: ticket queue assign/reply only — narrower than optional read-assist in 03", "packages ≡ plans rows; no parallel permission service outside billing + auth staff flags", "is_platform_admin retained; staff_role additive with null+admin-flag ⇒ staff_role admin back-compat", "A1/Local Demo Co Enterprise → Legacy assign is explicit ops step, not silent migration" ], "axes": { "plan_features": { "subject": "company.active_plan", "storage": ["plans.features", "plans.feature_profile", "platform_feature_gates"], "resolver": "ResolveEffectiveFeatures / CapabilitiesForCompany", "failure": { "http": 402, "code": "plan_gate" } }, "company_membership": { "subject": "user in company", "storage": "memberships.role", "values": ["admin", "member"], "notes": "Orthogonal; do not overload with platform admin" }, "platform_staff": { "subject": "user", "storage": ["users.is_platform_admin", "users.staff_role"], "resolver": "resolve_staff_role + staff_allows(capability)", "failure": { "http": 403, "error": "staff capability required" } } }, "runtime_formulas": { "effective_feature": "plan_allows(key) AND global_section_enabled(section(key)) AND global_feature_enabled(key)", "plan_allows": [ "if key in plans.features -> plans.features[key]", "else if resolve_plan_profile == legacy -> LegacyMatrix[key] (missing -> false)", "else if resolve_plan_profile == custom -> true", "else -> DefaultPlanFeatures(name, false)[key]" ], "resolve_plan_profile_priority": [ "plans.feature_profile if set (legacy|ladder|custom)", "legacy name patterns from 03-roles-matrix", "IsCustomPackage -> custom", "else ladder" ], "staff_allows": "user.is_active AND resolve_staff_role(user) in allowed_roles(capability)", "resolve_staff_role": [ "inactive -> none", "staff_role if not null", "else if is_platform_admin -> admin", "else none" ] }, "legacy": { "profile": "legacy", "shipping_modes": [ { "id": "named_profile", "preferred": true, "mechanism": "plans.feature_profile='legacy' or name-pattern derive; DefaultPlanFeatures/LegacyMatrix" }, { "id": "sparse_or_dense_features", "preferred": false, "mechanism": "plans.features explicit map; requires profile precedence so missing keys are not custom-all-on" } ], "must_not": "Fall through IsCustomPackage all-ON for A1/legacy names", "allowlist_authority": "docs/admin-roles-support/03-roles-matrix.md", "excluded_sections": [ "processing.monitor", "stores.*", "marketing.*", "integrations.*", "support.*" ], "name_patterns_ref": "docs/admin-roles-support/03-roles-matrix.json#plan_name_patterns", "a1_example": { "legacy_company_id": "97e1a309-3d23-4aa2-b518-8e8d7afdfec7", "demo_note": "Local Demo Co may still be on Enterprise until explicit Legacy assign" } }, "staff_roles": { "values": ["admin", "developer", "support_staff"], "storage": { "keep": "users.is_platform_admin", "add": { "column": "users.staff_role", "type": "TEXT NULL CHECK IN (admin, developer, support_staff)", "invariant": "staff_role set implies is_platform_admin=true" } }, "backfill": "UPDATE users SET staff_role='admin' WHERE is_platform_admin AND staff_role IS NULL", "capabilities": { "staff.admin_shell": ["admin", "developer", "support_staff"], "staff.support.queue": ["admin", "developer", "support_staff"], "staff.support.reply": ["admin", "developer", "support_staff"], "staff.support.assign": ["admin", "developer", "support_staff"], "staff.users.read": ["admin", "developer"], "staff.users.write": ["admin", "developer"], "staff.analytics": ["admin", "developer"], "staff.billing": ["admin", "developer"], "staff.plans_features": ["admin", "developer"], "staff.feature_gates": ["admin", "developer"], "staff.settings": ["admin", "developer"], "staff.jobs_stuck": ["admin", "developer"], "staff.impersonate": ["admin", "developer"], "staff.dev_password": ["admin", "developer"] }, "support_staff_least_privilege": { "allowed_web": ["/admin/support", "/admin/support/[id]"], "allowed_api": [ "GET /api/admin/support/tickets", "GET /api/admin/support/tickets/{id}", "POST /api/admin/support/tickets/{id}/messages", "PATCH /api/admin/support/tickets/{id}" ], "denied": [ "billing", "plans", "feature_gates", "credits", "settings", "users_write", "impersonate", "stuck_cleanup", "self_escalate_staff_role" ], "deferred": "ticket-side read-only company context without tenant write APIs" }, "middleware": { "prefer": ["RequireStaff", "RequireStaffCapability"], "extend": [ "apps/api/internal/httpapi/middleware.go#RequirePlatformAdmin", "apps/api/internal/httpapi/middleware.go#checkPlatformAdmin", "apps/api/internal/auth/invites.go#IsPlatformAdmin" ], "router_today": "apps/api/internal/httpapi/server.go /api/admin RequireSession+RequirePlatformAdmin", "ui": [ "apps/web/src/lib/admin-gate.ts", "apps/web/src/lib/components/AdminNav.svelte" ] } }, "security": { "session": "scs cookie + RequireSession; staff flags from DB not client claims", "csrf": "double-submit X-CSRF-Token on dashboard /api including /api/admin; v1/public/webhooks exempt", "company_isolation": [ "tenant APIs RequireCompany scoped", "admin support cross-company ticket view does not open tenant write APIs", "impersonate admin|developer non-prod only; never support_staff", "allowCompanyAdminOrPlatform: only admin|developer count as platform bypass — not support_staff" ], "no_privilege_escalation": [ "capability middleware on admin routes", "no self-service staff_role", "plan features never grant /admin", "staff never granted by plan JSON", "fail closed on missing role/capability" ] }, "performance": { "existing": { "symbol": "featureETag", "path": "apps/api/internal/billing/plan_features.go", "semantics": "sha256 of sorted enabled feature keys" }, "rules": [ "Do not fold staff_role into feature_etag", "Keep company capabilities cache keyed by company_id + feature_etag", "Invalidate on plan assign, plans.features write, global gates write", "etag remains pure function of effective enabled-key set (profile already reflected in map)", "Resolve staff role once per request in middleware" ], "me_payload_additive": { "user.staff_role": "string|null", "staff_capabilities": "string[]", "credits.feature_profile": "legacy|ladder|custom|null", "credits.feature_etag": "unchanged semantics" } }, "migration": { "file_suggestion": "apps/api/sql/schema/027_staff_roles_legacy_profile.sql", "idempotent": true, "destructive": false, "steps": [ "ALTER users ADD staff_role NULL CHECK", "ALTER plans ADD feature_profile NULL CHECK", "Backfill staff_role=admin for existing platform admins", "Ensure Legacy plan row with feature_profile=legacy", "Optional sparse plans.features from LegacyMatrix", "Explicit A1/demo assign — no silent Enterprise downgrade", "EnsureDefaultPlans must not clobber features or feature_profile" ], "optional_flag": "STAFF_RBAC=1 to enforce capability middleware", "breaking_changes": [], "preserve": [ "is_platform_admin", "public ladder names", "Stripe public-ladder checkout", "402 plan_gate shapes", "support assignee_admin_user_id schema", "CSRF session behavior" ] }, "reuse_symbols": { "billing": [ "DefaultPlanFeatures", "SparseDefaultOverrides", "IsCustomPackage", "PlanAllowsFeature", "ResolveEffectiveFeatures", "CapabilitiesForCompany", "featureETag", "EntitlementsForCompany" ], "auth_http": [ "IsPlatformAdmin", "RequirePlatformAdmin", "checkPlatformAdmin", "CSRF", "RequireSession", "allowCompanyAdminOrPlatform" ], "support": [ "ListAdmin", "GetAdmin", "ReplyAsAgent", "UpdateAdmin", "handleAdminListSupportTickets" ], "web": [ "requirePlatformAdmin", "AdminNav", "PlanPermissionsPanel", "planCapabilities", "Nav.svelte" ] }, "do_not_add": [ "parallel permissions package outside billing/auth", "packages table", "tenant feature_keys for /admin/*", "mixing LLM admin-ai-roles into staff RBAC", "staff fields inside feature_etag" ], "verification": [ "legacy excludes processing/stores/marketing/integrations/support", "A1/legacy profile not custom-all-on", "custom non-legacy still all-on", "ladder Free/Starter denials unchanged", "null staff_role + is_platform_admin => admin", "support_staff 403 on billing/plans/settings", "support_staff AdminNav Support-only", "CSRF still on admin mutations", "feature_etag stable across staff_role-only changes", "tenant APIs remain company-scoped for support_staff" ], "open_questions_defaults": { "a1_demo_cutover": "explicit_ops_assign", "support_staff_sets_is_platform_admin": true, "company_admin_platform_bypass": "admin_and_developer_only", "legacy_seed_density": "profile_first_sparse_optional" } }