Overview
Ottili ONE is built company-scoped. Every company works in its own clearly bounded tenant context, and sensitive actions never run without the right permission or a human approval. This article explains how tenant isolation works technically — from company-scoped storage and fail-closed enforcement to platform-wide enforcement in the Unified API, the realtime bus, the permission engine and search.
Tenant isolation is not an add-on and not a bolt-on filter. It is part of the shared control plane (Platform Core) and is enforced on every request. For a broader, cross-cutting view, see the [Security overview](/docs/security-overview).
Company-scoped tenant separation
In Ottili ONE every business record belongs to exactly one verified company_id. Cross-company access is never inferred from a default, a first membership, or request input.
- Every business entity — records, events, files, flow runs, webhooks, API keys and billing events — carries an explicit
company_id. - Tenant membership is derived from the verified token, not from a caller-supplied value.
- A company can never reach another company's data unless an explicit, authorized grant allows it.
Fail-closed enforcement with assert_same_company()
The shared assert_same_company() primitive is fail-closed: it rejects any request with no or mismatched company context. A caller-supplied company_id is never trusted.
- Every tenant-owned operation that receives a
company_idfrom the request must resolve its effective tenant throughassert_same_company()instead of trusting the caller. - A
Nonetarget resolves to the actor's own company — never to a global default. - A mismatch (target company differs from the verified company) is denied with a clearly defined error.
Database row-level security with PostgreSQL (RLS)
The database filters every platform row by the verified company_id using PostgreSQL Row-Level Security (RLS). The runtime default state is deny (deny-by-default).
- A session state without an explicit positive
company_id(no superadmin bypass in the request path) is treated as an error. - The tenant session binds the verified
company_idper transaction, so every query stays company-scoped. - A context without a company raises a
MissingCompanyContextError— there is no hidden global default.
A single tenant boundary: PlatformContext
PlatformContext is the one tenant boundary of the platform. There is no second, competing boundary.
validate(require_company=True)returns an HTTP 403 when no verified company is present.assert_company()rejects any mismatch the moment a different company than the verified one would be reached.- The
company_idfromPlatformContextis the single source of tenant scope for every downstream service.
Platform-wide enforcement via the Unified API middleware
Tenant isolation does not stop at the database. The Unified API middleware extracts the verified company_id per request and installs the database session's RLS hook, so every transaction stays company-scoped.
- The token is verified on every authenticated call.
- The extracted
company_idflows into the session context before business logic runs. - Services that talk through the Unified API inherit the same company-scoped context without re-deriving it.
Realtime: one bus, scoped channels
Ottili ONE runs a single realtime bus. Channels are company-scoped and authorized deny-by-default.
- Channels follow the pattern
co.{company_id}; a non-positive id is rejected. authorize_channel_accessdenies access when the caller has no context or does not own the channel.- Only a typed
payload_refreference is stored on channels — secrets, tokens and personal data never appear there.
One permission engine with deny-by-default
There is exactly one permission engine. An unknown role, unknown permission, missing company, or cross-tenant access is denied.
- There is no hidden admin shortcut and no wildcard (
*) grant. - Permissions are company-scoped: a user sees and may act only on what belongs to their own company and role.
- This includes AI-assisted actions: approval-required steps run only with a human approval through the [Approval queue](/docs/approval-queue).
Company-scoped search
Global search is company-scoped: the search layer indexes and returns only documents that belong to the requesting company, and records the company scope in observability.
- Search models carry an explicit
company_id. - Company-scoped search depends on the indexing job honoring
company_id. This is a known, honest caveat: company-scoped search is qualified (indexing-dependent) and is not published as a universal guarantee without reservation.
Status terms and availability
The tenant-isolation mechanisms described here — company-scoped records, assert_same_company(), PostgreSQL RLS, the single PlatformContext, the Unified API middleware, scoped realtime channels, and the one permission engine — are live and evidenced in the source code. Company-scoped search is qualified (see above).
Ottili ONE clearly distinguishes Live, Beta, Private Beta, In Development, Planned and Concept. What these terms mean is explained in [Understand feature status labels](/docs/understand-feature-status-labels).
Related articles
- [Security overview](/docs/security-overview) — the cross-cutting view of the security architecture.
- [Account & data security](/docs/account-and-data-security) — authentication, sessions, isolation, audit, vulnerability reporting.
- [Roles and permissions](/docs/roles-and-permissions) — who can do what.
- [Shared permissions and entitlements](/docs/shared-permissions-and-entitlements) — the platform-wide permission model.
- [Approval queue](/docs/approval-queue) — human approval for sensitive actions.
- [Company, team & permissions](/docs/company-and-team) — how roles and access work.
- [Navigate the Ottili Console](/docs/navigate-ottili-console) — where to find settings and administration.
- [Understand feature status labels](/docs/understand-feature-status-labels) — what the status terms mean.
Was this article helpful?
