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
| Resource | Isolation level |
|---|---|
| Knowledge content | Fully isolated |
| Domains & URL patterns | Fully isolated |
| User memberships | Per-organization roles |
| RAG files & documents | Fully isolated |
| AI configuration | Per-organization settings |
| Analytics & activity | Fully isolated |
| Conversations | Per-user, per-organization |
| Knowledge requests | Fully 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:
- Domain first. On a page whose hostname is registered, the organization is the one that registered it. No key is needed.
- Key where no domain resolves. On your own machine or server to server,
the widget key selects the organization, and the request's
Originis checked against the key's allowed domains. - 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.