Platform · Governance & Access Control

Governance, built into the core

In MindooDB, governance is a property of the data layer itself - not an add-on bolted onto a trusted server or application tier. Access policy, identity, and a cryptographically provable, point-in-time-reproducible audit trail are stored and enforced in the database core. The payoff: you can prove not just what changed, but who was allowed to change it - at the moment it actually entered the tenant.

The access control layer has two halves. The write side adds fine-grained control over write operations (doc_create, doc_change, doc_delete, doc_undelete, doc_snapshot, doc_purge), enforced even when users work offline and cannot be trusted to report an honest timestamp. The read side governs who may see data: admin-signed read policies gate what the server delivers and auto-purge data locally when access is revoked - layered on top of MindooDB's encryption, which already makes a document ciphertext to anyone without the key (see also the encryption-based read model).

Governance built into the core: client devices send signed write requests through a policy gate of admin-signed rules; allowed writes reach the encrypted database while denied writes are blocked, with an audit timeline showing point-in-time authorization
Why this is governance, not just permissions

Governance is a property of the data layer

Most stacks push governance up to the server or application: the database stores rows, and something else decides who may touch them and keeps the log. MindooDB inverts that. Policy, identity, full history, and the audit trail live in the same end-to-end-encrypted, append-only stores - so the guarantees travel with the data across servers, peers, and offline devices, and survive a fully compromised server.

Policy as versioned data

Policies and rules are admin-signed documents in the directory database, themselves append-only. The exact moment governance was switched on - or temporarily disabled - is part of the permanent record, not a config change that leaves no trace.

Reproducible authorization

Every entry carries a trusted time, and the directory keeps a time-travel chain of its own history. So wasAllowedAt(op, user, dbid, at) can replay what the rules were and who was authorized at any past moment - a deterministic verdict every honest replica agrees on.

Provable accountability

Every change is Ed25519-signed for non-repudiation and witnessed against a trusted clock. Violations aren't silently dropped - they're recorded in a per-tenant quarantine/audit log surfaced in Haven, so even rejected attempts are accountable.

Identity, revocation & erasure

Grants hold per-device key arrays; revocation is simply removing keys, and an explicit remote device wipe drops the whole tenant from a stolen or departed device on next connect - the identity lifecycle is governed in the same signed directory.

The core idea

The two-tier model

Every rule falls into one of two tiers, based on what the sync server can see. The server only ever handles ciphertext plus a small set of cleartext metadata - it can never read document bodies. That single distinction is the whole architecture: it lets MindooDB make an honest promise about exactly what is cryptographically guaranteed versus what is enforced by policy among cooperating clients.

Tier 1 vs Tier 2
Tier 1 identity rules check author, database and operation type and are enforced by both server and clients; Tier 2 content rules check withfields and are enforced by clients only
A rule is Tier 2 if and only if it has a withfields clause. Everything else is Tier 1.
How the two tiers compare
Tier What it checks Enforced by Strength
Tier 1 - Identity Author identity, target database, operation type Server and clients Cryptographic - the server refuses to witness a violating entry, so it cannot propagate
Tier 2 - Content The actual document content (withfields) Clients only Policy - gates honest clients and shapes UX. A tampered client can only bypass it locally; every honest replica re-checks withfields on receipt and quarantines a violating change, so it never becomes visible to anyone else
The offline-clock problem

Witness receipts: a trusted clock you don't have to trust the client for

The hardest question in an local-first system is "which clock do we trust?". A user working offline could backdate their own entries to slip a change past a policy. MindooDB answers this with a witness receipt: an attestation, signed by a trusted witness (your sync server), that an entry was accepted at a specific time. Each enforcement point then uses exactly one well-defined clock.

The write lifecycle
An author device creates an entry and pushes it; the server witness checks Tier 1, stamps receivedAt and signs a receipt; other replicas pull and trust the receipt. A denied entry stays local and cannot sync.
A user who has lost a right simply cannot sync the offending change - the offline clock can never be used to backdate around a policy.
Scenario A
Write locally

The SDK evaluates Tier 1 + Tier 2 against the user's local directory state at the current local time. If allowed, the entry is stored locally with no witness fields and is visible immediately on this device. A user's own clock governs their own local-only view - that's fine, because the change hasn't entered the shared tenant yet.

Scenario B
Push to a server

The server evaluates Tier 1 against its own state at server time. If allowed, it stamps receivedAt, records the witness key, signs the receipt, and returns the witness fields so they flow back to the sender and onward. If denied, it returns a structured AccessDenied and the entry stays local - it cannot propagate.

Scenario C
Pull from a server

The receiver verifies the receipt signature against its trusted-witness list. A valid signature means Tier 1 was satisfied at receivedAt, so by default it isn't re-evaluated. The receiver then checks Tier 2 locally and either materializes the change or routes it to a local quarantine/audit log.

Rules, policies & identities

How decisions are configured

All access-control state lives in the admin-only directory database and syncs to every participant. Everything the server needs for Tier 1 is encrypted with the $publicinfos key so it can be read without holding the default tenant key; withfields content is never readable by the server. Every mutating call is admin-signed.

Policies set the baseline

A default tenant policy and optional per-database policies define which operations are denied unless an allow rule matches:

  • denyDocCreate / denyDocChange / denyDocDelete / denyDocUndelete - default false
  • denyDocSnapshot / denyDocPurge - default true (admin-only)
  • disableAllAccessChecksAndPolicies - an explicit master off switch that short-circuits every check, including standalone deny rules

A brand-new tenant has no policy document at all, so everything is allowed until an admin writes one. Every revision is appended to history, so the exact enable/disable window stays auditable. Crucially, each change is judged against the policies that were active at its own trusted time (receivedAt): changes that entered the tenant after activation are covered, while earlier ones resolve against the implicit all-allow default - so pre-existing data is grandfathered in automatically, with no migration step.

