Files
descrybe/docs/plan-permissions/03-permission-contract.json
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

229 lines
7.3 KiB
JSON

{
"agent": "03",
"title": "Plan permission contract",
"status": "design_only",
"version": "1.0.0",
"coordinates_with": [
"docs/plan-permissions/01-dashboard-feature-catalog.md",
"docs/plan-permissions/01-feature-keys.json",
"docs/plan-permissions/02-plans-permissions-current.md",
"docs/plan-permissions/02-extension-points.json"
],
"assumptions": [
"Package equals plans row; no separate packages table",
"is_custom=true (including seeded Enterprise) defaults all features ON unless override false",
"Meters (max_products, credits, can_use_ai) remain; feature keys gate surfaces/actions",
"Client canFeature fail-open when features map missing until API cutover"
],
"runtime_formula": {
"effective": "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 plans.is_custom -> true",
"else -> DefaultMatrix[normalizePlanName(name)][key] (unknown -> false unless catalog All-plans)"
],
"global_missing_default": true,
"parent_prefix_rule": "UI treats disabled parent as disabling children; Assert uses specific action key"
},
"data_model": {
"plans_features_column": {
"table": "plans",
"column": "features",
"type": "JSONB NOT NULL DEFAULT '{}'",
"semantics": "sparse overrides only; not full expanded matrix"
},
"platform_feature_gates": {
"table": "platform_feature_gates",
"columns": {
"gate_key": "TEXT PRIMARY KEY",
"kind": "TEXT CHECK IN ('section','feature')",
"enabled": "BOOLEAN NOT NULL DEFAULT true",
"updated_at": "TIMESTAMPTZ",
"updated_by": "UUID NULL REFERENCES users(id)"
},
"section_keys": [
"shell",
"dashboard",
"catalog",
"feeds",
"stores",
"processing",
"marketing",
"integrations",
"billing",
"settings",
"support",
"capabilities"
]
},
"do_not_add": [
"packages table",
"parallel permissions package outside billing",
"v1 company-level feature overrides"
]
},
"feature_key_namespace": {
"authority": "docs/plan-permissions/01-feature-keys.json",
"pattern": "section.leaf(.subleaf)* snake_case",
"capability_prefix": "capability.",
"exclude": [
"/admin/*"
]
},
"default_matrix_sketch": {
"custom_and_enterprise_is_custom": "all_true_unless_override",
"free_denies": [
"catalog.products.process_ai_titles",
"catalog.products.process_ai_descriptions",
"marketing.campaigns.generate_ai",
"marketing.campaigns.send",
"marketing.brand_ai_apply",
"marketing.seo.ai_rewrite",
"integrations.ai.byok",
"settings.api_keys",
"capability.ai_processing",
"capability.campaign_ai",
"capability.email_live_send",
"capability.brand_ai_apply",
"capability.seo_ai_rewrite",
"capability.api_access",
"capability.byok"
],
"starter_plus_allows_ai_surfaces": true,
"growth_byok_default": true,
"all_plans_core": [
"shell.navigation",
"dashboard.overview",
"catalog.products",
"catalog.products.process_categories",
"catalog.products.process_attributes",
"feeds.list",
"feeds.export_feeds",
"stores.hub",
"processing.monitor",
"billing.overview",
"settings.profile",
"support.center",
"capability.normalize_specs_fill",
"capability.eprel",
"capability.sku_cap",
"capability.ai_credits"
],
"notes": [
"Expand leaves from 01 catalog; parent deny implies children deny for UI",
"capability.sku_cap / ai_credits enable hooks; numeric limits stay on plans meters"
]
},
"api": {
"admin_plans": {
"list": "GET /api/admin/plans",
"upsert": "POST /api/admin/plans",
"plan_fields_added": [
"features",
"resolved_features"
],
"features_write_semantics": "replace stored overrides object"
},
"admin_feature_gates": {
"list": "GET /api/admin/feature-gates",
"put": "PUT /api/admin/feature-gates",
"put_section": "PUT /api/admin/feature-gates/sections/{section}",
"auth": "RequirePlatformAdmin",
"response_shape": {
"sections": {
"marketing": true
},
"features": {
"capability.byok": false
}
}
},
"user_capabilities": {
"embed_in_credits_overview": true,
"endpoints": [
"GET /api/billing/credits",
"GET /api/auth/me",
"GET /api/billing/capabilities"
],
"credits_overview_fields_added": [
"features",
"disabled_features",
"feature_etag"
],
"capabilities_response": {
"plan_id": "int64",
"plan_name": "string",
"is_custom": "bool",
"features": "map[string]bool effective full registry",
"sections": "map[string]bool global section enables",
"entitlements": {
"can_use_ai": "bool",
"can_use_eprel": "bool",
"is_free_plan": "bool",
"is_paid_plan": "bool"
}
}
},
"enforcement_error": {
"http_status": 402,
"body": {
"error": "feature_disabled",
"code": "plan_gate",
"feature": "marketing.campaigns.generate_ai",
"upgrade_url": "/plans"
}
}
},
"backend_symbols_to_extend": {
"reuse": [
"apps/api/internal/billing/entitlements.go#EntitlementsForCompany",
"apps/api/internal/billing/entitlements.go#ComputeEntitlements",
"apps/api/internal/billing/service.go#Plan",
"apps/api/internal/billing/service.go#CreditsOverview",
"apps/api/internal/billing/service.go#UpsertPlan",
"apps/api/internal/billing/service.go#EnsureDefaultPlans",
"apps/api/internal/billing/service.go#AssertCanStartProcessing",
"apps/api/internal/httpapi/billing_handlers.go",
"apps/web/src/routes/admin/billing/+page.svelte",
"apps/web/src/lib/billing-display.ts",
"apps/web/src/lib/plan-gates.ts",
"apps/web/src/lib/components/Nav.svelte"
],
"new_symbols": [
"DefaultPlanFeatures",
"ResolveFeatures",
"FeaturesForCompany",
"AssertFeature",
"canFeature"
]
},
"migration": {
"file_suggestion": "apps/api/sql/schema/026_plan_features.sql",
"idempotent": true,
"destructive": false,
"steps": [
"ALTER TABLE plans ADD COLUMN IF NOT EXISTS features JSONB NOT NULL DEFAULT '{}'",
"CREATE TABLE IF NOT EXISTS platform_feature_gates (...)",
"EnsureDefaultPlans must not clobber non-empty features",
"Migrator inserts '{}' when source lacks column"
]
},
"frontend_gating": {
"helper": "canFeature(credits, key)",
"nav_filter": "Nav.svelte primaryItems + moreItemsAll by parent feature_key",
"fail_open_until_cutover": true,
"optional_env": "FEATURES_ENFORCE=1"
},
"breaking_changes": [],
"breaking_changes_policy": "prefer_none_additive_only",
"preserve_contracts": [
"can_use_ai",
"can_use_eprel",
"is_free_plan",
"is_paid_plan",
"public plan names Free/Starter/Growth/Business/Enterprise",
"Stripe checkout public-ladder-only",
"402 plan_gate for existing credit/SKU errors"
]
}