Security and privacy
The part that is checked, not claimed
Every product in this category has a page like this one. What makes this one worth reading is that each paragraph names a mechanism and links to the document or the gate behind it. If a sentence here has no link, it does not belong here.
Isolation
The tenant boundary is in the database
In multi-workspace mode, separation is not a filter the application remembers to apply. Every table carries the tenant, row level security enforces it, and every query runs inside a transaction that has declared which tenant it acts for. A query that has not is refused by PostgreSQL, not by a code review.
Every repository method carries a cross-tenant negative test, and a gate in the pipeline fails the build when a new method arrives without one. That is the difference between an isolation property and an isolation intention.
multi-tenancy.mdWhere authorisation happens
In the application layer, and nowhere else. Never in an HTTP handler, never in an MCP tool, never in a repository. That is what makes “an agent can do exactly what your role allows” a structural fact instead of three separate implementations that will drift.
ADR-0005Getting in
Sign-in, sessions and credentials
Passwords, done properly
Argon2id, and every token stored as a hash. A credential that leaks from the database is not a credential.
Your identity provider
OpenID Connect for sign-in, and an OAuth2 provider for third-party applications that want to act for a user.
Multi-factor
Time-based one-time codes with single-use recovery codes, enforceable per workspace for the owner and administrator roles.
Step-up
Sensitive operations ask again. Sessions are listable and revocable, and a revoked session stops working immediately.
Tokens with scopes
Personal access tokens and service accounts, scoped rather than all-powerful — the credential an integration or an agent should hold.
Secrets, sealed
Envelope encryption with key rotation for the credentials the system stores on your behalf, such as a backup target’s.
The trail
Tamper-evident, and content-free
Every auditable action is registered in a catalogue — a gate fails the build when a new one is not — and lands in a hash chain. Each entry commits to the one before it, so an altered or removed row breaks the chain at exactly that point. A verification call walks it and names the first broken link.
It stores no content of yours. Not a task title, not a note, not a comment. What is recorded is who did what to which entry and when — the same rule that keeps your text out of logs, metrics and traces. An audit log you would be embarrassed to hand to an auditor is not an audit log.
audit.mdWhat it is good for
- An auditor role Reads the trail and the configuration, and no work items at all.
- Export to your SIEM The trail leaves the system in a form your tooling already reads.
- Agent actions marked An action taken by an AI agent is recorded as one, so “what did the automation do” is answerable.
- No delete grant The application’s database role cannot update or delete an audit row. Not “does not” — cannot.
Data protection
GDPR as operations, not as a policy document
Rights with deadlines
Access, rectification, erasure, restriction and portability are use cases. A request is a record with a statutory deadline the system tracks, so “we will get to it” has a date attached to it.
A catalogue of every field
Personal data is inventoried with its legal basis, its storage location and the path that deletes it — including the copies in a backup archive and the reference in the trail. A gate in the pipeline fails when a new personal field arrives without an entry.
data-catalog.mdRetention with brakes
A rule previews what it would delete, warns in advance, keeps a grace period and can be stopped mid-run. A legal hold overrides it. Writing one asks for the owner’s authority, because a standing instruction to destroy work is not an administrator’s errand.
No content in telemetry
Titles, notes and comments never reach a log line, a metric label, a trace attribute or an audit entry. Redaction is a property of the logging layer rather than a discipline applied per call site.
Where the data sits
Wherever you put it. Self-hosted means self-hosted: there is no control plane, no vendor account, and no default backend anywhere. The one optional outbound path is an AI provider you configure, and an installation that configures none makes no outbound call at all.
The pipeline
Twelve security gates, and one that tests the gates
Security requirements that live in a document get skipped under deadline. These run on every change, and a red one blocks the merge.
Known vulnerabilities
Dependency scanning, secret scanning, an SBOM published with every release, and a signature on every image.
No unguarded outbound call
Every outbound HTTP request goes through a guarded client with timeouts and server-side request forgery protection. There is a gate for the ones that try not to.
No string-built SQL
Queries are generated and parameterised. No byte from a request becomes SQL text, including in the filter language.
No cross-tenant read
A negative test per repository method, and a gate that notices a missing one.
Headers and rate limits
A content security policy without inline execution, security headers, and limits that shed load rather than falling over.
A gate that fails on purpose
A self-test deliberately breaks each rule and proves the gate turns red. A gate nobody has seen fail is a gate nobody should trust.
Reporting a vulnerability. The path, the response aims — best effort, not
deadlines — and the advisory process are in SECURITY.md in the repository. Please use it rather than an issue.