Supabase 核心原理:PostgreSQL 原生后端与 RLS 权限设计

1. 项目概述:为什么 Supabase 是当下最值得投入的开源后端基建选择

Supabase 不是另一个“又一个 Firebase 克隆”,它是一次对现代后端开发范式的系统性重写。我从 2021 年初开始在三个不同规模的 SaaS 产品中落地 Supabase,从日活 200 的内部工具,到支撑 5 万用户并发的教育平台,再到需要对接医保结算系统的医疗 Saas,它的核心价值从来不是“长得像 Firebase”,而是用 PostgreSQL 这个被验证了 25 年的工业级数据库作为唯一数据底座,把实时能力、身份认证、文件存储、函数服务全部构建成可预测、可审计、可调试、可迁移的 SQL 原生扩展。你不需要记住一堆 SDK 方法名,所有操作最终都映射为一条SELECT、一条INSERT或一个NOTIFY事件。这直接消除了传统 BaaS(Backend-as-a-Service)最大的隐性成本:当业务增长到需要自建后端时,90% 的 Firebase 项目必须推倒重来——因为它的数据模型、权限逻辑、触发器机制全部绑定在私有协议和闭源服务上。而 Supabase 的整个权限系统(Row Level Security, RLS)就是几行 SQL 注释;它的实时订阅底层就是 PostgreSQL 的LISTEN/NOTIFY;它的函数就是标准的 PL/pgSQL 或 JavaScript 函数。这意味着,当你某天决定把 Supabase 拆解成独立微服务时,数据库 schema、RLS 策略、函数定义、甚至备份脚本,全都可以原封不动地迁移到你自己的云服务器或 Kubernetes 集群里。这不是“未来可能兼容”,这是“今天就运行在你的 PostgreSQL 里”。对于正在做数据库课程设计的学生、想快速验证 MVP 的创业者、或是被 Firebase Unity iOS 打点后台看不到数据折磨得睡不着觉的移动开发者,Supabase 提供的不是“替代品”,而是一条从零代码原型到企业级架构的平滑演进路径。它把后端开发的复杂度,从“学框架”降维到了“写 SQL”。

2. 核心设计思路与方案选型深度拆解

2.1 为什么是 PostgreSQL 而不是 MongoDB 或 SQLite?

Supabase 的技术选型不是为了标新立异,而是直面真实业务场景中的刚性约束。我曾用 MongoDB 搭建过一个活动报名系统,初期开发飞快,但当运营同事要求“查出所有在 A 时间段报名、B 地区、且 C 课程完成率大于 80% 的用户,并按 D 字段分组统计”时,聚合管道写到第 7 层嵌套,查询耗时飙升到 3.2 秒,而同样的需求在 PostgreSQL 里,一条带JOINHAVING的 SQL 在毫秒级返回。这就是关系型数据库不可替代的根基:强一致性语义成熟的优化器。Supabase 的整个 RLS 权限模型,依赖于 PostgreSQL 的USINGWITH CHECK子句,这些子句在每次SELECT/INSERT/UPDATE/DELETE时由数据库引擎自动注入并执行,其安全性不依赖于应用层 SDK 的调用顺序或版本兼容性。反观 Firebase 的 Security Rules,本质是客户端 JS 引擎解释执行的沙盒脚本,规则越复杂,客户端解析开销越大,且无法进行跨集合的原子性校验(比如“转账”操作必须同时扣减 A 账户余额并增加 B 账户余额,Firebase 无法保证这两步的 ACID)。而 Supabase 的函数(Stored Procedures)可以轻松封装这种事务逻辑,一行CALL transfer_funds('user_a', 'user_b', 100.00)就能完成。

提示:Supabase 的“实时”能力并非自己实现的 WebSocket 服务器,而是监听 PostgreSQL 的pg_notify通道。这意味着,即使你绕过 Supabase 客户端,直接用psql执行NOTIFY realtime_channel, '{"id": 123}';,所有订阅该通道的前端也会立刻收到消息。这种透明性,让调试变得极其简单——你永远知道数据变更的源头在哪里。

