This document describes how to build a secure and scalable multi-tenant platform on PolarDB Supabase. The solution uses schema-level data isolation, JWT identity binding, and database hooks to create a tenant isolation model enforced on the backend and transparent to frontend developers. This approach establishes clear security boundaries for platform administrators, tenant/application owners, and application users.
Overview
This solution provides a complete architectural framework for building a multi-tenant SaaS platform on PolarDB Supabase. Its primary goal is to provide each tenant with a fully isolated database environment while establishing clear, robust security boundaries for its key roles: platform administrators, tenant/application owners, and application users.
The core principle is backend enforcement, frontend transparency. By securely encoding tenant information into a JWT and using a database hook to match the intent and identity of each API request, this model provides a high level of security. It remains transparent to frontend developers and eliminates the need to manage numerous database roles.
Architectural principles
This solution is based on four core principles:
Data isolation: Each tenant's data is stored in a separate database schema, preventing data leakage at the logical level.
Zero-trust access control: Every API request must pass through a mandatory, automated security checkpoint that verifies its identity and intent.
Dynamic automation: Platform configurations, such as APIs for new tenants, are updated dynamically without service interruptions. Tenant and user lifecycle management is fully automated.
Developer-friendly: All complex security logic is encapsulated in the backend, allowing frontend developers to focus on business logic and user experience.
Role-based three-layer defense system
The security model is built around three core roles, each with clear responsibilities and robust isolation boundaries.
Role | Definition | Core responsibilities | Isolation and interaction mechanism |
platform administrator | The development and operations team of the SaaS platform. | Manages the platform lifecycle and data model changes (DDL) for all tenants. | Interacts through a controlled backend. Uses the |
tenant/application owner | The customer who purchases and uses the SaaS service. | Configures their business data structures through the platform-provided UI, or describes requirements in natural language. | Interacts through the SaaS platform UI. Tenants do not interact directly with the database. Their intent is translated into database operations by secure backend workflows and executed within a dedicated sandbox. |
application user | The tenant's customer and the end user of the SaaS application. | Uses the tenant's app to perform data manipulation language (DML) operations (create, read, update, and delete) on data they are authorized to access. | Interacts through the tenant application's frontend. All API requests are automatically routed and isolated to the correct tenant environment by the database's automated security mechanisms. |
Core components
The following are the core components of the multi-tenant architecture and their functions.
Component | Description |
Tenant data sandbox | Assigns a separate database schema to each tenant, ensuring that Tenant A's data and table structure are completely invisible to Tenant B. |
Immutable "digital passport" (JWT) | After a user logs in, their identity, including their tenant affiliation, is cryptographically signed into a JWT. This "digital passport" is sent with every request and serves as the authoritative source for backend identity verification. |
tenant access identifier (UUID) | A unique identifier generated for each tenant. It is used in public or anonymous access scenarios to indicate which tenant the current request belongs to. It does not grant high privileges and serves only as a basis for routing and isolation. |
Automated security checkpoint | A built-in security procedure that automatically runs before every API request. |
multi-tenant data plane (PostgREST) | Ensures that when a new tenant is created, its dedicated API endpoints are published automatically and in real time without requiring a service restart. |
multi-tenant authentication plane (Supabase Auth) | Manages and verifies tenant identity throughout the user lifecycle, including registration, login, and session refreshes. It enforces a match between the tenant ID in the "digital passport" and the target tenant of the current request to prevent cross-tenant impersonation. |
multi-tenant compute plane (Edge Functions) | Decouples shared cross-tenant logic from custom single-tenant logic by using a model of "global functions + tenant functions". At runtime, functions can only access the data sandbox and secrets of the corresponding tenant. |
multi-tenant management plane (Postgres Meta) | Provides the platform with tenant-level metadata and custom SQL capabilities. Requests select the corresponding database role and |
Architecture and workflow
Architecture overview
Tenant creation and automatic API publishing
This workflow describes how to create a new tenant for instant and seamless onboarding.
The platform administrator initiates a command to create a new tenant through the SaaS backend.
The SaaS backend calls the Kong Gateway's tenant management endpoint,
/auth/v1/admin/instances.Kong Gateway forwards the request to Supabase Auth, which runs with
service_rolepermissions.Supabase Auth completes the following operations within a single transaction: writes the tenant record, creates the tenant's dedicated schema and role, and sends a "configuration update" notification.
After receiving the configuration update signal, Kong Gateway reloads its routes and service configurations to automatically publish the APIs for the new tenant.
Using the platform's internal real-time notification mechanism, this approach avoids service restarts during configuration changes, ensuring high platform availability.
User registration and automatic tenant assignment
The following workflow describes how a new user is automatically assigned to the correct tenant upon registration:
The user submits registration information through the frontend application.
The frontend calls the standard registration endpoint, including the tenant access identifier (UUID) as a declaration of tenancy.
Supabase Auth creates a user record that includes the tenant access identifier (UUID).
The database automatically resolves ownership by writing the tenant ID based on the tenant access UUID.
During registration, the frontend only needs to declare the current tenant by providing the tenant access identifier (UUID). The backend and database automatically handle the user-tenant binding. The actual security boundary is enforced by mechanisms such as email or phone verification and business approval workflows, rather than by the UUID itself.
Data access for authenticated users
When an authenticated user requests data, the system performs the following security checks:
The user initiates a data request through the frontend application (for example, by clicking "Refresh My Orders").
The request includes a JWT (the "digital passport"), which declares the intent to access a specific tenant's data.
The request is automatically routed to the automated security checkpoint.
The security checkpoint performs a mandatory check: it decodes the user's identity from the JWT and compares it with the request's intent (which tenant's resource is being accessed).
If the identity and intent match, the request is executed within the corresponding tenant's schema sandbox, and the data is returned.
If a user attempts to access another tenant's data by tampering with the request, the security checkpoint immediately intercepts it upon detecting a mismatch between intent and identity. This ensures the request never reaches the database.
Anonymous access to public tenant data
For unauthenticated, anonymous access, the security checkpoint verifies the request's tenancy by checking the tenant access identifier (UUID). This ensures that anonymous traffic is also strictly confined within the authorized tenant's scope.
Any anonymous user with a tenant's access UUID can access that tenant's public data (subject to RLS policies), providing a security level equivalent to standard Supabase anonymous access. Actual access control is still provided by the database's RLS (Row Level Security) policies and business permission design.
Component-level multi-tenant isolation design
In addition to the overall architecture and workflows, each underlying component of PolarDB Supabase plays a role in enforcing multi-tenant isolation.
Supabase Auth: Bind identities to tenants
Unified tenant identifier: The platform generates a unique tenant ID (Instance ID) when a tenant is created and establishes an independent configuration space for that tenant within Supabase Auth.
Tenant assignment during registration and login: During registration or login, the frontend includes the current tenant's ID. Supabase Auth writes this ID to the user's profile and encodes it into the JWT upon issuance.
Automatic isolation during sessions: For all subsequent requests, an authenticated user only needs to include the JWT. The authentication layer parses the tenant ID from the token, and the database hook matches it against the request's intent.
PostgREST: Isolate REST APIs for multi-tenancy
Request-level tenant context injection: When PostgREST receives a request, it extracts the tenant identifier from the request headers (such as
X-Instance-IDor JWT claims) and automatically switches to the corresponding tenant's schema to execute the query.Automated tenant routing: Each API request is automatically routed to the correct tenant's data sandbox, so developers do not need to manually specify schema names in their business SQL.
Collaboration with RLS policies: SQL queries generated by PostgREST are automatically constrained by row-level security (RLS) policies. This provides defense-in-depth, as RLS will filter data at the row level even if a request attempts to bypass tenant schema restrictions.
Edge Functions: Multi-tenant logic and extensibility
Layering of global and tenant functions: The platform can implement "global functions" for logic shared by all tenants and "tenant functions" for custom requirements of individual tenants. The two are distinguished by the tenant ID.
Tenant context propagation in the call chain: When calling an Edge Function, the current tenant's identifier is passed along. The function uses this context internally to access the corresponding schema, secrets, and external services.
Postgres Meta: Tenant-level metadata and custom SQL
Metadata view isolation: When viewing table structures or function lists through Postgres Meta, you must explicitly specify the target tenant. Postgres Meta switches to that tenant's corresponding database role and
search_pathbefore executing the query, returning only objects visible to that tenant.Secure channel for custom SQL: When the SaaS backend needs to execute a one-time SQL script for a specific tenant, it does so through Postgres Meta within that tenant's sandbox. This prevents accidental operations from affecting other tenants' data.
Platform integration and usage
Prerequisites: Enable multi-tenant mode
To begin, enable the multi-tenant feature in your PolarDB Supabase application. On the Supabase application details page, go to the Configure section and set the following parameter:
studio.MULTI_TENANT_ENABLED=trueThis setting enables multi-tenant mode, allowing components like PostgREST, Auth, and Meta to recognize and handle tenant isolation logic.
For platform developers: SaaS backend integration
As a SaaS platform developer, your core tasks include:
Manage the tenant lifecycle: Create, query, update, and delete tenants.
Initialize the tenant data structure: Create the necessary database tables, views, functions, and RLS policies for new tenants.
You accomplish both of these tasks using the standard RESTful APIs provided by Supabase without direct database manipulation.
Authentication
To ensure that only authorized backend services can call high-privilege management interfaces, you must include the following HTTP headers in every call to the tenant management API:
The Service Role Key (secret.jwt.serviceKey) must be securely stored on the backend server and must never be exposed to the frontend. Any request that lacks this credential or provides an invalid one will be immediately rejected by the Kong Gateway.
Authorization: Bearer <SERVICE_ROLE_KEY>
apikey: <SERVICE_ROLE_KEY>Tenant management API endpoints
The Auth component provides a full set of capabilities for managing tenants (Instances) under the path prefix /auth/v1/admin/instances.
Actions | HTTP method | Endpoint | Request body (example) |
Create tenant |
|
|
|
List all tenants |
|
| (None) |
Get single tenant |
|
| (None) |
Update tenant |
|
|
|
Delete tenant |
|
| (None) |
Example (using Node.js fetch)
The id field is the tenant's unique identifier (Instance ID, in UUID format). After a tenant is created, the system automatically creates a separate schema for it in the database.
async function createNewTenant(id: string, name: string) {
const response = await fetch('https://<your-supabase-url>/auth/v1/admin/instances', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SERVICE_ROLE_KEY}`,
'apikey': process.env.SERVICE_ROLE_KEY
},
body: JSON.stringify({ id, name })
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to create tenant: ${error.message}`);
}
return await response.json();
}Tenant data structure initialization
After creating a tenant, you can initialize its database structure by using the SQL execution endpoint provided by the Postgres Meta component.
API endpoint:
POST /pg/queryRequest example:
NoteThe Postgres Meta component automatically switches to the appropriate tenant's database role and schema based on the
X-Instance-IDrequest header. All SQL statements are executed within that tenant's sandbox. Manage initialization SQL scripts centrally to ensure a consistent database structure across all tenants.async function initializeTenantSchema(instanceId: string) { const initSQL = ` -- Create a table in the tenant schema CREATE TABLE IF NOT EXISTS products ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, price DECIMAL(10,2), created_at TIMESTAMPTZ DEFAULT NOW() ); -- Enable RLS ALTER TABLE products ENABLE ROW LEVEL SECURITY; -- Create an RLS policy CREATE POLICY "Users can view all products" ON products FOR SELECT USING (true); `; const response = await fetch('https://<your-supabase-url>/pg/query', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.SERVICE_ROLE_KEY}`, 'apikey': process.env.SERVICE_ROLE_KEY, 'X-Instance-ID': instanceId }, body: JSON.stringify({ query: initSQL }) }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed to initialize tenant schema: ${error.message}`); } return await response.json(); }
For tenant developers: Application building
As a tenant developer, your development process is nearly identical to building a standard single-tenant Supabase application.
Get your tenant credentials
Log in to the SaaS platform's management console to obtain the following information:
Instance ID: The unique UUID that identifies your tenant.
Supabase URL: The Supabase service address provided by the platform.
Anon Key: The anonymous access key used by your frontend application (
secret.jwt.anonKey).
User registration
When registering a new user, simply pass your Instance ID in the request headers of a standard signUp call.
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: {
headers: {
'X-Instance-ID': INSTANCE_ID
}
}
});
async function handleSignUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password
});
// ... handle the result
}Data access (for authenticated users)
After a user logs in, the system automatically confirms the user's tenant affiliation and confines data operations to that tenant's schema. The usage is identical to the standard Supabase SDK:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: {
headers: {
'X-Instance-ID': INSTANCE_ID
}
}
});
async function getProducts() {
const { data: products, error } = await supabase
.from('products')
.select('*');
// The 'products' array only contains data belonging to the current tenant.
}Public data access (for anonymous users)
If your application has public-facing pages (such as a blog or product showcase), you must inject your Instance ID through the headers when creating the Supabase client.
import { createClient } from '@supabase/supabase-js';
const INSTANCE_ID = 'your-instance-uuid';
const publicSupabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: {
headers: {
'X-Instance-ID': INSTANCE_ID
}
}
});
async function getPublicPosts() {
const { data, error } = await publicSupabaseClient
.from('public_posts')
.select('*');
}Solution benefits
This solution encapsulates complex tenant isolation logic within the PolarDB Supabase backend, providing these core benefits:
High-level security: Schema-level isolation makes tenant data logically invisible. The Kong Gateway provides a unified entry point for tenant context propagation and authentication, which combines with zero-trust verification to form a dual layer of protection.
Excellent developer experience: Tenant developers use the standard Supabase SDK and only need to include the
X-Instance-IDin request headers for tenant isolation, without needing to understand the underlying complex security logic.End-to-end automation: Platform developers can manage the entire tenant lifecycle and initialize data structures by using the tenant management and SQL execution endpoints from the Auth and Postgres Meta components, without direct database manipulation.
Multi-plane coordinated isolation: The data plane (PostgREST), authentication plane (Supabase Auth), compute plane (Edge Functions), and management plane (Postgres Meta) all support tenant-level isolation, forming a full-stack multi-tenant capability.
Clear separation of responsibilities: The responsibilities of the platform, tenants, and end users are clearly defined. The
service_roleis reserved for controlled backend management, while frontend applications use low-privilege credentials, adhering to the principle of least privilege.