本文介绍如何基于PolarDB Supabase构建安全、可扩展的多租户(Multi-Tenant)平台。方案通过Schema级数据隔离、JWT身份绑定和请求预处理钩子实现“后端强制,前端无感”的租户隔离模型,为平台管理员、租户和应用使用者定义清晰的安全边界。
方案概述
本方案旨在为构建基于PolarDB Supabase的多租户SaaS平台提供一套完整的架构方案。方案的核心目标是为每个租户提供完全隔离的数据库环境,同时为平台的所有参与者——平台管理员、租户/应用所有者和应用使用者定义清晰、坚固的安全边界。
方案的核心原则是后端强制,前端无感。通过将租户信息安全地编码进JWT,并通过请求预处理钩子对每个API请求的“意图”和“身份”进行强制匹配,实现了一个对前端开发者透明、无需管理大量数据库角色、同时保持最高安全级别的租户隔离模型。
架构设计原则
本方案的设计基于以下四大核心原则:
数据隔离:每个租户的数据都存储在独立的数据库Schema中,从物理层面杜绝数据泄露的可能。
零信任访问控制:每一个API请求都必须通过强制性的自动化安全检查点,验证其身份与意图是否匹配。
动态与自动化:平台的配置(如新租户的API)动态更新,无需服务中断。租户和用户的生命周期管理全自动化。
开发者友好:将所有复杂的安全逻辑封装在后端,让前端开发者可以专注于业务逻辑和用户体验。
基于角色的三层防御体系
安全模型围绕三个核心角色构建,每一层都有明确的职责和坚固的隔离边界。
角色 | 身份定义 | 核心职责 | 隔离与交互机制 |
平台管理员 | SaaS平台的开发者和运维团队 | 负责平台生命周期管理和所有租户的数据模型变更(DDL)。 | 通过受控后端交互。使用 |
租户/应用所有者 | 购买和使用SaaS服务的客户 | 通过平台提供的UI配置其业务数据结构,或以自然语言描述需求。 | 通过SaaS平台UI交互。租户不直接接触数据库。其“意图”由安全的后端工作流翻译成数据库操作,并在专属沙箱内执行。 |
应用使用者 | 租户的客户,SaaS应用的最终用户 | 通过租户提供的App对自己有权访问的数据进行增删改查(DML)。 | 通过租户App前端交互。所有API请求都会被数据库的自动化安全机制强制路由和隔离到正确的租户环境中。 |
核心组件
以下是多租户架构中的核心组件及其作用。
组件 | 说明 |
租户数据沙箱 | 为每个租户分配独立的数据库Schema,确保租户A的数据和表结构对租户B完全不可见。 |
不可篡改的“数字护照”(JWT) | 用户登录后,其身份(包括所属的租户)被加密签名在一个JWT中。这份“数字护照”随每个请求发送,成为后端验证身份的权威依据。 |
租户访问标识(UUID) | 为每个租户生成的唯一标识符,用于在公开或匿名访问场景中指示“当前请求属于哪个租户”。它本身不具备高权限,仅作为路由与隔离依据。 |
自动化安全检查站 | 内建的安全验证程序,在每个API请求执行前被强制调用。确保没有任何请求可以绕过身份和权限的校验。 |
多租数据平面(PostgREST) | 确保新租户创建后,其专属的API接口能被实时、自动地发布,无需重启服务。 |
多租认证平面(Supabase Auth) | 负责在用户注册、登录、会话刷新等全生命周期中携带并校验租户身份,将“数字护照”中的租户ID与当前请求的目标租户进行强制匹配,防止跨租户冒用。 |
多租计算平面(Edge Functions) | 通过“全局函数 + 租户函数”的模式,将跨租户共享逻辑与单租户定制逻辑解耦,函数在运行时只访问对应租户的数据沙箱与Secrets。 |
多租运维平面(Postgres Meta) | 为平台提供租户级元数据与自定义SQL能力,请求通过租户ID选择对应的数据库角色与search_path,使运维操作严格限定在各自租户沙箱内。 |
整体架构与工作流程
架构总揽
租户创建与API自动发布
以下是新租户的创建流程,确保新租户的入驻是即时且无缝的。
平台管理员通过SaaS后端发起创建新租户的指令。
SaaS后端调用Kong网关的租户管理接口
/auth/v1/admin/instances。Kong将请求转发给Supabase Auth(认证服务),Auth以
service_role权限运行。Auth在单个事务中完成以下操作:写入租户记录、创建租户专属的Schema和角色、发送“配置更新”通知。
Kong网关接收到配置更新信号后,重新加载路由和服务配置,自动发布新租户相关的API。
通过平台内部的实时通知机制,避免了因修改配置而需要重启服务的弊端,保证了平台的高可用性。
用户注册并自动归属
新用户注册时自动绑定到对应租户的流程如下:
用户通过前端应用提交注册信息。
前端调用标准注册接口,附带租户访问标识(UUID)作为“归属声明”。
Supabase Auth创建用户记录,携带租户访问标识UUID。
数据库自动解析归属关系:基于租户访问标识UUID写入租户ID。
前端只需在注册时声明“当前租户是哪一个(租户访问标识UUID)”,由后端/数据库自动完成用户与租户的绑定。真正的安全边界由邮箱/手机号验证、业务审批流程等机制来提供,而非依赖UUID本身。
已登录用户的数据访问
已登录用户发起数据请求时,系统执行以下安全校验流程:
用户通过前端应用发起数据请求(例如“刷新我的订单”)。
请求携带JWT(“数字护照”),声明意图访问某租户的数据。
请求被强制路由至自动化安全检查站。
安全检查站执行强制校验:从JWT解码用户身份,对比请求意图(访问哪个租户的资源)与身份归属是否匹配。
匹配通过后,请求在对应租户的Schema沙箱中执行查询并返回数据。
如果用户尝试通过篡改请求来访问其他租户的数据,安全检查站会在发现“意图”与“身份”不匹配时立即拦截请求,使其根本没有机会接触到数据库中的任何数据。
匿名用户访问租户的公开数据
对于未登录的匿名访问场景,安全检查站通过验证请求携带的租户访问标识(UUID)来确认其归属,确保匿名流量也被严格限制在授权的租户范围内。
任何拿到某个租户访问标识UUID的匿名用户,都可以访问该租户的公开数据(受RLS策略控制),其安全级别与普通Supabase匿名访问等效。真正的访问控制依然由数据库的RLS(Row Level Security)策略和业务权限设计来提供。
组件级多租隔离设计
在整体架构和工作流程之上,PolarDB Supabase的各个基础组件也各自承担了多租隔离职责。
Supabase Auth:身份与租户的绑定
统一的租户标识:平台在创建租户时生成唯一的租户ID(Instance ID),并在Auth侧为该租户建立独立的配置空间。
注册与登录时的租户归属:前端在调用注册、登录等接口时,会携带当前租户的租户ID,Auth将其写入用户档案,并在签发JWT时一并编码进去。
会话期内的自动隔离:已登录用户后续的所有请求只需携带JWT,认证层会从中解析租户ID,并在数据库钩子处将其与请求意图匹配。
PostgREST:REST API的多租隔离
请求级租户上下文注入:PostgREST在接收到请求时,会从请求头(如
X-Instance-ID或JWT claims)中提取租户标识,并自动切换到对应租户的Schema执行查询。自动化的租户路由:每个API请求都会被自动路由到正确的租户数据沙箱,开发者无需在业务SQL中手动拼接Schema名称。
与RLS策略的协同:PostgREST生成的SQL查询会自动受到RLS策略的约束,即便某个请求试图绕过租户Schema限制,RLS也会在数据行级别进行二次过滤,形成纵深防御。
Edge Functions:多租逻辑与扩展能力
全局函数与租户函数的分层:平台可以为所有租户共用的逻辑实现“全局函数”,为单个租户的个性化需求实现“租户函数”,二者通过租户ID进行区分。
调用链中的租户上下文传递:调用Edge Functions时,会将当前租户的标识一并传入,函数在内部基于这一上下文访问对应的Schema、Secrets与外部服务。
Postgres Meta:租户级元数据与自定义SQL
元数据视图隔离:通过Meta查看表结构、函数列表时,会显式指定目标租户,Meta在执行查询前切换到该租户对应的数据库角色与search_path,只返回该租户可见的对象。
安全的自定义SQL通道:当SaaS后端需要为某个租户执行一次性SQL时,通过Meta在该租户的沙箱内执行,避免误操作影响其他租户的数据。
平台集成与使用指南
准备工作:启用多租户模式
在开始接入之前,您需要先在PolarDB Supabase应用中启用多租户功能。在Supabase应用详情页的配置管理功能中,配置以下参数:
studio.MULTI_TENANT_ENABLED=true此配置将开启多租户模式,使REST、Auth、Meta等组件能够识别和处理租户隔离逻辑。
面向平台开发者:SaaS后端如何接入
作为SaaS平台的开发者,您的核心任务包括:
管理租户生命周期:创建、查询、更新、删除租户。
初始化租户数据结构:为新租户创建必要的数据库表、视图、函数、RLS策略等。
这两个任务都无需直接操作数据库,而是通过Supabase提供的标准RESTful API来完成。
认证机制
为确保只有授权的后端服务才能调用高权限的管理接口,您需要在每次调用租户管理API时携带以下HTTP头:
Service Role Key(secret.jwt.serviceKey)必须妥善保管在后端服务器,绝不可暴露给前端。任何缺少或凭证不正确的请求都将被Kong网关立即拒绝。
Authorization: Bearer <SERVICE_ROLE_KEY>
apikey: <SERVICE_ROLE_KEY>租户管理API端点
Auth组件提供了完整的租户(Instance)管理能力,路径前缀为/auth/v1/admin/instances。
操作 | HTTP方法 | 端点 | 请求体(示例) |
创建租户 |
|
|
|
列出所有租户 |
|
| (无) |
获取单个租户 |
|
| (无) |
更新租户 |
|
|
|
删除租户 |
|
| (无) |
调用示例(使用Node.js fetch)
id字段为租户的唯一标识(Instance ID,UUID格式),创建成功后,系统会自动在数据库中创建该租户的独立Schema。
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();
}租户数据结构初始化
创建租户后,可以通过Postgres Meta组件提供的SQL执行接口来初始化数据库结构。
API端点:
POST /pg/query请求示例:
说明Meta组件会根据
X-Instance-ID请求头自动切换到对应租户的数据库角色和Schema。所有SQL语句均在该租户的沙箱内执行,建议将初始化SQL脚本统一管理,确保所有租户拥有一致的数据库结构。async function initializeTenantSchema(instanceId: string) { const initSQL = ` -- 在租户 schema 中创建表 CREATE TABLE IF NOT EXISTS products ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, price DECIMAL(10,2), created_at TIMESTAMPTZ DEFAULT NOW() ); -- 启用 RLS ALTER TABLE products ENABLE ROW LEVEL SECURITY; -- 创建 RLS 策略 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(); }
面向租户开发者:如何构建您的应用
作为平台的租户开发者,您的开发流程与构建一个标准的单租户Supabase应用几乎完全一样。
获取您的租户凭证
登录SaaS平台管理后台获取以下信息:
租户访问标识(Instance ID):用于标识您的租户的唯一UUID。
Supabase URL:平台提供的Supabase服务地址。
Anon Key:前端应用使用的匿名访问密钥(
secret.jwt.anonKey)。
用户注册
注册新用户时,只需在标准的signUp调用中,通过请求头传入您的租户访问标识(Instance ID)即可。
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
});
// ... 处理结果
}数据访问(已登录用户)
用户登录后,系统将自动确认其所属租户,将数据操作限定在租户Schema内。使用方式与标准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('*');
// 'products' 数组中只包含属于当前租户的数据
}公开数据访问(匿名用户)
如果您的应用有公开页面(如博客、产品展示),需要在创建Supabase客户端时通过headers注入您的租户访问标识(Instance ID)。
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('*');
}方案优势总结
本方案通过将复杂的租户隔离逻辑完全内聚在PolarDB Supabase后端,实现了以下核心优势:
高级别的安全:Schema级隔离使租户数据在物理层面完全不可见,Kong网关统一入口实现租户上下文透传和鉴权,结合零信任校验形成双重保障。
卓越的开发体验:租户开发者使用标准Supabase SDK,只需在请求头携带
X-Instance-ID即可实现租户隔离,无需感知底层复杂的安全逻辑。全流程自动化:平台开发者通过Auth组件的租户管理接口和Meta组件的SQL执行接口,即可完成租户生命周期管理和数据结构初始化,无需直接操作数据库。
多平面协同隔离:数据平面(PostgREST)、认证平面(Auth)、计算平面(Edge Functions)、运维平面(Meta)均支持租户级隔离,形成全栈多租户能力。
权责清晰:平台、租户、最终用户的职责边界清晰。
service_role仅用于受控的后端管理,前端应用使用低权限凭证,符合最小权限原则。