Security model
ERGON TIME is multi-tenant: many organizations share one database. Separation is enforced by Postgres row level security, not by application code, and every rule resolves through your organization membership.
Row security is enabled on every table. With no matching policy the answer is no rows and no write — never an open table.
Every helper begins with user = auth.uid(), so a caller can never have a policy evaluated as somebody else.
Authorization lives in Postgres, not in the UI. A leaked API call is refused by the same rules the screens obey.
Every business record — transactions, invoices, customers, employees, deals, meetings, workbooks — carries the organization that owns it. A request is answered only when three things line up: you are signed in, you hold an active membership in that organization, and your role (or an explicit override) allows the operation.
request
└─ signed in? no → refused
└─ active membership in org? no → no rows returned
└─ role or override
allows this action? no → refused, decision traced
└─ yes → row returned or written, action auditedOwner
Full control, cannot be removed while last owner
Admin
Governance, permissions, integrations
Finance
Money-moving work, approvals under threshold
Accountant
Records and reporting, limited edits
Viewer
Read-only
Policies never query the table they protect — that would recurse. They call small, audited helper functions that run with elevated rights but always re-check the caller's own identity first.
is_org_member(org, user)
True only when the user has an active membership row in an active organization. Every read path starts here.
is_org_admin(org, user)
Owner or admin. Governs settings, permissions, audit surfaces and the access trace itself.
can_write_org(org, user)
Owner, admin or finance. Guards money-moving work such as reconciliation matching and approval submission.
has_org_role(org, user, roles[])
Explicit role list check, used where a specific seniority is required rather than a broad tier.
can_approve(org, user, required_role)
Resolves the approval policy threshold to the role that may decide, and refuses self-approval below admin.
has_permission(org, user, permission)
Fine-grained permission resolution: member override, then organization role override, then role defaults.
can_view_spreadsheet / can_edit_spreadsheet
Collaborative workbook access, combining organization admin rights with per-sheet collaborator grants.
The baseline is checked automatically, not assumed. Continuous integration fails the build if any workspace table loses row security, gains a blanket-allow policy, becomes reachable by anonymous callers, or stops resolving through membership. A separate integration suite signs in as real users and proves that a member reads their own organization while a member of another organization — and a user in no organization at all — reads, writes and deletes nothing.
Every workspace table must have row security on, at least one policy, no blanket allow and no anonymous access. A violation stops the release.
Two throwaway organizations, real sign-ins, real API calls, in both directions — including attempts to join another organization or promote a role.
Sensitive operations — approval submission and decisions, reconciliation matching and unmatching — record which helper was consulted, which membership path applied, and whether access was granted or refused, together with the reason. Owners and admins read this trace inside the app; nobody can write to it by hand, and neither the trace nor the audit log can be edited or deleted.
Available in the app under Governance → Access trace, alongside the append-only audit log.
Create an isolated workspace and invite your team with the exact rights they need.
Create your workspace