Controls
Compliance frameworks and controls with evidence files and notes, recurring evidence tasks with assignees and an overdue state, versioned policies that members acknowledge, and a status rollup per framework.
controls keeps what an audit asks for in one place. You list the controls of
a framework such as SOC 2 or ISO 27001, attach evidence to each control, plan
the evidence that has to be collected again on a schedule, and publish
policies that every member acknowledges. Each control's status follows from
its evidence and tasks, and posture rolls it up per framework.
better-supabase add controlsimport { createControls } from "better-supabase/blocks/controls";The module requires tenant, access and versions. With
attachments a piece of evidence can be a file,
with notifications a missed task notifies its
assignee, with outbox it records the events, and with audit every change
is audited.
Tables
| Table | Holds |
|---|---|
control_frameworks | A framework per organization by key, with a name and version |
controls | A control in a framework by code, with a title, an owner and whether it applies |
control_evidence | Evidence for a control: an attachment, a note or both, who collected it and until when it is valid |
evidence_tasks | Recurring evidence: a title, an interval in days, the next due date and an assignee |
policies | A policy by key with an owner and its current published version |
policy_acknowledgements | Which member acknowledged which version of a policy, and when |
Members with controls.read read every table.
Frameworks and controls
const controls = createControls({ transport });
const soc2 = await controls
.saveFramework(organizationId, {
key: "soc2",
name: "SOC 2",
version: "2017",
})
.orThrow();
const access = await controls
.saveControl({
frameworkId: soc2.id,
code: "CC6.1",
title: "Logical access",
ownerId,
})
.orThrow();Saving a framework with a key that exists, or a control with a code that
exists in the framework, updates it. A control with applicable: false is
out of scope. Frameworks, controls, evidence tasks and policies need
controls.manage.
A control's status is computed, never stored:
| Status | When |
|---|---|
not_applicable | the control doesn't apply |
failing | an active evidence task is overdue, or all its evidence has lapsed |
passing | it has evidence that is still valid |
not_started | it has no evidence yet |
Evidence and tasks
await controls
.addEvidence(access.id, {
attachmentId,
note: "Quarterly access review",
validUntil: Temporal.PlainDate.from("2026-12-31"),
})
.orThrow();
const task = await controls
.saveEvidenceTask(access.id, {
title: "Review access",
intervalDays: 90,
assigneeId,
})
.orThrow();Evidence is an attachment in the organization, a note, or both; adding it
needs controls.collect. A task is due on dueOn (default today) and is
overdue once that day has passed. Evidence added with evidenceTaskId
answers the task: its due date moves intervalDays past today, and the
evidence is valid until then unless validUntil says otherwise. A task's
assignee may collect for it without controls.collect.
Run notifyOverdue daily with the service role, for example from a
job. It records evidence_task.missed once for each
lapse and notifies the assignee, or the control's owner when the task has
none.
Posture
const [framework] = await controls.posture(organizationId).orThrow();
// { total, passing, failing, notStarted, notApplicable, overdueTasks }posture returns one rollup per framework, or for one framework when you
pass its id.
Policies
const policy = await controls
.savePolicy(organizationId, {
key: "acceptable-use",
title: "Acceptable use",
})
.orThrow();
await controls
.publishPolicy(policy.id, { body: markdown }, "2026 edition")
.orThrow();
await controls.acknowledgePolicy(policy.id).orThrow();publishPolicy stores the content as a frozen version
and makes it current. Members acknowledge the current version with
controls.acknowledge; publishing again asks everyone to acknowledge the new
version. policies(organizationId, { pendingOnly: true }) lists the policies
waiting for the caller, and acknowledgements(policyId) lists every member
with the time they acknowledged the current version. An archived policy can't
be published or acknowledged.
| Method | Returns |
|---|---|
saveFramework(organizationId, input) | the framework |
frameworks(organizationId) | the frameworks |
saveControl(input) | the control |
getControl(controlId) | the control with its evidence and tasks |
controls(organizationId, options) | the controls by framework and code |
addEvidence(controlId, input) | the evidence |
removeEvidence(evidenceId) | the removed evidence |
saveEvidenceTask(controlId, input) | the task |
evidenceTasks(organizationId, opts) | the tasks, the earliest due first |
posture(organizationId, frameworkId) | one rollup per framework |
notifyOverdue() | how many lapsed tasks it reported |
savePolicy(organizationId, input) | the policy |
publishPolicy(policyId, content) | the policy with its new version |
archivePolicy(policyId) | the archived policy |
acknowledgePolicy(policyId) | the policy with the caller's acknowledgement |
policies(organizationId, options) | the policies with acknowledgement counts |
acknowledgements(policyId) | every member and when they acknowledged |
Errors
| Hint | Cause |
|---|---|
CONTROLS_FORBIDDEN | the caller lacks controls.read, .manage, .collect or .acknowledge |
CONTROLS_INVALID | a field is invalid, or the attachment or a member isn't in the organization |
CONTROL_FRAMEWORK_NOT_FOUND | no framework with this id |
CONTROL_NOT_FOUND | no control with this id |
CONTROL_EVIDENCE_NOT_FOUND | no evidence with this id |
EVIDENCE_TASK_NOT_FOUND | the evidence task isn't a task of this control |
POLICY_NOT_FOUND | no policy with this id |
POLICY_INVALID | the content isn't an object, or the owner isn't a member |
POLICY_ARCHIVED | the policy is archived and can't be published |
POLICY_NOT_PUBLISHED | the policy has no current version to acknowledge, or it is archived |
Events
control.saved, control.evidence_added, evidence_task.missed,
policy.published and policy.acknowledged. The
events page lists their payloads.
Last updated on
Contracts
Contracts with counterparties from draft through review and signature to active, expired or terminated, with values in minor units, renewal terms that schedule reminders, and obligations with due dates and owners.
Incidents
Incidents with a severity, a status timeline, affected components and postmortems, and a public status page that anyone can read through one function that returns only public fields.