How to Implement Multi-Tenancy with Logto Organizations: A Complete Guide
Implement multi-tenancy in Logto by treating each tenant as an organization, using the tenant-organization helpers to create deterministic IDs, and requesting organization-specific access tokens with the urn:logto:scope:organizations scope.
Logto provides a native multi-tenancy architecture where each SaaS tenant maps to a special organization record. This approach leverages Logto's organization-based RBAC (Role-Based Access Control) to isolate tenant data while maintaining a unified identity infrastructure. In this guide, you'll learn how to implement multi-tenancy with Logto organizations using the exact helpers and database schemas found in the logto-io/logto repository.
Understanding Logto's Multi-Tenancy Architecture
Logto treats a tenant as a specialized type of organization. When provisioning a new SaaS tenant, you create an admin-tenant organization that stores the tenant's roles, scopes, and user memberships.
The implementation flow follows these steps:
- Create an organization for the tenant using deterministic ID generation
- Define tenant-specific scopes converted to OrganizationScope records
- Create tenant-specific roles as OrganizationRole records
- Map roles to scopes via the
tenantRoleScopesconfiguration - Add users to the organization through membership relations
- Grant organization-specific access tokens containing the organization ID and roles
This architecture is backed by SQL tables including organizations, organization_scopes, organization_roles, and organization_user_relations defined in packages/schemas/tables/.
Core Types and Reserved Resources
Multi-tenancy in Logto relies on specific OIDC resource and scope identifiers defined in packages/toolkit/core-kit/src/openid.ts.
The reserved resource for organization templates is:
urn:logto:resource:organizations
Two critical scopes control organization token grants:
urn:logto:scope:organizations– Returns the list of organization IDs a user belongs tourn:logto:scope:organization_roles– Returns the roles the user has within each organization
These scopes are surfaced in the UserScope enum (lines 33-40) in the same file, enabling your application to request organization context during authentication.
Creating Tenant Organizations with Helper Functions
The packages/schemas/src/types/tenant-organization.ts file provides utility functions that standardize tenant-to-organization mapping. These helpers ensure consistent ID formatting and URI construction across your implementation.
Key helper functions:
getTenantOrganizationId(tenantId)– Returns deterministic organization ID (t-<tenantId>)getTenantOrganizationCreateData(tenantId)– Builds theCreateOrganizationpayload with proper tenant scopinggetTenantScope(scope)– Converts aTenantScopeto anOrganizationScoperecordgetTenantRole(role)– Converts aTenantRoleto anOrganizationRolerecordtenantRoleScopes– Maps roles to their permitted scopes
For example, calling getTenantOrganizationCreateData('abc123') returns:
{
tenantId: 'admin',
id: 't-abc123',
name: 'Tenant abc123'
}
This deterministic approach ensures that tenant organizations are always created with predictable identifiers following the t-<tenantId> convention.
Defining Tenant Scopes and Roles
Each tenant requires specific permissions (scopes) and access levels (roles) implemented through Logto's organization-level RBAC.
Tenant scopes represent granular permissions like read:data or invite:member. The getTenantScope helper converts these into OrganizationScope records with hyphenated IDs:
{
tenantId: 'admin',
id: 'read-data',
name: 'read:data',
description: 'Read the tenant data.'
}
Tenant roles such as admin or collaborator become OrganizationRole records via getTenantRole. These include the RoleType.User designation, distinguishing them from machine-to-machine roles.
The tenantRoleScopes mapping links each role to its authorized scopes. For instance, the admin role typically maps to [read:data, write:data, invite:member], while restricted roles receive narrower scope assignments.
Database Schema for Multi-Tenancy
Logto's multi-tenancy implementation persists data across several linked tables in packages/schemas/tables/:
organizations– Stores organization metadata (id, name, description, branding) perorganizations.sqlorganization_user_relations– Links users to organizations (membership table)organization_scopes– Holds template scopes likeread:dataandwrite:dataorganization_roles– Stores role definitions (admin, collaborator)organization_role_scope_relations– Junction table connecting roles to their granted scopesorganization_role_user_relations– Assigns specific roles to users within specific organizations
This schema implements a complete RBAC model where organization membership, role assignments, and permission scopes are strictly separated but relationally linked.
Requesting Organization Access Tokens
To access resources within a specific tenant, clients must request an organization-specific access token. Include the organization scope and target organization_id in your token request:
POST /oidc/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://app.example.com/callback&
scope=openid%20profile%20email%20urn:logto:scope:organizations&
organization_id=urn:logto:organization:123
The organization_id parameter must be a URN constructed using buildOrganizationUrn (defined in openid.ts lines 251-265). This helper ensures consistent URN formatting: urn:logto:organization:<id>.
A successful response includes an id_token containing:
organization_idclaim – The URN of the accessed organizationorganization_rolesclaim – Array of role IDs the user holds within that organization
These claims enable your backend services to enforce tenant isolation and role-based permissions.
Complete Implementation Example
The following TypeScript example demonstrates the complete multi-tenancy setup using Logto's official helpers:
import {
getTenantOrganizationCreateData,
getTenantScope,
getTenantRole,
tenantRoleScopes,
TenantScope,
TenantRole,
} from '@logto/schemas';
// 1. Create the tenant organization
const tenantId = 'my-customer';
const orgCreate = getTenantOrganizationCreateData(tenantId);
// Result: { tenantId: 'admin', id: 't-my-customer', name: 'Tenant my-customer' }
await db.insert('organizations', orgCreate);
// 2. Create scopes for this organization
for (const scope of Object.values(TenantScope)) {
const orgScope = getTenantScope(scope);
await db.insert('organization_scopes', orgScope);
}
// 3. Create roles and bind scopes
for (const role of Object.values(TenantRole)) {
const orgRole = getTenantRole(role);
await db.insert('organization_roles', orgRole);
// Link role to authorized scopes
for (const scope of tenantRoleScopes[role]) {
const orgScope = getTenantScope(scope);
await db.insert('organization_role_scope_relations', {
organizationId: orgCreate.id,
organizationRoleId: orgRole.id,
organizationScopeId: orgScope.id,
});
}
}
// 4. Add user to organization with role assignment
await db.insert('organization_user_relations', {
tenantId: 'admin',
organizationId: orgCreate.id,
userId: 'user-123',
});
await db.insert('organization_role_user_relations', {
tenantId: 'admin',
organizationId: orgCreate.id,
organizationRoleId: 'admin',
userId: 'user-123',
});
This implementation uses the same factory functions found in packages/schemas/src/types/tenant-organization.ts, guaranteeing that IDs, names, and URNs follow Logto's canonical format.
Summary
Implementing multi-tenancy with Logto organizations requires mapping each tenant to an organization record and configuring the associated RBAC structure:
- Treat tenants as organizations using
getTenantOrganizationCreateDatato generate deterministic IDs in the formatt-<tenantId> - Define permissions using
getTenantScopeandgetTenantRoleto convert tenant-level scopes and roles into organization-level records - Configure RBAC via the
tenantRoleScopesmapping and theorganization_role_scope_relationstable - Assign membership through
organization_user_relationsand role assignments viaorganization_role_user_relations - Request tokens with
urn:logto:scope:organizationsand theorganization_idparameter to receive claims containingorganization_idandorganization_roles
Frequently Asked Questions
How does Logto distinguish between regular organizations and tenant organizations?
Logto uses a deterministic ID convention where tenant organizations are prefixed with t-. The getTenantOrganizationId function in packages/schemas/src/types/tenant-organization.ts generates IDs like t-abc123, while regular organizations use standard UUIDs. This naming convention allows Logto to identify tenant-scoped resources while using the same underlying organizations table schema.
What is the difference between urn:logto:scope:organizations and urn:logto:scope:organization_roles?
The urn:logto:scope:organizations scope returns a list of organization IDs that the authenticated user belongs to, while urn:logto:scope:organization_roles returns the specific roles the user holds within those organizations. Both are defined in packages/toolkit/core-kit/src/openid.ts and are used together to provide complete organization context in ID tokens.
Can I use custom scopes and roles for tenant organizations?
Yes, while Logto provides standard TenantScope and TenantRole enums, you can define custom scopes by calling getTenantScope with your own scope identifiers, or create organization-scoped resources directly in the organization_scopes table. Ensure you also update the tenantRoleScopes mapping to associate your custom roles with the appropriate custom scopes.
How do I validate organization membership in my backend API?
Extract the organization_id claim from the access token to identify the tenant context, then verify the organization_roles claim to enforce RBAC. The token contains the URN format (urn:logto:organization:<id>) generated by buildOrganizationUrn, allowing your API to match the request against the stored organization records in your database.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →