这段时间帮几个项目搭后端,前后对比了一圈方案,最后都落在了同一个组合上:Supabase做应用后端,PostgreSQL承载核心数据。更让我意外的是,这个组合和AI编程的适配程度远超预期——AI写业务逻辑的时候,几乎不用我额外调教就能直接接上数据库操作。用AI辅助开发的全栈团队、正在从Firebase迁移的个人开发者、需要给客户做私有化交付的团队,我觉得这篇都能帮上忙。
Supabase本身是开源的Firebase替代品,底层直接跑在PostgreSQL上,对外提供认证、数据库、对象存储、实时订阅、边缘函数这些能力。和Firebase最大的区别是:它的数据层就是原生的PostgreSQL,这意味着完整的SQL能力、行级安全策略、触发器、全文检索、空间数据这些高级特性全都在,只是被外面包了一层好用的API。换句话说,你用Supabase不只是用了一个BaaS,你是在用一个被重新包装过的PostgreSQL。
1. 为什么AI编程的后端选型偏偏是Supabase
1.1 用AI写业务代码时,最怕后端逻辑藏在别处
过去用AI做全栈开发,最大的痛点不是界面生成得慢,而是后端逻辑没法让AI“看见”。如果用传统框架,业务逻辑散落在路由、服务层、数据库模型、中间件里,一整套上下文塞给AI往往超出上下文窗口,而且AI看不见真实的数据表结构,写出来的联表查询、权限判断经常是猜的。
Supabase把这个问题压缩到了很小的范围。它的数据访问是声明式的:一张表就是一个API端点,行级安全策略(RLS)就是权限声明,复杂查询可以用数据库函数(RPC)封装。AI只需要拿到建表SQL、策略SQL、一两个函数定义,就能完全理解这个后端的数据流。我试过让AI直接把“根据用户角色返回可见的订单列表”翻译成RLS策略加一个RPC函数,它一次写对的概率比我手写高不少。
这里有个反直觉的点:很多开发者觉得PostgreSQL太重、学习曲线太陡,不如MongoDB这种无模式的数据库好对付。实际配合AI编程时,模式和约束反而成为优势。AI生成的MongoDB查询经常在字段命名和嵌套结构上飘,因为文档结构隐藏在代码里;而PostgreSQL的表结构、列名、外键、索引是显式存在的,AI在生成SQL前能读到schema,准确率自然高。这就像给AI一张地图和让AI自己摸黑探路,差别非常明显。
1.2 Supabase的一组核心模块,每个都能被AI直接调用
拆开看,Supabase由几个相对独立的模块组成。理解每个模块能做什么,是后面用AI接管业务逻辑的前提。
Database:底层就是标准PostgreSQL,存储数据、执行SQL、约束和触发器。所有表结构可以直接在Studio控制台里建,也可以用supabase migrate这样的迁移方式管理。数据访问方式有两种:REST API(基于PostgREST,自动生成)和直接连数据库驱动。REST API适合前端调用,驱动适合后端和脚本。
Auth:邮件密码登录、OAuth第三方登录、匿名登录都支持。它把用户信息存在auth.users表里,表内的id字段可以直接在写规则时引用,像auth.uid()就是当前登录用户的ID。自定义的业务表通过用户ID字段和它关联,权限判断就围绕这个ID展开。
Storage:对象存储服务,底层是S3兼容协议,对外提供文件上传、下载 SDK。它最值钱的是把文件访问策略和数据库的RLS打通了,也就是说一个用户能上传、下载哪些文件,不是写死在应用逻辑里,而是用策略声明。
Realtime:数据库变化实时推送到客户端。它的实现原理不是轮询,而是监听PostgreSQL的WAL日志(Write-Ahead Log,预写日志),数据库有变更时通过WebSocket推给订阅端。聊天、通知、协作编辑类的功能都依赖它。
Edge Functions:跑在边缘的Serverless函数,基于Deno运行时。适合做Webhook、轻量接口聚合,官方建议在Supabase里优先用数据库函数Edge Functions,尽量少写重量级后端服务。
这些模块的共同特点是:配置和逻辑都以文本形态存在,并且有高度标准的描述方式。AI编程工具最擅长的就是处理和生成这类有明确规范、有大量公开语料可以参考的内容,所以Supabase天然是AI编程的好搭档。
1.3 适合场景:从AI原型到生产项目的跨度
我个人把适合用Supabase的场景分成三类。第一类是AI生成的MVP验证,比如一周内我就要给客户演示一个带登录、数据列表、报表的Demo,这时候用Supabase云服务版,Auth、REST API都现成,AI可以直接照着官方文档写前端调用代码,省掉整个后端搭建周期。第二类是做内部工具,用户量不大、数据敏感度中等,用本地部署版把数据存在自己服务器上,既保留PostgreSQL能力,又不用为单独的数据库和认证服务各写一套。第三类是中小规模的产品项目,用户量在几百到几十万之间,Supabase的性能完全扛得住,关键的是后续要扩展查询能力时,写原生SQL和创建索引这条路没有断。
不适合的场景也有,比如超大规模多租户架构、需要和既有企业系统深度集成的复杂业务。这时候直接用PostgreSQL加自己的服务框架,反而比套一个BaaS更清晰。做技术选型最忌讳为了用一个工具而扭曲业务架构,这个判断比选哪个工具本身重要得多。
2. 本地部署Supabase:5分钟起跑与四个坑
2.1 Docker Compose拉起整套服务
本地部署Supabase,官方推荐方式是直接用GitHub仓库里的Docker编排文件。整体思路很直接:用Docker Compose把PostgreSQL、Kong网关、Auth服务、Studio控制台、Storage服务、Realtime服务这些容器一次性拉起来。
第一步先装好Docker和Docker Compose插件,然后克隆官方docker仓库,复制环境变量模板文件。不同版本提供的编排细节略有差异,但大流程是一致的:
git clone --depth 1 https://github.com/supabase/docker cd docker cp .env.example .env打开.env文件,需要重点确认几个值。POSTGRES_PASSWORD决定数据库超级用户密码,部署后基本不会再变,一定要设成强密码。ANON_KEY和SERVICE_ROLE_KEY是API访问用的JWT密钥,开发环境下用默认值可以跑通,但如果是公网服务器上做私有化,务必要改成自定义生成的一对密钥。JWT生成方式在官方文档有现成脚本,也可以用openssl rand -base64 64生成随机SECRET,再基于它签出两个角色对应的JWT。
环境变量确认无误后,直接执行:
docker compose pull docker compose up -d首次启动会拉取一堆镜像,需要几分钟。全部容器变成healthy状态后,打开浏览器访问Studio控制台(默认端口根据版本不同一般是8000或者3000),输入默认密码就能进入可视化界面。
到这里本地版就跑起来了。能用的东西包括:PostgreSQL终端、REST API自动生成、Auth登录注册、Storage上传下载、Realtime订阅。用AI编程辅助的话,可以直接把.env里的数据库连接串告诉AI,让它帮忙生成初始表结构。
2.2 坑一:端口冲突,改了宿主端口却连不上Studio
我部署时遇到的第一个问题是端口占用。开发机上已经跑着一个本地PostgreSQL占着5432,Docker里的PostgreSQL默认又要映射5432,启动直接失败。
按惯常思路会去改.env里的POSTGRES_PORT,把它改成5433。从原理上说,这会让PostgreSQL容器映射到宿主机的5433端口,Studio内部通过网络访问数据库,不受这个映射影响。但如果你从宿主机直接用psql连接就需要连localhost:5433,而且项目前端的数据库连接池配置也得跟着改。这一连串改动里,最容易漏掉的是Studio控制台里保存的连接配置,或者后端服务里写死的端口。
我当时的排查过程是:先docker compose logs看哪个容器起失败,发现是db起不来,报端口绑定错误;接着确认了宿主机5432被占用;改成5433后,api容器起来但返回的REST API访问不通。继续看日志,发现Kong网关里写的是数据库服务名而不是宿主端口,问题根本不在端口映射上。最后定位到是.env里POSTGRES_PASSWORD和新容器的密码不一致——因为之前有一次启动失败,我重新生成了密码,但api容器读的是旧值。所以排查端口问题时要记住一句话:Docker内部用服务名互相访问,改宿主映射端口不会影响容器间通信,真正影响的是所有从宿主机直连的客户端。
2.3 坑二:邮件验证形同虚设
本地部署版默认没有配置真正的邮件发送服务。Auth模块可以正常注册用户,但发给用户的验证邮件、密码重置邮件实际发不出去,系统要么把邮件写入日志,要么直接报错。
对着日志排查时,会看到auth容器里类似“failed to send email”的报错。处理办法是在.env里配置SMTP服务商参数,把SMTP_HOST、SMTP_PORT、SMTP_USER、SMTP_PASS、SMTP_SENDER_NAME填好。如果没有邮件服务商,开发阶段最省事的方案是用Mailpit这类本地假邮件服务,把SMTP指到它,所有邮件都收进本地收件箱查看,完全不影响调试。
但如果是给客户部署的生产环境,这一步必须认真做。我经历的真实案例是,某次部署交付后客户注册页面一直转圈,检查发现认证服务日志里全是SMTP连接超时,最后发现部署机器所在网络屏蔽了云邮箱的SMTP端口。这个坑提醒我:本地部署版的邮件能力完全依赖外部服务,不是开箱即用,需要在部署前就确认好邮件通道,否则集成测试阶段全卡在收不到验证码上。
2.4 坑三:Storage的默认策略不是你以为的“私有”
云服务版开通Storage时,新建的bucket默认是私有的,文件访问要带Token。但自托管版的处理不一样,某些版本下Storage服务启动时默认策略比较宽松,新建的公共bucket可能任何人都能上传、下载,RLS策略还需要自己在控制台里手动加上。
这个差异很容易踩。我之前有次部署内部工具,上传文件走的是Supabase Storage,想着默认应该跟云服务版一样严格,结果用匿名请求直接下载到了一个不属于当前用户的文件。排查后确认,问题就出在自托管实例的初始RLS策略是空的,文件被当成公开资源处理。
解决办法:在Studio的SQL编辑器里手动给storage.objects表补上按用户隔离的策略。下面这个示例是让已登录用户只能访问自己目录下的文件:
create policy "用户可以读自己的文件" on storage.objects for select using ( auth.role() = 'authenticated' and bucket_id = 'user-files' and (storage.foldername(name))[1] = auth.uid()::text );不要因为本地部署就跳过这个步骤。安全配置在Demo阶段看不出问题,一旦放到公网,它就成了最直接的漏洞。
2.5 坑四:升级不能原地docker compose pull完事
自托管版最容易被低估的成本是升级维护。官方发布新版本后,本地部署不会自动升级,而且从旧版本跳到新版本不是单纯拉新镜像就行。数据库结构的迁移脚本、各服务间的兼容版本组合,都需要处理。
我的做法是:每次升级前先把当前数据库用pg_dump完整导出一份存入备份目录,再对照官方仓库的迁移说明,看有没有需要手动执行的SQL迁移文件。如果只是小版本升级,拉镜像重启基本没问题;跨大版本时,服务间接口可能不兼容,API网关和数据库之间的认证方式都可能变。有次升级后所有REST请求返回401,查了半天发现是新版本改了JWT secret的读取方式,.env里多了个新配置项,不填就用随机值导致签名对不上。
所以自托管部署从第一天起就要把升级当正事对待,不要以为它是家具,摆好就不用管。备份、迁移、兼容性测试这三件事,每次升级都逃不掉。
3. PostgreSQL高级特性在Supabase里的正确打开方式
3.1 RLS:云原生版的行级安全
RLS(Row Level Security,行级安全)是Supabase数据权限体系的核心。它的本质是PostgreSQL在查询执行层面加的一层过滤:对某张表启用RLS后,如果用户没有匹配的策略,查到的行数永远是0,而不是一把拒绝。这是很多人第一次接触时会困惑的地方——明明表里数据还在,但通过API查询就是查不到,原因是策略没写对。
启用RLS本身很简单,在SQL编辑器里执行:
alter table profiles enable row level security;然后创建策略。以“用户只能读自己的资料”为例:
create policy "读自己的资料" on profiles for select using (auth.uid() = id);auth.uid()在API请求里解析成当前登录用户的ID,id是业务表里关联用户的字段。这条策略的作用域是行级的,意思是即使前端用排序、分页、联表查询,最后返回的数据也只能是自己那一行。
真正的坑通常在for all或with check上面。using管的是“现有数据能不能读”,with check管的是“新写入的数据符不符合条件”。AI生成的策略经常漏掉其中一个。比如允许用户创建订单,如果把条件只写在using里、没写with check,可能就会出现用户能读到别人订单的意外。我经验是:凡是涉及到insert和update的策略,一定把with check一并写上;凡是AI写的策略,改完代码顺手进SQL编辑器执行一下,再用不同的登录身份验证一下能访问哪些行。
RLS的另一个好处是安全兜底。即使前端代码有漏洞,甚至在客户端直接拼SQL,策略依然会限制能操作的数据范围。这个“数据库层兜底”的思路,比在业务代码里到处加权限判断优雅很多。
3.2 触发器与审计字段:让数据库自己维护元数据
PostgreSQL的触发器在Supabase项目里最常见的用途是自动维护时间戳。很多业务表都有created_at和updated_at两个字段,如果靠应用代码每次写入时手动赋值,总会存在漏更新或者时钟不一致的问题。正确做法是在数据库层做约束。
先建一个通用的更新函数:
create or replace function public.set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$;再把它挂到目标表上:
create trigger trg_profiles_updated_at before update on profiles for each row execute function public.set_updated_at();此后任何UPDATE操作都会自动刷新updated_at,应用层完全不用管。AI开发时这类模板代码生成度非常高,但要注意函数放在哪个schema下、权限是security definer还是security invoker——用错了权限模型,函数内部访问其他表时会绕过RLS,形成越权风险。对这个机制不熟的开发者,看到函数能读到所有用户数据别慌,先查函数的权限配置是不是security definer,这是数据库函数权限模型引起的老问题。
3.3 全文搜索:英文分词开箱即用,中文要额外处理
PostgreSQL自带的全文检索能力,在Supabase里做站内搜索是个很实用的方案。它的原理是把文本转换成tsvector向量,再和用户的查询tsquery匹配,最后按相关性排序。
以文章表为例,一张表里加一个专门用于搜索的向量字段,并建GIN索引:
alter table articles add column search_vector tsvector; update articles set search_vector = to_tsvector('english', coalesce(title, '') || ' ' || coalesce(content, '')); create index articles_search_idx on articles using gin(search_vector);查询时直接用向量匹配:
select id, title from articles where search_vector @@ plainto_tsquery('english', 'postgresql') order by ts_rank(search_vector, plainto_tsquery('english', 'postgresql')) desc;这个方案里,英文分词是PostgreSQL内核自带支持的,english配置能处理词形变化,比如单复数、时态。但中文没有内置分词器,直接对中文文本做to_tsvector,结果会按单个字符切开,搜索效果很差。要做中文站内搜索,自托管版可以装pg_jieba、pg_bigm这类分词扩展,云服务版则要看平台是否允许安装自定义扩展。
实际项目里我用过比较省事的方式:中文搜索需求不复杂时,用PostgreSQL的ILIKE配合表达式索引就够了;文本量大、要求分词准确时,考虑把搜索场景单独拆出去,交给专门的检索引擎。技术选型没有银弹,PostgreSQL内置全文搜索适合中小规模场景,超过阈值再考虑外挂服务。
3.4 RPC函数:把复杂SQL封装成AI友好接口
PostgreSQL的create function在Supabase里扮演重要角色。它能把冗长的多表联查、复杂的业务计算封装成一个带名字的接口,前端和AI只需要调用函数名加参数,不用理解底层SQL。
RPC函数在Supabase里的打开方式是:创建函数后,通过PostgREST暴露成RPC端点,前端用supabase.rpc('函数名', {参数})就能调用。这个机制对AI编程特别友好,因为AI不用去猜复杂的查询怎么写,只需要知道函数的入参出参。
举一个搜索文章的RPC函数:
create or replace function public.search_articles(search_term text) returns setof articles language sql security invoker stable as $$ select * from articles where search_vector @@ plainto_tsquery('english', search_term) order by ts_rank(search_vector, plainto_tsquery('english', search_term)) desc limit 20; $$;调用时:
const { data } = await supabase.rpc('search_articles', { search_term: 'postgresql' });这里security invoker很重要,意思是函数以调用者的权限执行,继续受RLS约束;如果用security definer,函数就绕过RLS,这时候函数内部必须自己处理权限判断。新建RPC函数时,默认的安全策略通常建议用security invoker,除非你确定这个函数就是给服务端用的。
4. AI编程与Supabase的适配实操
4.1 让AI直接接管Schema的三种接入方式
我试过三种把Supabase项目接入AI编程工作流的方式,效果和适用场景各不相同。
第一种是官方提供的MCP Server接入。MCP(Model Context Protocol)是AI应用和外部工具之间的标准化连接协议,Supabase官方的MCP Server能让AI直接读数据库schema、执行受控查询、甚至管理迁移。在客户端工具里配置好MCP之后,AI会先拉取真实表结构,再基于真实结构生成代码,比凭空想象准确得多。AI能直接问我“profiles表里有没有user_id字段,我准备用这个字段做过滤”,这种对话体验完全改变了调试SQL的效率。
第二种是直接把schem递给AI。如果你用的AI工具不支持MCP,或者项目包含多个数据库环境,最朴素的办法是把表结构DDL整理成一段文本,粘贴进对话。PostgreSQL的DDL里每张表的字段、类型、默认值、外键、索引都写得明明白白,AI能从中推断关联关系和权限边界。关键是要养成给表和字段写COMMENT的习惯,AI做设计时才会考虑业务语义,而不是只凭字段名猜。
comment on table orders is '电商订单主表'; comment on column orders.status is '订单状态:pending/paid/shipped/completed/cancelled';第三种是约束AI只通过RPC函数操作数据。把写操作全部封装成数据库函数,前端只调用RPC,不在代码里裸写insert、update语句。这样AI碰不到敏感的原始表操作,所有业务逻辑都收敛在PostgreSQL内部,审计和测试都变得容易。
4.2 给AI写SQL时的约束清单
AI写SQL虽然强,但不会自动带上工程约束。几次踩坑后,我整理了一份约束清单,每次让AI生成或修改数据库相关代码前都会过一遍:
- 显式声明schema。AI生成的SQL经常不带
public.前缀,在搜索路径被改动的复杂项目里容易出错。要求它把所有表名写成schema.table格式。 - 启用RLS的表,任何涉及读写的SQL都要说明如何通过策略验证。AI写出来的查询在SQL编辑器里可能跑得很顺,但通过API执行时可能返回空结果,原因就是对RLS没概念。
- 参数必须是带类型的。数据库函数入参都明确写类型,不要让AI生成
any类型。 - 关联查询务必确认外键关系。AI有时会脑补不存在的关联字段,生成能跑但逻辑错误的SQL。生成完看一遍执行计划,成本很低。
- 不做全表扫描。AI生成的搜索查询经常忘了限制返回条数,生产环境要加上
limit。
这组约束不是教条,是拿真实故障换来的。我见过最典型的例子是:AI生成了一条“查询所有未支付订单”的SQL,一看执行计划是seq scan(全表扫描),订单量起来后迟早把数据库拖垮。后来加上“所有查询都要有对应的索引支持和合理的返回上限”这条约束,这类问题少了大半。
4.3 一个实战案例:用AI从需求到API的全流程
用一个真实做过的模拟项目来串一下流程:做一个内部任务管理工具,用户登录后能创建任务、看到自己创建的任务、把任务指派给他人,任务有状态和截止时间。
我直接把需求描述给AI,附带约束:用Supabase PostgreSQL实现,启用RLS,RPC封装复杂逻辑。AI生成的数据库设计大致是:
create table tasks ( id uuid primary key default gen_random_uuid(), title text not null, assignee_id uuid references auth.users(id), creator_id uuid references auth.users(id), status text not null default 'todo', due_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); alter table tasks enable row level security; create policy "任务对组织内成员可见" on tasks for select using (true); create policy "登录用户可以创建任务" on tasks for insert with check (auth.uid() = creator_id);第一直觉就能发现两个问题:select策略写成了using (true),意味着任何登录用户可以读所有任务,如果任务只对成员可见,思路是OK的,但要确认是公司全体的任务而不是个人私密任务。第二个是更新任务的策略缺失,AI默认认为“能读就能写”,这不是自动成立的。
我让AI补上更新策略,要求“只能更新自己创建的任务或被指派给自己的任务”,它生成:
create policy "用户可以更新自己的任务" on tasks for update using (auth.uid() = creator_id or auth.uid() = assignee_id) with check (auth.uid() = creator_id or auth.uid() = assignee_id);到这里逻辑才算闭合。整个过程中,AI负责快速产出方案,人的价值在于识别权限边界、补齐业务约束。这恰好印证了前面说的:Supabase的RLS模型把权限决策显式化,AI更容易理解并补全,人的审查负担反而小。
5. 云服务版和自托管的取舍:成本、备份与升级策略
5.1 一张表看清差异
我整理了一张对比表,包含了选型时最关心的几个维度:
| 维度 | 云服务版 | 自托管版 |
|---|---|---|
| 启动成本 | 最快,注册即用,免费档够学习 | 需要Docker基础,环境配置半小时起 |
| 数据库备份 | 平台自动备份,可配置时间点恢复 | 自己规划,pg_dump或第三方备份工具 |
| 邮件服务 | 内置SMTP发送能力 | 需要自己配置SMTP服务商 |
| 功能更新 | 新功能、插件跟随官方节奏更新 | 升级依赖手动操作和迁移脚本 |
| 可扩展插件 | 受平台约束,支持列表有限 | 完全掌控,可装任意PostgreSQL扩展 |
| 数据主权 | 数据在平台服务器 | 数据完全在自己服务器 |
| 成本模型 | 免费额度+按量计费,规模大了费用明显 | 一台服务器费用固定,无按量费用 |
| 运维负担 | 平台兜底,数据库性能问题仍需自己优化 | 全部自己扛,包括监控和告警 |
这个表的核心逻辑是:选云服务版买的是“时间”,把运维时间节省下来投入到业务上;出自托管版买的是“自由度”和“数据控制权”。真正决定选哪个的,不是技术能力,而是你到底想把精力花在哪里。
5.2 什么阶段选云,什么阶段必须本地
从我个人的项目经验来看,有几条判断规则值得参考。
刚起步的项目、验证商业想法的小产品、教学和AI编程练习项目,无脑选云服务版。免费档的额度和学习成本都是零负担,项目失败了损失也微乎其微。云版的问题不是功能不够,而是当你的用户量和请求量上来后,费用会变成一个需要持续关注的变量。很多团队在免费额度内用得很爽,突然一个月账单超出预期,才开始焦虑。
有数据合规诉求的项目、需要给客户做私有化部署的项目、需要深度定制PostgreSQL插件的场景,自托管就是不二之选。给客户交付时,把整套Docker编排拷过去就能在客户的服务器上跑起来,客户对数据安全也放心。
还有一种折中方案:开发期用云版顺手,生产期用自托管承接,中间用迁移机制切换。PostgreSQL是同一个,只是换了部署环境,数据导出导入都顺畅。这个方式适合已经有经验、想控制成本的团队。
5.3 自托管升级与备份的兜底方案
自托管有一个绕不开的日常场景:数据备份和升级迁移。
备份最基础的做法是定期pg_dump整库导出,配合定时任务写一个脚本。pg_dump导出的SQL文件可以随时在另一个环境恢复,简单粗暴但有效。注意两点:可靠的备份不仅要定时做,还要定期真的 восстановить一遍,否则备份文件可能因为环境差异已经恢复不了;其次数据库连接串里的密码、AK/SK这类敏感信息,不要写死在备份脚本的明文参数里,用环境变量或密钥管理工具统一存放。
升级迁移的兜底方案,我用的是三步走:第一步,完整备份当前数据;第二步,对照官方版本的迁移说明,把需要的SQL迁移脚本在测试环境跑一遍;第三步,测试环境验证通过后,再在生产环境操作。这套流程和大型系统中的数据库发布流程一致,只不过规模小一些。切忌直接在生产库上执行未经测试的迁移脚本,一个错误的drop column就能让业务停摆。
说说我的实际体会
这几个项目做下来,我对Supabase最大的感受是:它的价值不在某个单一功能有多强,而在于把PostgreSQL的复杂性包装成了AI和人都能快速理解的接口。本地部署版虽然多了一些运维活,但数据在自己手里、扩展不受限制,心情踏实不少。如果只是快速验证想法,云服务版值得从第一天就上手;如果是交付给客户或做长期产品,自托管的初期投入也完全值得。