This page is maintained by Viewli to answer common questions about how tenant data is isolated in the Viewli backend. It describes the app's current controls; it is not an independent certification.
Security & tenant isolation
Viewli is a multi-tenant application. Every customer workspace is a tenant, and all business data — screens, playlists, schedules, rules, analytics, audit trails — is scoped to a tenant_id. Isolation is enforced in the database with Postgres Row-Level Security (RLS), not in application code, so a compromised or buggy client cannot bypass it.
Membership model
tenants— one row per workspace.tenant_members(tenant_id, user_id)— which users belong to which tenants.user_roles(user_id, tenant_id, role)— roles are stored separately from profiles to prevent privilege escalation. Roles are checked throughSECURITY DEFINERhelpers that read this table on the server side.
Core RLS helpers
All tenant-scoped policies delegate to a small set of stable SECURITY DEFINER functions. Policies never inline joins to user_roles or tenant_members, which avoids recursive RLS and keeps predicates cheap.
is_tenant_member(_user_id, _tenant_id)— is this user a member of this tenant?has_role(_user_id, _tenant_id, _role)— does this user hold a specific role in this tenant?has_any_role(_user_id, _tenant_id, _roles[])— any of a set of roles.is_system_admin(_user_id)— global operator role, scoped withtenant_id IS NULL.
Required policy shape for tenant tables
Every table that stores tenant-owned data must follow the same four rules. New migrations that violate any of them are considered broken.
tenant_id uuid NOT NULL— no nullable tenant column, ever. A nullabletenant_idcombined with anIS NULLbranch in a policy is a cross-tenant bypass.ENABLE ROW LEVEL SECURITYon the table, plus explicitGRANTs toauthenticated(andservice_rolefor admin/edge paths). No blanketanongrants on tenant tables.- A single
FOR ALLpolicy scoped to authenticated users with bothUSINGandWITH CHECKset tois_tenant_member(auth.uid(), tenant_id). Same predicate on both sides prevents a user from moving a row into another tenant onUPDATE. - For admin-only mutations (billing, member removal, role changes) add a second policy gated on
has_role(..., 'tenant_admin')oris_system_admin(auth.uid()).
Audit events
audit_events is append-only from the client's perspective. It intentionally has no client INSERT policy — writes only happen through a server-side RPC so the actor, tenant, and payload cannot be forged.
- Write:
log_audit_event(_tenant_id, _entity_type, _action, _entity_id?, _entity_label?, _metadata?)— aSECURITY DEFINERfunction that requiresauth.uid()andis_tenant_member(auth.uid(), _tenant_id). It stampsactor_idandactor_emailfrom the JWT, so callers cannot spoof them. - Read: a single
FOR SELECT TO authenticatedpolicy usingis_tenant_member(auth.uid(), tenant_id). Members see their own tenant's history; nobody sees anyone else's. - Mutation: no
UPDATEorDELETEpolicies are exposed to clients. Retention is handled by scheduled server jobs only.
Tenant lifecycle RPCs
Membership and role changes go through SECURITY DEFINER RPCs so authorization is checked once, in the database:
create_tenant(_name, _slug)— creates the tenant, adds the caller astenant_admin, sets it as the caller's current tenant.accept_invitation(_token)— validates token, expiry, and email match, then joins the tenant and applies the invited role.set_member_role(_tenant_id, _user_id, _role)andremove_tenant_member(_tenant_id, _user_id)— restricted totenant_adminorsystem_admin.
Regression testing
The invariants above are covered by an in-database regression suite (run_tenant_isolation_regression) that impersonates two authenticated users and an anonymous session, then asserts:
screens.tenant_idisNOT NULLand the ALL policy has noIS NULLbypass.audit_eventshas no client INSERT policy.- Authenticated users only see, insert, update, and delete rows in tenants they belong to.
log_audit_eventsucceeds for the caller's own tenant, rejects other tenants, and rejects anonymous callers.- Anonymous sessions see zero rows in
screensandaudit_events.
Any future migration that reopens a null-tenant bypass, drops the NOT NULL on tenant_id, or re-adds a client INSERT path on audit_events will fail this suite before shipping.
Reporting a vulnerability
If you believe you've found a security issue in Viewli, email security@viewli.co.uk with a description and reproduction steps. Please give us a reasonable window to investigate and remediate before public disclosure.