Plan 103 — Frameworks as Subscription Products¶
Status: OPEN
Priority: P1 — sales-driven onboarding requires this. Sequenced after Plan 96 RICS pack final wave; ship before second paying customer.
Captured: 2026-05-09
Depends on: Plan 92 (Customer onboarding flow — shipped v0.10.0), Plan 96 WI-2 (multi-framework load + per-tenant enabled_frameworks — shipped v0.11.24), Plan 93 (usage metering — P2, future Stripe integration).
Why¶
Today (2026-05-09), tenants.settings.frameworks.enabled_frameworks (Plan 96 WI-2) is a flat list — platform admin flips frameworks on a tenant via PATCH /tenant/settings/frameworks. This works for prototype delivery but doesn't scale to a sales process where each customer buys a specific bundle.
Per founder direction 2026-05-09:
"Klient docelowo nie może mieć wszystkiego. Przy onboardingu admin powinien wybrać do jakich regulacji klient powinien mieć dostęp."
Each framework is a product / SKU with subscription metadata. During onboarding, platform admin selects the regulation bundle for that customer's contract.
Out of scope¶
- Stripe billing per framework — Plan 93 territory; Plan 103 ships subscription metadata only, billing reconciliation is Phase 2
- Self-service framework upgrade — customer-driven activation deferred until ≥3 paying customers and operational maturity around revenue recognition
- Framework deprecation / sunset (e.g. RICS PS Sep 2025 → v2 in 2027) — defer; needs versioning strategy beyond this plan
- Trial / freemium tier per framework — defer; sales motion is direct-contracted for now
- Multi-currency billing parity — Plan 93 dependency; framework subscription metadata is currency-agnostic
Schema¶
tenant_framework_subscriptions (new platform-level table)¶
CREATE TABLE tenant_framework_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
framework_id TEXT NOT NULL, -- 'eu_ai_act' / 'rics' / 'uk_defence_ai_playbook' / etc.
plan TEXT NOT NULL CHECK (plan IN ('basic','full')), -- defines unlocked features per framework
activated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NULL, -- NULL = perpetual; renewal management Phase 2
contract_reference TEXT, -- internal sales reference; Stripe sub id when Plan 93 ships
notes TEXT,
created_by_user_id UUID REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, framework_id)
);
CREATE INDEX ix_tenant_framework_subscriptions_tenant
ON tenant_framework_subscriptions (tenant_id);
CREATE INDEX ix_tenant_framework_subscriptions_active
ON tenant_framework_subscriptions (tenant_id, framework_id)
WHERE expires_at IS NULL OR expires_at > now();
The derived view tenants.settings.frameworks.enabled_frameworks (Plan 96 WI-2) becomes computed from the active subscription rows rather than being the source of truth. Migration backfills existing tenants:
- All tenants get an eu_ai_act row (perpetual, plan='full', notes='backfilled from pre-Plan-103 default')
- Tenants with rics in current enabled_frameworks get a rics row
The enabled_frameworks JSONB list stays for backward compatibility but is read from the subscription view at GET time.
Routes (cp-api)¶
Platform admin¶
GET /admin/tenants/{id}/frameworks— list active subscriptions for a tenant (already exists from Plan 96 WI-2; extends to include subscription metadata)POST /admin/tenants/{id}/frameworks— body:{framework_id, plan, expires_at?, contract_reference?, notes?}— adds a subscription. Audit log entrytenant.framework.subscribedwith full context.PATCH /admin/tenants/{id}/frameworks/{framework_id}— update plan / expires_at / contract_reference / notesDELETE /admin/tenants/{id}/frameworks/{framework_id}— soft-delete (sets expires_at to now()); audit log entrytenant.framework.unsubscribed
Tenant-facing (org_admin only, read-only)¶
GET /tenant/settings/frameworks— extends Plan 96 WI-2 endpoint to include subscription metadata in the response (active subs, plan tier, expires_at)- No PATCH for org_admin — they can't self-activate frameworks
Onboarding integration¶
POST /admin/onboarding-requests/{id}/approve (Plan 92) gains a body field:
{
"framework_subscriptions": [
{"framework_id": "rics", "plan": "full", "expires_at": null, "contract_reference": "RLB-2026-01"},
{"framework_id": "uk_defence_ai_playbook", "plan": "full", "expires_at": null, "contract_reference": "RLB-2026-01"}
]
}
Approval atomically:
1. Creates tenant, tenant_member
2. Creates tenant_framework_subscriptions rows for each chosen framework
3. Backfills tenants.settings.frameworks.enabled_frameworks from the new rows
4. Audit log entries for tenant + each subscription
Frontend¶
Admin onboarding approval modal¶
Plan 92's /admin/onboarding-requests/{id} detail page approval action gains a framework picker step:
- Click "Approve" → modal opens with framework checkboxes
- Each framework: plan radio (basic / full), expiry date picker (optional), contract reference text input
- Confirm → atomic apply
Admin tenant detail (Plan 96 WI-2 extension)¶
/admin/tenants/{id}/page.tsx already shows enabled frameworks toggle. Extends to subscription editor:
- Per active framework: plan, expiry, contract ref, notes (inline editable)
- "Add framework" button → opens picker for non-active frameworks
- "Expire framework" button → soft-deletes the subscription
Customer-facing tenant settings¶
/settings/tenant/page.tsx (Plan 74) gains a "Your regulations" read-only block:
- List of active subscriptions with plan tier, expiry date
- Footer: "Want another regulation? Contact sales"
- No edit controls — sales-controlled
Per-framework feature gating¶
Existing endpoints add subscription checks:
GET /export/ai-disclosure(Plan 96 WI-5) — already gated onenabled_frameworkscontainingrics; Plan 103 makes this explicitly look up the subscription, returning 404 with detail "RICS subscription required" when missingGET /exports/firm-ai-register(Plan 96 WI-6) — same- Future:
GET /export/defence-ai-statement(Plan 99 WI-3) — gated onuk_defence_ai_playbooksubscription - Future:
GET /export/ds-05-138-evidence(Plan 100 WI-4) — gated onds_05_138subscription
Helper in cp-api:
async def require_framework_subscription(
db: AsyncSession,
tenant_id: UUID,
framework_id: str,
) -> None:
"""Raise 404 if the tenant doesn't have an active subscription to the framework."""
...
Audit log events¶
tenant.framework.subscribed— context: framework_id, plan, expires_at, contract_reference, actor_user_idtenant.framework.unsubscribed— context: framework_id, expires_at_set_to, actor_user_idtenant.framework.subscription_updated— context: framework_id, before/after diff
Effort¶
| Block | Deliverable | Effort |
|---|---|---|
| Backend | Migration + tenant_framework_subscriptions table + 4 endpoints + onboarding integration + helper |
4-5d |
| Frontend | Admin framework picker modal (in onboarding approval) + admin tenant detail editor + customer read-only block | 3d |
| Tests | Integration (subscription lifecycle, onboarding atomicity, RLS) + Playwright (admin picker, customer read-only) | 2d |
| Docs | docs/runbooks/framework-subscription-management.md for sales/ops + docs/sources/per-framework-pricing-tiers.md placeholder |
1d |
Total: ~10 days FT.
Acceptance¶
- Onboarding approval atomically creates tenant + framework subscriptions in one transaction
- Existing tenants migrated to have
eu_ai_actsubscription rows (no behaviour change) - Platform admin can add / edit / expire frameworks per tenant via
/admin/tenants/{id}UI - Org_admin sees read-only "Your regulations" block on
/settings/tenant - All export endpoints (Plan 96 WI-⅚, future Plan 99/100 exports) check subscription before serving
- Audit log captures every subscription state change
- RLS isolation: a tenant cannot see another tenant's subscription metadata
Open questions¶
- Plan tier semantics — what features differ between
basicandfullfor each framework? Default v1: onlyfullexists;basicis a forward-compat placeholder for future tiered features (e.g. "RICS basic = rule classification only; RICS full = rules + exports + connector signals"). Confirm during first sales conversation post-RLB. - Contract reference format — free-text for v1; structured (Stripe sub id format) when Plan 93 ships
- Expires_at default — v1 default is NULL (perpetual); annual contracts set to +12 months from activation. Confirm with sales/founder during onboarding playbook iteration.
- Multi-counsel framework alignment — when Plan 97 ships per-framework counsel routing, the counsel's framework_id should match a tenant's active subscription before the counsel can review rules for that tenant. Cross-link in Plan 97 spec.