Skip to content

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.

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.