2.2 自建 vs 托管:何时该选择自建 Supabase?

Supabase 官方提供免费托管服务(supabase.com),这对个人项目和小团队极具吸引力。但我的经验是:一旦你的数据涉及任何合规性要求(如 GDPR、HIPAA、等保2.0),或你需要对网络拓扑、备份策略、监控粒度进行精细控制,自建就是唯一选项。举个真实案例:我们为一家三甲医院开发的随访系统,其数据库必须部署在院内私有云,所有网络流量不能出内网。使用托管服务意味着要打通内外网隧道,这会引入额外的审计风险和运维复杂度。而自建 Supabase,我们只需将官方 Docker Compose 文件稍作修改:把POSTGRES_HOST指向院内已有的 PostgreSQL 集群(而非 Supabase 自带的db容器),关闭anonservice_role密钥的公网暴露,再通过 Kubernetes Ingress 配置 TLS 终止和 IP 白名单,整个过程不到 2 小时。更重要的是,自建让你完全掌控数据生命周期。Supabase 的备份不是简单的“一键快照”,而是利用 PostgreSQL 原生的pg_dump和 WAL 归档。我们可以配置每日全量备份 + 每 15 分钟一次 WAL 归档,恢复点可以精确到秒级,这是任何托管服务都无法承诺的 SLA。

2.3 架构全景图:Supabase 不是单体,而是一个协同生态

很多人误以为 Supabase 是一个“大而全”的单体应用,其实它是一个由 7 个核心服务组成的松耦合集群,每个服务都可独立伸缩和替换:

服务名称核心职责可替换性关键配置项
PostgREST将 HTTP 请求翻译为 SQL 查询,提供 RESTful API高(可用其他 PostgREST 实现)db-uri,jwt-secret,schema
Realtime监听 PostgreSQL 的pg_notify,通过 WebSocket 推送变更中(需兼容 NOTIFY 协议)postgres-url,redis-url
GoTrue处理用户注册、登录、密码重置、邮箱验证高(可集成 Auth0、Keycloak)jwt-secret,external-*(第三方登录)
Storage API对象存储服务,支持 S3 兼容接口高(可指向 MinIO、AWS S3)storage-s3-bucket,storage-s3-region
Edge Functions运行用户自定义的无服务器函数(Deno)中(需适配 Deno Runtime)deno-pkg-url,deno-runtime-version
Vector Extensions提供 pgvector 扩展,支持向量相似度搜索高(需 PostgreSQL 14+ + pgvector)pgvector-version,embedding-dim
DB Web UI (Studio)基于 Web 的数据库管理界面低(高度定制化)studio-host,studio-port

这个设计意味着,你可以用 Supabase 的 Realtime 服务搭配你自己的 PostgreSQL,或者用 Supabase 的 GoTrue 认证服务搭配你自己的 REST API。这种“乐高式”组合能力,是它超越 Firebase 的根本原因——Firebase 是一个封闭的黑盒,而 Supabase 是一套开放的、可插拔的协议栈。

3. 自建全流程实操:从零开始搭建生产级 Supabase

3.1 环境准备与基础依赖安装

自建 Supabase 的最低硬件要求远低于普遍认知。我曾在一台 2 核 4GB 内存的腾讯云轻量应用服务器(CVM)上成功运行了包含 3 个项目的 Supabase 实例,日均处理 20 万次 API 请求。关键不在于 CPU 和内存,而在于I/O 性能网络延迟。PostgreSQL 对磁盘随机读写非常敏感,因此务必使用 SSD 云盘(如腾讯云 CBS、阿里云 ESSD),并禁用 swap 分区(sudo swapoff -a && sudo sed -i '/swap/d' /etc/fstab)。操作系统推荐 Ubuntu 22.04 LTS,因其内核对容器网络和 PostgreSQL 的优化最为成熟。

第一步是安装 Docker 和 Docker Compose v2:

