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¶
- Open
/admin/tenants/<tenant-id>in the platform-admin dashboard. - Click Add framework.
- 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). - Confirm. The platform writes:
- one row in
tenant_framework_subscriptions, - one
tenant.framework.subscribedaudit log entry, - refreshes
tenants.settings.frameworks.enabled_frameworksfrom the active subscription rows.
The customer's "Your regulations" block on /settings/tenant
refreshes on the next request.
Renewing an annual contract¶
- Open the tenant detail page.
- In the framework's row, set Expires to the new contract end (today + 12 months for an annual contract).
- Optionally update Contract reference to the renewal sales-order id.
- Save. Audit row:
tenant.framework.subscription_updatedwithbefore/aftershowing the expiry change.
Renewals don't create a new subscription row — they update the existing one. The
activated_attimestamp keeps pointing at the original subscription start so historical reporting (months of coverage) stays correct.
Expiring (soft-delete) a subscription¶
- Open the tenant detail page.
- Click Expire next to the framework's row.
- Confirm. The row's
expires_atis set tonow().
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/tenantblock 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:
- Step 1: checkboxes for every known framework. EU AI Act is pre-selected since it's baseline coverage for every paying customer.
- Step 2: for each checked framework, set plan / expiry / contract reference inline.
- Confirm triggers the atomic provisioning:
- new tenant row,
tenant_memberrow granting the requesterorg_admin,- one
tenant_framework_subscriptionsrow per chosen framework, - refreshes the legacy JSONB,
- 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_subscriptionreturns 404 unexpectedly. Check the row'sexpires_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_actrow. 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.