Why every resource belongs to exactly one org
Every camera, gateway, site, user, event, alert, clip, and audit entry in NovaVMS belongs to exactly one organisation. Every tenant table carries an org_id column with a FOREIGN KEY to organizations(id). The primary key remains a single id (UUID) for ergonomics, but every query MUST filter by (org_id, entity_id) rather than entity_id alone — composite filtering at the query layer is the isolation boundary (per D11).
Why this works this way
A VMS holds footage of people, often inside private buildings, sometimes from before an incident the operator did not expect to need. The risk of one customer accidentally seeing another customer’s video is unacceptable. You can mitigate that risk with careful code, or you can make the risk not exist.
We picked the second option.
Decision D11 records the rule: every tenant table has an org_id column, every query filters by org_id first. All TENANT data lives in per-org tables. The platform-admin tier (D80) maintains a separate set of cross-org tables — platform_users, platform_sessions, impersonation_tokens, platform_audit_log — that org users never see and that contain no tenant content. Tenant audit logs are per-org.
The practical consequence is that every repository method in the cloud backend takes orgID as its first argument, and every SQL statement has WHERE org_id = $1 before any other predicate. The code review rule is simple: if a query touches a tenant table without org_id in the WHERE clause, it does not ship. This is enforced by convention, not by the database — Postgres row-level security was considered and rejected because it hides the isolation rule behind a policy layer that is prone to misconfiguration. Explicit is better than implicit here.
Invites carry the inviter’s org, and that org is the one the invited user lands in. A user can exist in more than one org over time (by accepting multiple invites, or by being re-invited after removal), but each user row is local to one org — a human who works across orgs has multiple rows, one per org. Roles are per-org for the same reason: the same human can be an Admin in one org and a Viewer in another. The user record and the role record share the org_id.
Cross-org operations exist for exactly one purpose: Novalien staff (platform admins) supporting a specific customer, and only through a scoped impersonation token that the audit log records (per D86 and D80 — the scoped impersonation pattern borrowed from Eagle Eye Networks’s reseller flow). There is no standing cross-org read. Platform admins who need to investigate an issue mint a short-lived token that pins them to one org; when the token expires they are back to seeing nothing.
This is more restrictive than Verkada’s model, which allows an Org Admin role to view resources across sub-sites within the same parent org. It is roughly equivalent to EEN’s reseller pattern, minus the part where EEN couples cross-org access to a persistent reseller relationship.
What this means for you
- Inviting a user into your org grants them nothing in any other org they might belong to. The two are independent.
- Changing a user’s role in your org does not affect their role in any other org.
- API keys, webhook secrets, and export URLs are all org-scoped. A URL generated in one org does not resolve in another, even if an operator with access to both holds the link.
- Searches, filters, and reports never return results from outside your org, even if an AI tag or camera ID happens to match.
- If you are a Novalien staff member helping a customer, you see nothing until you mint an impersonation token for that specific org — and everything you do while the token is active is in that org’s audit log with
actor_type = 'platform_admin_impersonating'.
Where to go next
- Invite a user — how the invite flow anchors users to your org
- Why NovaVMS uses five roles — how per-org roles work alongside org isolation
- Role matrix by endpoint — the exact HTTP permissions within an org