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 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.
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.
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.
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.
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 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 | 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 |
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 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.
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.
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.
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.
A default tenant policy and optional per-database policies define which operations are denied unless an allow rule matches:
denyDocCreate/denyDocChange/denyDocDelete/denyDocUndelete- default falsedenyDocSnapshot/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.
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.
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".
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
Auditable by design, honest about its limits
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.
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.
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.