Rules decide with deny-overrides-allow

Each rule targets an operation type and a database ("*" = all), and lists the users or groups it applies to. Evaluation is set-based and order-independent:

  • any matching deny rule → denied
  • else any matching allow rule → allowed
  • else the baseline policy decides

Order-independence matters because rule documents merge across replicas via CRDTs - no rule ordering to disagree about.

A server can only reach a Tier 1 verdict. If the only thing standing between allow and deny is a Tier 2 content rule, it treats the entry as Tier 1-allowed and leaves the withfields check to the clients. Every decision returns a structured result - allowed, a human-readable reason, the matchedRuleId, and the tier - which the SDK uses to grey out actions a user can't perform.

withfields: content constraints

A withfields clause checks a dot-path inside the document with a closed set of operators (equals, contains, gt, …) and placeholders like ${user.usernames}. Each clause is evaluated against a chosen document state:

  • before - the existing document (default for change/delete): "you must already be an editor". Evaluating after would let someone add themselves and authorize their own edit.
  • after - the document with the change applied (default for create): "the creator must add themselves to myeditors".
Identities, groups & revocation

Rules match against a user's hashed username, their group hashes (including nested groups), and reserved pseudo-tokens:

  • $everyone - all registered users
  • $admin - admin only
  • $author - the original creator of the document (Tier 1, ownership model)

Revocation is performed by removing keys from the admin-signed grant document - no separate revocation docs. An admin can also issue an explicit, opt-in remote device wipe by signing key, dropping the tenant from a stolen or departed device the next time it connects.

Worked example

A CRM with per-record editors

This walks one realistic policy end-to-end so the moving parts line up. The tenant has a crm database, and we want four rules: everyone may create contacts but must put themselves in myeditors; only an already-listed editor may change a contact; only the original creator may delete it; and the HR group may change anything as a supervisor escape hatch.

Alice creates a contact

Baseline is deny. Rule 1 matches via $everyone, and its withfields passes because the after state's myeditors contains Alice → allowed. The server only confirms Tier 1, then witnesses the entry.

Bob (not an editor) tries to change it

Rule 2 matches by $everyone, but its withfields fails: the before state's myeditors doesn't contain Bob → denied locally, even if his change tries to add himself. A tampered client could push it, but every honest receiver quarantines it on materialization.

Alice deletes & HR overrides

Rule 3 matches via $author - the creator key and the delete signer resolve to the same user → allowed and witnessed (Tier 1, survives a malicious client). Rule 4 lets any HR-group member change any contact regardless of myeditors.

This shows the division of labor: Tier 1 rules ($author, group hr) are enforced at the server and survive a malicious client; Tier 2 rules (the myeditors content checks) are enforced by every honest client and quarantined on receipt if violated.

The read side

Read access control

Write rules decide who may change data; read access decides who may see it. On top of plain key possession, an admin can publish $publicinfos-encrypted read policies and rules - a default-allow/deny baseline per tenant or database, plus rules scoped by database and optional decryptionKeyId, targeting users, groups, $everyone or $admin with the same deny-overrides-allow evaluation. It is opt-in: with no read policy, read access is unrestricted (key possession is the only gate, exactly as before).

Server delivery gate (the strong layer)

The zero-trust server resolves the authenticated user and only ships entries they are entitled to read, filtering by the cleartext dbid + decryptionKeyId against trusted server time. Disallowed entries are simply never delivered. The directory database itself is never read-gated - it carries the very policies the gate depends on.

Cooperative client purge + crypto-shred

An honest client that loses entitlement deletes the already-synced ciphertext and crypto-shreds the affected key from its KeyBag, so the scope can't be re-materialized. Because the author's signature is over the ciphertext, history is never re-encrypted in place; revocation gates future delivery and purges existing copies. Best-effort and fails-open - the server gate is the authority.

No client-trusted dates

Read rules carry no notAfter a client could evade by setting its clock back. Time-bound access is revocation by policy revision: an admin (or automation) flips the rule to deny, and the next directory sync purges the scope - clock-spoofing-proof and deterministic. wasAllowedToReadAt(user, dbid, key, at) audits read decisions at any past time.

Admin-blind key delivery

Granting read access usually means handing over a key. A key-holding user RSA-wraps a key to each recipient's public key; the admin merely signs and publishes the wrapped bundle and never sees the plaintext key. Clients detect deliveries addressed to them on sync, unwrap, and the existing reveal-on-add path surfaces the now-readable documents.

Audit & scope

Auditable by design, honest about its limits

Reproducible for any point in time

Because each entry carries a trusted time (receivedAt, falling back to createdAt for local-only entries) and the directory keeps a time-travel chain of its own history, you can answer "was user X allowed to change this document when the change actually entered the tenant?" - and reconstruct exactly how a user's access changed over time. A wasAllowedAt(op, user, dbid, at) query makes this a first-class API. Tier 2 violations don't disappear silently; they're recorded in a per-tenant quarantine log surfaced in Haven's audit view.

v1 scope and non-goals

The layer governs both writes and reads (document- and key-level). It cannot stop a tampered client from authoring an entry locally, but it prevents that entry from being accepted into the tenant, and the server read gate stops unentitled data from ever reaching a client. v1 targets server-mediated sync as the witness; richer peer-to-peer witnessing and true field-level read control (per-field keys) are tracked as future work. History is never re-encrypted in place, since author signatures are over the ciphertext.

See it in action
Haven - the workspace built on this access model

MindooDB Haven puts the same end-to-end encryption, signed history, and built-in governance into a calm, cross-platform workspace - including the audit view that surfaces quarantined changes. Try the free beta or explore the deep dives.