Operating principles¶
The non-negotiables. These are the rules of the road that constrain every product decision, every feature, and every piece of code that ships.
Tenant isolation¶
One customer organisation, one tenant. Tenants share infrastructure
but never share data. Every customer table has a tenant_id column
and a Postgres row-level security policy keyed off the session
variable app.current_tenant_path (an ltree value, set at the start
of every authenticated request).
The session variable is bound to the tenant at session-token issue time. If a request arrives without it, RLS returns zero rows — the platform fails closed.
There is no admin API that aggregates across tenants. Internal
Secruna staff who need cross-tenant access do so through a separate
/admin/* surface that is logged to audit_log with the platform
admin's identity stamped on every row.
Audit log¶
Every mutation writes at least one row to audit_log. Each row
records the actor, the entity, the diff, the request id, the IP, and
a hash chain back to the previous row. The chain means a tampered
row breaks downstream verification — silent edits are detectable.
Org admins can read and export the log through the dashboard. There is no API surface that mutates state without auditing. There is no "silent" admin path.
Magic-link 4-eyes flows¶
Some actions need a second human. The pattern is a magic link emailed to a designated reviewer (Plan 71 introduced this primitive for tenant member management; Plan 97 extends it to per-framework counsel review). The reviewer clicks, lands on a single-purpose page, approves or rejects, and the audit log records both the originator and the reviewer.
This is not OAuth. It's a signed, single-use, time-bounded URL. Magic links never give the reviewer a tenant session — they only let them act on the one decision they were summoned for.
Webhook fan-out¶
Customers configure webhooks at /settings/webhooks. The cp-api
fans out events on a small fixed event vocabulary — discovery
completed, verdict changed, audit log row, member invited.
Subscriptions are per-event-type, signed with a per-tenant secret,
and retried with exponential back-off.
The fan-out is best-effort: webhooks are a notification channel, not the system of record. The dashboard and the audit log remain the authoritative surfaces.
Discovery: cron and event-triggered¶
Phase 1 of the discovery worker (shipped) runs every two minutes via a Container Apps Job cron. Phase 2 (Plan 61 P2, partially shipped) adds an explicit invocation path so cp-api can kick the job immediately when a customer clicks "Run discovery", reducing the worst-case latency from two minutes to roughly thirty seconds.
A future Phase 3 (Plan 79) will add per-connection schedules — daily, weekly, on-demand — so high-churn tenants run more often than quiet ones.
ntfy deploy notifications¶
Every deploy fires a notification to a self-hosted ntfy.sh topic (ADR 006). The topic is silent on success but loud on failure. This is a small, deliberate choice: it keeps the on-call engineer in the loop without requiring a full PagerDuty integration before there's revenue to justify it.
Subscription gating¶
Per Plan 103, every tenant has a framework_subscriptions row. The
rule loader, the dashboard navigation, and the export endpoints all
read from it. A tenant subscribed only to RICS will not see EU AI
Act rules, will not run them in discovery, and will not be billed
for them.
Row-level security via app.current_tenant_path¶
The full mechanism is documented in How it works,
but the short version: RLS policies on every customer table compare
tenant_id (or, more often, tenant_path) to the session variable.
The session variable is an ltree rather than a UUID so that
hierarchical tenant relationships (parent organisation → subsidiary)
can be expressed cleanly when we need them — today, every tenant is a
single-node path.