Przejdź do treści

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 entry tenant.framework.subscribed with full context.
  • PATCH /admin/tenants/{id}/frameworks/{framework_id} — update plan / expires_at / contract_reference / notes
  • DELETE /admin/tenants/{id}/frameworks/{framework_id} — soft-delete (sets expires_at to now()); audit log entry tenant.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:

  1. Click "Approve" → modal opens with framework checkboxes
  2. Each framework: plan radio (basic / full), expiry date picker (optional), contract reference text input
  3. 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 on enabled_frameworks containing rics; Plan 103 makes this explicitly look up the subscription, returning 404 with detail "RICS subscription required" when missing
  • GET /exports/firm-ai-register (Plan 96 WI-6) — same
  • Future: GET /export/defence-ai-statement (Plan 99 WI-3) — gated on uk_defence_ai_playbook subscription
  • Future: GET /export/ds-05-138-evidence (Plan 100 WI-4) — gated on ds_05_138 subscription

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_id
  • tenant.framework.unsubscribed — context: framework_id, expires_at_set_to, actor_user_id
  • tenant.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_act subscription 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

  1. Plan tier semantics — what features differ between basic and full for each framework? Default v1: only full exists; basic is 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.
  2. Contract reference format — free-text for v1; structured (Stripe sub id format) when Plan 93 ships
  3. Expires_at default — v1 default is NULL (perpetual); annual contracts set to +12 months from activation. Confirm with sales/founder during onboarding playbook iteration.
  4. 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.