Actionary treats security as a set of architectural properties — invariants the substrate enforces so a missed check in a repository method, a forgotten filter clause, or a stolen bearer token cannot leak data.
Every control below runs in production. Every one has a spec doc, a code path, and — where the failure mode warrants it — a merge-time gate that fails the build the moment someone drifts.
Tenant isolation is a database property
Every multi-tenant table carries a Postgres row-level security policy keyed on a request-scoped tenant setting. Every request-path connection acquires a runtime role that is NOSUPERUSER NOBYPASSRLS and sets the tenant setting before the first query fires.
A repository method that forgets its tenant clause returns zero rows. A raw psql session under the runtime role returns zero rows. A rogue mass-insert inside a migration returns zero rows. Fail-closed at the substrate.
The connection role has no data-plane authority. The Postgres master user proves membership at handshake time; every request-path transaction then opens by assuming the least-privileged runtime role, and Postgres uses it for every privilege check for the rest of the transaction. Three separate BYPASSRLS roles — one for the staff-platform runtime, one for the migration container, one for the Schema Admin DDL surface — are acquired only inside the specific callsite that needs cross-tenant reach, and never granted to each other. A staff runtime request cannot run DDL. Full enumeration in the tenant isolation spec.
The superuser flag is absent. Everywhere.
The users table has no such column. JWT payloads carry no such claim. The auth-context response exposes no such field. Bearer-token rows omit it. The SPA has no code path that reads it.
Staff identity lives in a physically separate table with its own login endpoint, its own cookie scope, and a three-times-stricter idle window (30 minutes vs 4 hours for tenant sessions). The session kind — one of staff-platform, staff-grant, or tenant — is set at session creation. Bearer JWTs are stripped of any staff-platform session claim at the request boundary, so a captured external token cannot claim staff elevation regardless of what the payload says.
A staff-platform session presented against any tenant-resolved route returns a grant-required error. The only path from staff to tenant data is a customer-issued grant, audit-trailed end to end in the session-event log. SOC 2 CC6.1, made structural.
One master key. HKDF-derived subsystems.
A single platform master key is the single credential, held outside the database it protects. Six subsystem keys derive from it via HKDF-SHA256 with distinct info labels covering MFA, SSO configuration, LLM provider keys, integration tokens, MCP OAuth sessions, and JWT signing.
A leak in one cipher context cannot decrypt another. Adding a subsystem is one HKDF label, not one new environment variable. Rotation is one in-app operator surface — set the previous value as the old master, run the consolidation, drop the old master. Every encrypted column re-encrypts idempotently, composite-PK aware, in a single pass.
Application code never touches secrets. Credentials resolve through cloud-SDK default chains, environment configuration, or IAM roles. The source has zero credential strings.
Thirteen OAuth controls for machine identity
Standards-compliant OAuth authorisation server, full discovery metadata, audit-friendly grant types only. Mandatory PKCE S256 on every flow — the plain variant is structurally rejected at the authorise endpoint. Audience-bound tokens per RFC 8707. Short-lived access tokens, opaque revocable refresh tokens with reuse detection, single-use authorisation codes that delete on consumption.
Client secrets hashed at rest with constant-time comparison, revealed once at creation. Redirect URIs whitelisted, non-loopback HTTP refused. Dynamic Client Registration is opt-in per tenant (RFC 7591), off by default, rate-limited, with idle-client auto-revoke. Hard tenant isolation on every token, refresh row, client, and consent record — cross-tenant reuse is structurally impossible. Full audit trail on every authorise, mint, refresh, revoke, registration, and secret-rotation event.
Five-layer XSS defence. Five-layer agent safety perimeter.
XSS defence layers: closed-allowlist server-side sanitisation at the write boundary (fourteen tags); matching client-side DOMPurify with a force-rel="noopener noreferrer" hook on every anchor; CSP blocking inline scripts as a third backstop; session cookies genuinely unreachable from JavaScript (__Host-prefixed, HttpOnly, browser-refused from document.cookie); and CSRF as a server-bound token held in the SPA’s module-scope memory rather than any DOM-readable surface.
Every agent turn passes through the layered agent safety perimeter: typed tool-result framing (1), a system-prompt directive naming common injection patterns (2), untrusted-content markers on text-shaped paths (3), a tenant tool policy for allow-destructive and require-confirm-destructive dispatch (4), and a tenant content policy of blocked phrases, profanity filter, response-token cap, and tool allow-list (5). The content-policy filter runs after every turn before the assistant text is persisted.
The MCP surface carries ten defensive-design constraints: no tool accepts a credential parameter, zero authentication tools, every tool stateless and idempotent, concrete parameter types over untyped bags, schema constraints in place of prose warnings, no implementation leakage in descriptions, a single canonical server, and a live per-tool p50/p95 anomaly surface for operator triage.
Meeting audio deletes on transcription unless the tenant opts to retain
Three retention modes ship under /settings/uploads: delete after transcription (the platform default — audio is gone the moment the transcript lands, matching the data-minimisation posture enterprise buyers ask for), retain for N days (bounded 1–3650, for coaching or dispute evidence), and keep indefinitely (explicit legal-hold shape for regulated industries).
A dedicated background worker wakes every minute regardless of user activity, purges audio whose window has elapsed, and reports its heartbeat on /platform/operations alongside the workflow and billing workers. Every policy change writes one audit-log row with before / after JSON. The retention promise is a monitored control.
The transcript is the artefact of record. The audio has done its job the moment the words are extracted.
Every uncaught failure is a forensic row
Router code carries no silent catches. The “catch-Exception, return generic error” pattern is structurally absent across every router file — exceptions bubble unconditionally to one global server-side handler that captures full traceback, request id, exception type, tenant, and actor into a forensic table. The browser tier does the same from the standard client-side error events and the SPA root error boundary into a matching frontend forensic table. Both surfaces triage from the SPA with per-transition audit-log rows. No third-party log shipper. No sampling. No ingestion lag.
Error envelopes never leak implementation detail. Detailed exception messages stay on the server; the client sees a generic server error.
The takeaway
Enterprise-grade security is a property the substrate has or does not have. Actionary makes the boundary load-bearing — in the row-level security predicate, in the host-only cookie scope, in the HKDF-derived subsystem key, in the sweeper’s heartbeat. Contracts live in the spec, in the code, and in a merge-time gate. A regression cannot ship. Start at security, tenant isolation, or SOC 2.