Przejdź do treści

Framework Subscription Management — Runbook

Audience: Sales / ops / platform admins Plan reference: Plan 103 — Frameworks as Subscription Products Owner: Platform team


What this is

Each Secruna customer has a set of framework subscriptions — contracted access to one or more regulatory rule books (EU AI Act, RICS, future: UK Defence AI Playbook, DS-05-138, …). Adding / removing / renewing a framework on a tenant is a sales-controlled action; this runbook covers the platform-admin operations behind it.

Source of truth: tenant_framework_subscriptions table. The legacy tenants.settings.frameworks.enabled_frameworks JSONB list stays populated as a derived view of the subscription rows for backward compatibility — never edit it by hand.

Adding a framework after a contract closes

  1. Open /admin/tenants/<tenant-id> in the platform-admin dashboard.
  2. Click Add framework.
  3. Pick the framework, plan tier (basic / full), optional expiry, and a contract reference (Stripe sub id when Plan 93 ships; internal sales reference until then — e.g. RLB-2026-01).
  4. Confirm. The platform writes:
  5. one row in tenant_framework_subscriptions,
  6. one tenant.framework.subscribed audit log entry,
  7. refreshes tenants.settings.frameworks.enabled_frameworks from the active subscription rows.

The customer's "Your regulations" block on /settings/tenant refreshes on the next request.

Renewing an annual contract

  1. Open the tenant detail page.
  2. In the framework's row, set Expires to the new contract end (today + 12 months for an annual contract).
  3. Optionally update Contract reference to the renewal sales-order id.
  4. Save. Audit row: tenant.framework.subscription_updated with before / after showing the expiry change.

Renewals don't create a new subscription row — they update the existing one. The activated_at timestamp keeps pointing at the original subscription start so historical reporting (months of coverage) stays correct.

Expiring (soft-delete) a subscription

  1. Open the tenant detail page.
  2. Click Expire next to the framework's row.
  3. Confirm. The row's expires_at is set to now().

Soft-delete semantics:

  • The row is kept in the table — never DELETEd. The audit trail survives renewals + cancellations + accidental clicks.
  • The framework gating helper (require_framework_subscription) treats the row as inactive immediately. The customer's /settings/tenant block drops it on the next refresh.
  • The legacy JSONB list refreshes to exclude the now-expired framework_id.

To un-expire a soft-deleted subscription, use the PATCH editor to set expires_at back to NULL (perpetual) or a future timestamp. A new audit row records the reactivation.

Onboarding approval — provisioning subscriptions atomically

When approving an onboarding request from /admin/onboarding-requests/<id>, the Approve modal presents a 2-step framework picker:

  1. Step 1: checkboxes for every known framework. EU AI Act is pre-selected since it's baseline coverage for every paying customer.
  2. Step 2: for each checked framework, set plan / expiry / contract reference inline.
  3. Confirm triggers the atomic provisioning:
  4. new tenant row,
  5. tenant_member row granting the requester org_admin,
  6. one tenant_framework_subscriptions row per chosen framework,
  7. refreshes the legacy JSONB,
  8. audit log entries: onboarding.request.approved (one) + tenant.framework.subscribed (one per framework).

If the request fails halfway (slug collision retry exhausted, etc.) nothing is committed — the failure is rolled back to a clean state.

If the modal is bypassed (legacy curl / older frontend), the approval defaults to a single perpetual eu_ai_act subscription so existing flows don't break.

Audit log queries

All subscription mutations land in the audit log. Common queries:

-- "Who subscribed which tenant to RICS in the last 30 days?"
SELECT created_at, actor_user_id, resource_id, context
FROM audit_log
WHERE action = 'tenant.framework.subscribed'
  AND context->>'framework_id' = 'rics'
  AND created_at > now() - interval '30 days'
ORDER BY created_at DESC;

-- "Show every subscription change on tenant <id>."
SELECT created_at, action, context
FROM audit_log
WHERE tenant_id = '<tenant-id>'::uuid
  AND action IN (
    'tenant.framework.subscribed',
    'tenant.framework.subscription_updated',
    'tenant.framework.unsubscribed'
  )
ORDER BY created_at DESC;

-- "Subscriptions due to expire in the next 30 days."
SELECT t.name, tfs.framework_id, tfs.expires_at, tfs.contract_reference
FROM tenant_framework_subscriptions tfs
JOIN tenants t ON t.id = tfs.tenant_id
WHERE tfs.expires_at IS NOT NULL
  AND tfs.expires_at BETWEEN now() AND now() + interval '30 days'
ORDER BY tfs.expires_at ASC;

Things that look like bugs but aren't

  • The legacy JSONB still has the framework after I expired it. The handler refreshes the JSONB inside the DELETE transaction — if you're reading from a cached page, hard-refresh. The DB value is always correct.

  • A second POST on the same framework returns 409. Subscriptions use UNIQUE (tenant_id, framework_id). To re-subscribe a tenant to a framework they previously had expired, PATCH the existing row back to active (expires_at → null or a future date). This keeps the audit trail continuous instead of fragmenting it across multiple rows.

  • Org admin can't change frameworks. By design (Plan 103 §Don't). Self-service framework upgrade is deferred until ≥3 paying customers

  • revenue-recognition operational maturity. Customers email hello@secruna.com — the "Your regulations" block carries the link.

Troubleshooting

  • require_framework_subscription returns 404 unexpectedly. Check the row's expires_at — a row whose expiry is in the past is treated as inactive. Use the PATCH editor to extend the expiry.

  • Migration backfill missed a tenant. The 0030 migration backfills every non-deleted tenant with an eu_ai_act row. Tenants created AFTER the migration ran do NOT auto-receive a subscription — they must come through the onboarding-approval flow, which provisions subscriptions atomically. If a tenant got past onboarding without any subscriptions (manual fixture / data cleanup), seed via the admin POST endpoint.