Skip to main content

Multi-Tenancy

Appilot is a multi-tenant platform where each organization's data is fully isolated.

How it works​

Every user belongs to one or more organizations. The JWT names the active organization, but the organization and role a request actually acts under are read from the live membership row on every request, not from the token. Demote an administrator or remove a member and the change takes effect on their next call rather than when their token expires; a membership that is gone or no longer active is rejected outright rather than quietly downgraded. Treat the token as an identity assertion and nothing more.

User A ─── Organization 1 ──┐
├─── Shared Appilot Backend
User B ─── Organization 2 ──┘

Organization 1 and Organization 2 share infrastructure but never see each other's data.

What's isolated per organization​

ResourceIsolation level
Knowledge contentFully isolated
Domains & URL patternsFully isolated
User membershipsPer-organization roles
RAG files & documentsFully isolated
AI configurationPer-organization settings
Analytics & activityFully isolated
ConversationsPer-user, per-organization
Knowledge requestsFully isolated

Organization context in APIs​

Every API request is scoped to the organization in the JWT:

Authorization: Bearer <token with org_id=abc>
GET /knowledge/content
→ Returns only knowledge for organization "abc"

There is no way to query another organization's data through the API. The backend enforces row-level data filtering.

Ownership is proved by loading the row​

An endpoint that acts on a record by id proves ownership by loading that record and checking who owns it. Validating an id that arrived in the body is not the same check, and neither is the caller's own admin role, which describes their organization while the handler acts on an id from the URL. A record whose owning organization cannot be resolved is refused rather than allowed through.

This matters if you drive the API directly. Passing another tenant's app, domain or subscription id returns a refusal, including on routes that used to answer. PUT /apps/domains/:id accepted another tenant's domain id and reassigned that domain to the caller's own app, which took over how its views and knowledge resolved; alias_of accepted another tenant's domain and read through to its content; the subscription route returned any organization's plan and billing identity to any authenticated caller; PUT /apps/:id let an app be re-parented into another organization through a body field; and the coverage route accepted a plain member uncapping the organization's spend. All five refuse now.

An assistant turn is scoped the same way. The tenant is the authenticated principal, the widget key or Origin for the widget and the live membership for the extension, never the context.url in the body. The URL still selects which of that tenant's domains the turn runs on, and a domain owned by somebody else is a refusal rather than a fallback to its owner.

Users in multiple organizations​

A user can belong to multiple organizations. To access data from a different organization, they must switch context:

POST /auth/switch-org
Authorization: Bearer <token>
{ "organizationId": 42 }
→ Returns a new JWT with the updated org context

Implications for developers​

When integrating the widget or extension:

  • Widget: the page's domain selects the organization where the domain is registered; a widget key selects it where no domain resolves. You never configure an orgId.
  • Extension: the hostname selects the app, and the signed-in user's live membership selects the organization. A member of another organization is refused on that hostname.
  • API: all calls are scoped from the caller's own membership, so your requests never carry an organization id.

How the widget's tenant is resolved​

The rule, stated once and linked from everywhere else:

  1. Domain first. On a page whose hostname is registered, the organization is the one that registered it. No key is needed.
  2. Key where no domain resolves. On your own machine or server to server, the widget key selects the organization, and the request's Origin is checked against the key's allowed domains.
  3. Secret for identity. Minting a user token needs the server secret paired with the key, from your backend. The public key alone cannot assert who somebody is.

The full decision table is in Do I need an API key?.

Either way one page resolves to exactly one organization, so a page cannot reach two tenants through the widget.