Integrity is a property of the data layer.
Most GRC platforms enforce auditor-grade behavior through RBAC configuration that admins can change. We enforce it architecturally — at the database, in the schema, in the code path. An admin cannot accidentally weaken the guarantees, and an examiner can verify them in the codebase.
RBAC config vs. data-layer enforcement.
Both can read identical to a buyer in a demo. They look very different to an examiner asking "prove it."
Each one verifiable in the codebase.
AUDITOR role read-only at the data layer
The AUDITOR role is enforced in the database, not via UI permissions. An auditor can view, comment, and submit reviews — but cannot edit a record, accidentally or otherwise. An admin cannot grant write access to an auditor without changing the schema. The guarantee is structural.
// app/lib/rbac.js — server-side enforcement on every API mutation
if (role === 'AUDITOR') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}Immutable PolicyVersion on publish
When a policy publishes, a new PolicyVersion record is created and locked. The Policy document continues to evolve through future drafts; the published version is permanently fixed. Auditors can see exactly what was published, when, and by whom — not what's currently in the editor.
// Publish handler creates the locked version. Submit/approve never do.
await PolicyVersion.create({
policyId, content, publishedAt: new Date(), publishedBy: userId,
});Tenant isolation by query enforcement
Every read query for shared content uses getGrcOrgFilter, which resolves to { organizationId: { $in: [userOrgId, SYSTEM_ORG_ID] } } at the database layer. Tenant boundaries are enforced in the query, not in middleware that can be bypassed. Verified in the codebase pre-push grep.
// Every shared-content read goes through this filter const orgFilter = await getGrcOrgFilter(organizationId); await Framework.find(orgFilter);
Soft-delete + AuditLog on every mutation
Audit-significant records (risks, policies, evidence) cannot be hard-deleted. Soft-delete sets isDeleted: true and writes an AuditLog row with actor, timestamp, and reason. Every mutation across the platform writes to AuditLog — including every Effy AI tool call, logged centrally as EFFY_TOOL_<name>.
// Soft-delete pattern — never .deleteOne() on audit-significant records
await Risk.findOneAndUpdate(
{ _id: id, organizationId },
{ isDeleted: true, deletedAt, deletedBy: userId },
);The difference shows up in three specific moments.
When an admin makes a configuration mistake
RBAC-config platforms: the auditor accidentally gets write access; nobody notices until something gets edited. Architectural: the database refuses the write; the mistake produces a 403 instead of a corrupted record.
When your examiner asks 'prove it'
RBAC-config platforms: you screenshot the admin panel and hope the examiner trusts the screenshot. Architectural: you walk them through the code path. Two minutes vs two days.
When you grow past 100 employees
RBAC-config platforms: the configuration drift problem grows linearly with team size — more admins means more chances to break the guarantees. Architectural: scale doesn't degrade integrity because integrity isn't in the configuration.
Read the code with us.
30-minute architecture walkthrough with our team. We'll show you the actual filter calls, the schema-level enforcement, and where AuditLog rows get written.