# 安装 Docker CE curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo usermod -aG docker $USER # 重启 shell 或执行 newgrp docker # 安装 Docker Compose v2(必须!v1 已废弃) sudo mkdir -p /usr/libexec/docker/cli-plugins sudo curl -SL https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64 -o /usr/libexec/docker/cli-plugins/docker-compose sudo chmod +x /usr/libexec/docker/cli-plugins/docker-compose

注意:Supabase 官方文档有时仍引用旧版 Compose 命令(如docker-compose up),请务必统一使用docker compose up(无连字符)。这是 Docker 官方强制切换的分水岭,旧命令在新环境中会报错。

3.2 获取并定制官方 Docker Compose 文件

Supabase 的核心是其docker-compose.yml文件。不要直接下载官网提供的“一键部署”脚本,那只是演示用途。生产环境必须使用其 GitHub 仓库中的prod配置:

# 创建项目目录 mkdir -p ~/supabase-prod && cd ~/supabase-prod # 下载生产级配置(注意:不是 master 分支,而是 prod 分支) curl -L https://raw.githubusercontent.com/supabase/supabase/prod/docker/docker-compose.prod.yml -o docker-compose.yml curl -L https://raw.githubusercontent.com/supabase/supabase/prod/docker/.env.example -o .env

此时,.env文件是空的模板,需要根据你的环境填充。最关键的 5 个变量是:

  • POSTGRES_PASSWORD: 数据库超级用户的密码(必须强密码,至少 16 位,含大小写字母、数字、符号
  • JWT_SECRET: 用于签名 JWT Token 的密钥(必须与 GoTrue 服务的jwt_secret完全一致,否则登录失败
  • ANON_KEY: 匿名访问密钥(前端 SDK 初始化时使用)
  • SERVICE_ROLE_KEY: 服务角色密钥(拥有数据库最高权限,仅后端可信服务使用)
  • DATABASE_URL: 如果你希望复用现有 PostgreSQL,这里填postgresql://user:pass@host:port/dbname

实操心得:JWT_SECRETSERVICE_ROLE_KEY必须用openssl rand -base64 32生成,绝不能手写。我曾因手输一个0(零)和O(大写字母 O)混淆,导致连续 3 小时无法登录 Studio,最后用diff对比才发现差异。

3.3 启动与首次初始化:绕过“欢迎页陷阱”

执行docker compose up -d后,服务会启动,但此时你无法访问http://localhost:3000(Studio UI),因为 Supabase 的初始化流程是异步的,且依赖于db容器的完全就绪。官方文档没有明确说明这个等待窗口,导致大量新手卡在这里。正确做法是:

# 查看数据库容器日志,等待出现 "database system is ready to accept connections" docker logs -f supabase-db-1 # 当看到上述日志后,Ctrl+C 退出,再执行初始化脚本 docker exec -it supabase-db-1 psql -U postgres -c "CREATE DATABASE supabase;" docker exec -it supabase-db-1 psql -U postgres -d supabase -c "CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";"

然后,手动触发 Supabase 的初始化 SQL 脚本(该脚本位于supabase-db-1容器内):

# 进入数据库容器 docker exec -it supabase-db-1 bash # 在容器内执行(注意路径) psql -U postgres -d supabase -f /docker-entrypoint-initdb.d/001-init.sql # 退出容器 exit

这个001-init.sql脚本会创建authstoragerealtime等核心 schema,并设置初始 RLS 策略。只有当此脚本成功执行后,supabase-studio-1容器才会显示欢迎页面。如果跳过此步,Studio 会一直显示“Initializing...”,且所有 API 返回 500 错误。

3.4 配置 HTTPS 与域名:生产环境的生死线

Supabase 的 GoTrue 认证服务(处理登录/注册)强制要求 HTTPS。如果你在http://localhost:3000上测试登录,会看到浏览器控制台报错Mixed Content,因为 GoTrue 会尝试从https://your-domain.com/auth/v1/token获取 Token。因此,在生产环境,必须配置反向代理。我推荐使用 Nginx(比 Caddy 更稳定,社区支持更广):

# /etc/nginx/sites-available/supabase.conf upstream supabase_api { server 127.0.0.1:54321; # PostgREST 默认端口 } upstream supabase_realtime { server 127.0.0.1:4000; # Realtime 默认端口 } upstream supabase_studio { server 127.0.0.1:3000; # Studio 默认端口 } server { listen 443 ssl http2; server_name api.your-domain.com; ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; location / { proxy_pass http://supabase_api; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 443 ssl http2; server_name realtime.your-domain.com; ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; location / { proxy_pass http://supabase_realtime; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

关键细节:WebSocket 连接必须设置proxy_http_version 1.1Upgrade头,否则 Realtime 功能会静默失败。我曾因漏掉proxy_http_version 1.1,导致前端supabase.channel().subscribe()无任何错误,但永远收不到消息,排查了整整一天。

3.5 数据库 Schema 设计与 RLS 权限实战

Supabase 的灵魂在于其 Row Level Security(RLS)。它不是“开关”,而是一套完整的 SQL 策略语言。以一个常见的“博客系统”为例,我们需要profiles(用户资料)和posts(文章)两张表:

-- 创建 profiles 表 create table profiles ( id uuid references auth.users not null primary key, updated_at timestamp with time zone, username text unique, avatar_url text, full_name text ); -- 创建 posts 表 create table posts ( id bigint generated by default as identity primary key, user_id uuid references profiles(id) on delete cascade not null, title text check (char_length(title) > 0), content text, created_at timestamp with time zone default now(), published boolean default false );

现在,为profiles表添加 RLS 策略:

-- 启用 RLS alter table profiles enable row level security; -- 策略1:用户只能查看自己的资料(SELECT) create policy "Users can view own profile" on profiles for select using (auth.uid() = id); -- 策略2:用户只能更新自己的资料(UPDATE) create policy "Users can update own profile" on profiles for update using (auth.uid() = id); -- 策略3:任何人都可以查看公开资料(SELECT),但只显示部分字段 create policy "Public profiles are viewable by everyone" on profiles for select using (true);

注意using (true)这个策略,它允许任何人SELECT * FROM profiles,但结合 PostgreSQL 的列级权限,我们可以进一步限制:

-- 只允许匿名用户 SELECT username 和 avatar_url,隐藏 email 和 full_name revoke select on profiles from anon; grant select (id, username, avatar_url) on profiles to anon; grant select (id, username, avatar_url, full_name, email) on profiles to authenticated;

这种“策略叠加”机制,让权限设计变得极其灵活。你不再需要在应用层写if (user.role === 'admin') { ... },所有逻辑都在数据库里,且经过了 25 年的生产环境锤炼。

4. 核心功能深度解析与避坑指南

4.1 Realtime 实时订阅:不只是“推送”,而是“状态同步”

Supabase 的 Realtime 不是简单的消息推送,它是基于 PostgreSQL 的 MVCC(多版本并发控制)机制实现的状态同步。当你执行supabase.from('posts').on('INSERT', ...).subscribe()时,SDK 并非监听一个全局频道,而是向 Realtime 服务发送一个 SQL 查询(如SELECT * FROM posts WHERE published = true),Realtime 服务会将此查询编译为一个 PostgreSQL 的LISTEN通道,并在posts表发生INSERT/UPDATE/DELETE时,自动广播变更前后的完整行数据(OLDNEW记录)。

这带来两个关键优势:

  1. 精准过滤:你可以在on()方法中传入filter参数,如.on('UPDATE', { filter: 'published=eq.true' }),Realtime 服务会在数据库层面执行WHERE published = true,只推送符合条件的变更,极大减少网络带宽。
  2. 状态一致性:前端收到的不是“事件”,而是“当前状态”。例如,一个UPDATE事件会同时包含OLD(更新前)和NEW(更新后)两行数据,前端可以据此计算 diff,更新局部 UI,而无需重新SELECT整个列表。

常见问题:为什么on('INSERT')收不到消息?
答案:检查posts表是否启用了 RLS。如果INSERT策略中WITH CHECK子句为false(如create policy "Users can insert own posts" on posts for insert with check (auth.uid() = user_id);),而插入时user_id不匹配,那么INSERT会被数据库拒绝,自然也不会触发NOTIFY。此时,API 会返回 403 错误,但 Realtime 不会广播任何消息。Realtime 只广播成功的 DML 操作

4.2 Storage 文件存储:如何安全地上传用户头像

Supabase Storage 是一个 S3 兼容的对象存储服务,但它与 AWS S3 的最大区别在于:所有权限都由 RLS 控制,而非 IAM 策略。这意味着,你可以为每个“Bucket”(存储桶)设置独立的 RLS 策略,精细到每个文件。

假设我们要实现“用户只能上传和读取自己的头像”:

-- 创建名为 'avatars' 的 bucket insert into storage.buckets (id, name) values ('avatars', 'avatars'); -- 为 'avatars' bucket 设置 RLS create policy "Avatar images are publicly accessible" on storage.objects for select using (bucket_id = 'avatars'); create policy "Users can upload their own avatar" on storage.objects for insert with check ( bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1] -- 文件名格式:'user_id/avatar.png' );

前端上传代码如下:

// Vue 3 Composition API 示例 const uploadAvatar = async (file) => { const fileExt = file.name.split('.').pop().toLowerCase() const fileName = `${auth.user.id}/avatar.${fileExt}` const { data, error } = await supabase .storage .from('avatars') .upload(fileName, file, { contentType: file.type, upsert: true // 覆盖同名文件 }) if (error) throw error return data }

关键点在于fileName的构造:auth.user.id作为文件夹名,这样 RLS 策略(storage.foldername(name))[1]就能提取出第一级文件夹(即用户 ID),并与auth.uid()比较。这种方式比在元数据(metadata)中存 user_id 更安全,因为元数据可以被篡改,而文件路径是对象存储的固有属性。

4.3 Edge Functions:用 Deno 编写无服务器函数

Supabase 的 Edge Functions 运行在 Deno(一个安全的 TypeScript 运行时)上,它与传统 Node.js 函数的最大区别是默认无权访问文件系统、网络或环境变量,所有权限都需显式声明。这从根本上杜绝了“函数逃逸”风险。

一个典型场景:发送邮件通知。Firebase 的 Cloud Functions 可以直接require('nodemailer'),但 Supabase 的 Edge Function 必须:

  1. 在函数头部声明net权限:// @deno-types="https://deno.land/x/nodemailer@v6.9.7/mod.ts"
  2. 使用Deno.env.get('SMTP_HOST')获取环境变量(需在supabase/functions/notify_email/deploy.sh中配置);
  3. 所有外部请求必须通过fetch(),且 URL 必须在--allow-net参数中白名单。
// supabase/functions/notify_email/index.ts import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; // 从环境变量获取 SMTP 配置 const SMTP_HOST = Deno.env.get("SMTP_HOST") || "smtp.gmail.com"; const SMTP_PORT = parseInt(Deno.env.get("SMTP_PORT") || "587"); serve(async (req) => { const { email, message } = await req.json(); // 此处调用 nodemailer 发送邮件... return new Response(JSON.stringify({ sent: true }), { headers: { "Content-Type": "application/json" }, }); });

部署时,必须指定权限:

# 在函数目录下执行 supabase functions deploy notify_email --no-verify-jwt --allowed-origins="https://your-app.com" --import-map=import_map.json

注意--no-verify-jwt:此参数表示该函数不验证 JWT Token,适用于 Webhook 回调等场景。但对于需要用户上下文的函数(如“获取用户订单”),必须去掉此参数,并在函数内通过req.headers.get('Authorization')解析 Token。

4.4 向量数据库集成:pgvector 的实战应用

Supabase 对pgvector的支持,让它从一个“数据库后端”升级为一个“AI 应用基础设施”。pgvector是一个 PostgreSQL 扩展,它将向量(数组)作为一等公民,支持高效的余弦相似度、欧氏距离等计算。

以“知识库问答”为例,我们有一个documents表,其中embedding列存储 1536 维的向量:

-- 添加 pgvector 扩展 create extension if not exists vector; -- 创建 documents 表 create table documents ( id bigserial primary key, content text not null, embedding vector(1536) -- OpenAI ada-002 模型的输出维度 ); -- 创建索引(至关重要!否则查询极慢) create index on documents using ivfflat (embedding vector_cosine_ops) with (lists = 100);

查询最相似的 3 个文档:

select content, 1 - (embedding <=> '[0.1,0.2,...,0.9]') as similarity from documents order by embedding <=> '[0.1,0.2,...,0.9]' limit 3;

这里的<=>pgvector提供的余弦距离操作符。ivfflat索引是近似最近邻(ANN)搜索的关键,它将向量空间划分为lists个簇,查询时只搜索最相关的几个簇,将 O(n) 的暴力搜索优化为 O(log n)。

实操心得:lists参数的选择有经验公式:lists = sqrt(num_rows)。例如,你的documents表有 100 万行,则lists = 1000。设置过小会导致召回率下降(找不到真正相似的文档),设置过大会增加索引体积和查询延迟。我建议先用100测试,再根据EXPLAIN ANALYZE的实际执行计划调整。

5. 常见问题排查与独家避坑技巧

5.1 “Firebase Unity iOS 打点后台看不到数据”问题的 Supabase 解法

这是一个高频痛点。Firebase 的 iOS SDK 在某些网络环境下(如企业内网、特定运营商)会因证书链问题或 DNS 污染,导致打点请求(/v1/projects/xxx/events:batchWrite)超时或 403,且错误日志极其晦涩。Supabase 的解决方案是完全透明的 HTTP 协议

  1. 抓包定位:在 iOS 设备上安装 Proxyman ,配置设备代理,然后运行你的 App。你会清晰地看到所有POST https://api.your-domain.com/rest/v1/请求,包括完整的请求头、Body 和响应 Body。
  2. 检查 JWT Token:Unity SDK 初始化时,会将anon_key作为apikey放在Authorization头。确保anon_key.env文件中的一致,且没有多余的空格。
  3. 验证 RLS 策略:iOS 端的supabase.from('events').insert(...)会触发INSERT策略。如果策略中WITH CHECKfalse,API 会返回403 Forbidden,Proxyman 会明确显示此错误。此时,只需在 Studio 的 SQL 编辑器中执行SELECT * FROM pg_policies WHERE schemaname = 'public' AND tablename = 'events';查看策略详情。

5.2 “Vue 访问后端服务”连接失败的 5 种可能

Vue 应用连接 Supabase 失败,90% 的情况源于配置错误。以下是按发生概率排序的排查清单:

问题序号现象检查方法解决方案
1Network ErrorERR_CONNECTION_REFUSED在浏览器控制台执行fetch('https://api.your-domain.com/rest/v1/')检查 Nginx 是否监听 443 端口,server_name是否匹配,SSL 证书是否有效(curl -v https://api.your-domain.com
2401 Unauthorized检查 Vue 初始化代码createClient({ supabaseUrl: '...', supabaseAnonKey: '...' })确保supabaseAnonKey.env中的ANON_KEY不是SERVICE_ROLE_KEY
3403 Forbidden在 Studio 的Authentication->Providers中查看Email是否启用确保GoTrueENABLE_EMAIL_SIGNUP环境变量为true
4422 Unprocessable Entity检查INSERT的 JSON Body 是否符合表结构(如NOT NULL字段缺失)在 Studio 的Table Editor中,点击表名旁的i图标,查看各字段的Null?Default属性
5500 Internal Server Error查看supabase-postgrest-1容器日志:docker logs supabase-postgrest-1日志中会显示具体的 SQL 错误,如permission denied for table xxx,此时需检查 RLS 策略或GRANT权限

5.3 数据库并发锁与性能优化

PostgreSQL 的行级锁(SELECT ... FOR UPDATE)在高并发场景下极易成为瓶颈。Supabase 的auth.users表就是一个典型例子:当大量用户同时注册,GoTrue 会尝试INSERT INTO auth.users,如果email字段有唯一索引,PostgreSQL 会对冲突的索引页加锁,导致后续插入排队。

终极解决方案:使用INSERT ... ON CONFLICT DO NOTHING替代INSERT,并在应用层处理冲突:

-- 在 GoTrue 的注册流程中,将 INSERT 改为 INSERT INTO auth.users (id, email, encrypted_password, ...) VALUES ($1, $2, $3, ...) ON CONFLICT (email) DO NOTHING;

这样,当邮箱重复时,数据库不会加锁,而是静默忽略,GoTrue 会捕获ON CONFLICT事件并返回400 Bad Request。虽然增加了应用层逻辑,但彻底消除了锁竞争。

独家技巧:对于需要“抢购”、“秒杀”的业务,不要在数据库里做UPDATE stock = stock - 1 WHERE id = 123 AND stock > 0。这会产生间隙锁(Gap Lock),阻塞所有范围查询。正确做法是:先SELECT stock FROM products WHERE id = 123 FOR UPDATE NOWAIT,如果 stock > 0,则UPDATE,否则抛出异常。NOWAIT是关键,它让锁等待变为立即失败,避免长事务。

5.4 Docker 镜像打包与 CI/CD 最佳实践

将 Supabase 服务打包成 Docker 镜像,不是为了“炫技”,而是为了环境一致性灰度发布。Supabase 官方镜像(supabase/postgres,supabase/realtime)是基于 Alpine Linux 的精简版,但它们不包含pg_dumppsql等调试工具。生产环境必须构建自己的镜像:

# Dockerfile.supabase-db FROM supabase/postgres:15.3.0.111 # 安装调试工具 RUN apk add --no-cache postgresql-client # 复制自定义初始化脚本 COPY init-db.sh /docker-entrypoint-initdb.d/ RUN chmod +x /docker-entrypoint-initdb.d/init-db.sh # 暴露 pgAdmin 端口(仅限开发环境) EXPOSE 5432

CI/CD 流程中,我使用 GitHub Actions:

# .github/workflows/deploy.yml name: Deploy Supabase on: push: branches: [main] paths: ['docker-compose.yml', '.env'] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy to Server uses: appleboy/scp-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} source: "docker-compose.yml,.env" target: "/home/ubuntu/supabase-prod/" - name: SSH Deploy uses: appleboy/ssh-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | cd /home/ubuntu/supabase-prod docker compose down docker compose up -d --remove-orphans # 等待数据库就绪 docker logs -f supabase-db-1 --tail 10 | grep "ready to accept connections" || exit 1 echo "Supabase deployed successfully!"

这个流程确保了每次部署都是原子性的:docker compose down会停止所有容器,--remove-orphans会清理旧配置残留,grep "ready to accept connections"是健康检查,避免在数据库未就绪时就认为部署成功。

6. 从入门到精通:我的三年 Supabase 实战体悟

我在 2021 年第一次接触 Supabase 时,把它当作一个“高级的 Firebase”,直到我负责重构一个日活 50 万的在线教育平台的后端。当时,平台的 Firebase 项目已经积累了超过 200 个 Security Rules,每次上线新功能,都要花半天时间在模拟器里反复测试规则是否覆盖了所有边界条件,而线上却依然偶发 403 错误。切换到 Supabase 后,我把所有 Rules 迁移为 RLS 策略,整个过程只用了 3 天。最震撼的是,当我需要为“直播课”功能添加一个复杂的权限:学生只能在开课前 15 分钟到下课后 30 分钟内进入房间,且同一课程的多个班级共享一个房间。在 Firebase 里,这需要写一个嵌套 5 层的规则,而在 Supabase 里,它就是一行 SQL:

create policy "Students can join live class within time window" on live_rooms for select using ( auth.uid() in (select user_id from enrollments where course_id = live_rooms.course_id) AND now() between (start_time - interval '15 minutes') and (end_time + interval '30 minutes') );

这行代码,数据库