Skip to main content
Administration and Security

Tenant isolation

How Ottili ONE keeps your company separate from others: company-scoped records, fail-closed enforcement, database row-level security, a single tenant boundary, and end-to-end enforcement across platform, realtime, permissions and search.

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_id from the request must resolve its effective tenant through assert_same_company() instead of trusting the caller.
  • A None target 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_id per 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_id from PlatformContext is 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_id flows 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_access denies access when the caller has no context or does not own the channel.
  • Only a typed payload_ref reference 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?