
Byted Supabase
- 33 installs
- 411 repo stars
- Updated August 4, 2026
- bytedance/agentkit-samples
byted-supabase is a Claude skill that manages Volcengine's Supabase-edition BaaS platform (workspaces, databases, Auth, Realtime, Edge Functions, Storage) via the byted-supabase-cli.
About
This skill manages Volcengine's AI-native BaaS platform, the Supabase edition, which differs from official open-source Supabase. A developer uses it to create and inspect workspaces, branches, run SQL and schema changes, and configure Auth, Realtime, Edge Functions, Storage, and TypeScript type generation through the byted-supabase-cli tool. It includes a Supabase-specific security and RLS checklist and flags high-risk operations like stopping a workspace.
- Manages Volcengine's Supabase-edition BaaS via the byted-supabase-cli
- Covers workspaces, branches, SQL, Auth, Realtime, Edge Functions and Storage
- Bundles a Supabase-specific RLS and security checklist
Byted Supabase by the numbers
- 33 all-time installs (skills.sh)
- Ranked #3,349 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
byted-supabase capabilities & compatibility
Requires a Volcengine account and byted-supabase-cli auth via OAuth login or AK/SK
- Capabilities
- supabase management · database · auth management · edge functions · storage management
- Works with
- supabase · postgres
- Use cases
- database · api development · devops
- Platforms
- macOS · Linux
- Pricing
- Bring your own API key
What byted-supabase says it does
Manage Volcengine's AI-native BaaS platform (Supabase edition / 火山引擎 AI 原生 BaaS 平台 Supabase 版)
installed via `npm i -g @byted-supabase/cli`; this is NOT the official `supabase` CLI
npx skills add https://github.com/bytedance/agentkit-samples --skill byted-supabaseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 411 |
| Last updated | August 4, 2026 |
| Repository | bytedance/agentkit-samples ↗ |
What it does
Manage Volcengine Supabase workspaces, databases, Auth, Storage, and Edge Functions through the byted-supabase-cli.
Who is it for?
Skip if: General database discussions, non-Supabase services, or pure client-side coding unrelated to Supabase backend management.
When should I use this skill?
You need to create, inspect, or manage Volcengine Supabase workspaces, branches, SQL, Auth, Realtime, Edge Functions, Storage, or generate TypeScript types.
What you get
Runs byted-supabase-cli to create and manage workspaces, databases, Auth, Storage, and Edge Functions with real-time results.
By the numbers
- Single binary byted-supabase-cli powers all capabilities
- Ships a multi-point Supabase security checklist
Files
火山引擎 AI 原生 BaaS 平台 · Supabase 版
本 Skill 用于在对话中充当火山引擎「AI 原生 BaaS 平台 Supabase 版」的智能运维与开发代理。
ℹ️ 这是火山引擎自研的 AI 原生 BaaS 平台(Supabase 版),不是官方开源 Supabase。 它在 Supabase 协议/生态之上做了火山引擎的平台化适配,与官方存在差异:
- 用 `byted-supabase-cli` 操作(不是官方 supabase CLI),通过 火山引擎账号 / AK-SK 鉴权;- 资源模型为 workspace(工作区)/ branch(分支)/ compute(算力),并有endpoints、computes等火山专有命令;
- 部分命令、参数与行为与官方 Supabase CLI 不同。
>
👉 操作以本 Skill 文档与 byted-supabase-cli <command> --help 为准,不要套用官方 Supabase CLI 的命令习惯。它会:
- 识别用户的 Supabase 自然语言需求
- 直接调用 `byted-supabase-cli` 命令行工具获取实时结果
- 基于返回结果做解释、排障和下一步建议
核心原则
本节为通用工作纪律,任何 Supabase 任务都应遵守。
1. 平台演进快 —— 先查在线文档/发布记录,不要只信训练数据。 命令、参数、config 项、API 约定会随版本变化。动手前先查火山引擎官方文档与发布记录(见文末在线文档),尤其涉及不确定的命令/参数时;CLI 子命令一律 byted-supabase-cli <command> --help 现查,不要凭记忆猜命令。
2. 验证你的工作。 任何变更落地后,再跑一条只读查询确认生效。没有验证的修复=未完成。
3. 从错误中恢复,不要死循环。 同一方法连续失败 2-3 次就停下来换思路:换方法、查文档、更仔细地看报错、必要时查日志。Supabase 类问题不总是靠重试同一条命令解决。
4. 把表暴露给 Data API(REST)。 新建的表不一定自动通过 Data API 暴露。若要让 anon / authenticated 角色经 REST 访问某表,需显式 GRANT。
注意:这与 RLS 是两回事 —— Data API 暴露 + 角色授权决定表是否可达;RLS 决定表可达后哪些行可见。当用户反馈"SQL 建的表访问不到",先确认是否已授权这两个角色。对外(anon/authenticated)授权时务必同时启用 RLS。
5. ⛔ 高危红线:`projects stop`(暂停 workspace)会让客户业务彻底不可用。 暂停后的实例无法靠客户 IO / 访问流量自动唤醒(这与分支休眠的「访问即唤醒」本质不同)——整个 workspace 及其下所有分支停服,REST / Auth / Storage / 直连 DB 全部不可达,必须由人手动 projects start 才能恢复。因此执行 projects stop 前必须:
1. 向用户明确复述影响并取得二次确认——「这会停掉整个 workspace <ref>,期间客户业务完全不可用、且不会自动恢复,确认吗?」2. 核对目标 `ref` 无误,绝不停错 workspace;
3. 绝不把它当作「重启 / 休眠 / 省资源」的手段——重启用 branches restart,休眠是平台自动行为,二者都不会导致业务停服。>
(projects delete 同属不可逆高危操作,同样需先确认。)6. 暴露 schema 内的表必须启用 RLS。 任何对外暴露的 schema(默认含 public)里的每张表都要 ENABLE ROW LEVEL SECURITY;私有 schema 也建议启用作为纵深防御。启用后,按真实访问模型写策略,不要无脑给每张表套同一个 `auth.uid()` 模式。
7. 安全 checklist。 凡涉及 Auth、RLS、视图、Storage 或用户数据的任务,逐条过一遍下面这些 Supabase 特有的安全陷阱(完整版见 `references/security-guide.md`):
- 绝不用
user_metadata/raw_user_meta_data做鉴权判断 —— 它用户可改,放进 RLS/授权逻辑即可被伪造。鉴权数据存raw_app_meta_data/app_metadata。 - `service_role` / 密钥绝不出现在前端。前端用 publishable /
anonkey;service_role仅后端。 - 视图默认绕过 RLS —— PG15+ 用
CREATE VIEW ... WITH (security_invoker = true);更低版本则从anon/authenticated收回视图权限或放进未暴露 schema。 - `auth.role()` 已弃用 —— 改用策略上的
TO authenticated/TO anon。开启匿名登录后,匿名用户也带authenticated角色,auth.role() = 'authenticated'会静默失效。 - 只写 `TO authenticated` 是"认证而非授权"(BOLA/IDOR) —— 必须配所有权谓词:
to authenticated using ( (select auth.uid()) = user_id )。 - UPDATE 策略要同时有 `USING` 和 `WITH CHECK`,否则用户能把行的
user_id改成别人。 - UPDATE 需要 SELECT 策略 —— RLS 下 UPDATE 要先 SELECT 到行;缺 SELECT 策略时更新静默返回 0 行(无报错)。
- `SECURITY DEFINER` 函数绕过 RLS,且
public下默认对所有角色可调用。别用它来"解决"权限报错;非用不可时放进未暴露 schema + 函数体内做auth.uid()检查 + 改完跑db advisors。 - Storage upsert 需要 INSERT + SELECT + UPDATE 三个权限,只给 INSERT 会导致覆盖上传静默失败。
- 固定依赖版本并提交 lockfile(
@supabase/supabase-js、@supabase/ssr、supabase-py等)。
何时使用本 Skill
适用场景(应使用):
- 用户提到 Supabase、火山引擎 Supabase 相关的操作需求
- 需要创建、查看、暂停或恢复 Supabase 工作区(workspace / project),或重启分支算力
- 需要对 Supabase 数据库执行 SQL 查询、建表、schema 变更
- 需要管理 Supabase 分支(创建、删除、恢复、设默认)
- 需要配置 Auth(认证) 或使用 Realtime(实时)
- 需要部署或管理 Edge Functions
- 需要操作 Storage Bucket(创建、删除、查看配置、上传/下载对象)
- 需要获取 Supabase 项目的 API Key 或 连接信息
- 需要为 Supabase 数据库 生成 TypeScript 类型定义
不适用场景(无需使用):
- 仅讨论通用数据库概念,不涉及火山引擎 Supabase 实例
- 操作非 Supabase 的数据库服务(如 RDS MySQL、Redis)
- 纯前端 / 客户端代码编写,不涉及 Supabase 后端资源管理
- 用户只是询问 Supabase 文档 / 概念,无需调用 CLI
命令行工具
本 Skill 的所有能力都来自 byted-supabase-cli 这一个二进制:
npm i -g @byted-supabase/cli # 安装(一次);装后得到 byted-supabase-cli 命令
byted-supabase-cli --help # 自检 / 看帮助
byted-supabase-cli <command> --help # 任何子命令都可加 --help 看参数文档示例统一写作byted-supabase-cli;若你的环境已把它别名/软链为supabase(CLI 二进制自身在--help里也自称supabase),两种写法等价。(用--help现查命令结构、不要猜——见核心原则 1。)
鉴权与前置条件
byted-supabase-cli 用火山引擎凭据鉴权。优先使用 CLI 自带的配置 / 登录方式(写入 ~/.volcengine/config.json,一次配置长期复用):
方式 1 · CLI 配置 / 登录(推荐)
byted-supabase-cli login --region cn-beijing # 1a. 浏览器 OAuth 登录(最省事,缓存临时 STS 凭据);其他地域替换 cn-beijing
byted-supabase-cli configure set --access-key <AK> --secret-key <SK> # 1b. 或用 AK/SK 写入默认 profile,可选 [--region <region>]
byted-supabase-cli configure list # 管理 profile:列出
byted-supabase-cli configure get # 查看当前 profile
byted-supabase-cli configure profile <name> # 切换当前 profileregion 可缺省;若在login/configure set时指定了,会写进 profile,后续命令无需再带--region。
方式 2 · 环境变量(次要,适合无法持久化的 headless / CI 场景)
export VOLCENGINE_ACCESS_KEY=<AK>
export VOLCENGINE_SECRET_KEY=<SK>
export VOLCENGINE_REGION=<region> # 例如 cn-beijing / cn-shanghai
export VOLCENGINE_SESSION_TOKEN=<token> # 可选:使用临时凭据时才需要在沙箱 / VeFaaS IAM 等已自带角色凭据的环境里,可不显式设置 AK/SK,CLI 会自动取临时凭据。
地域(region):可缺省,不指定时默认 cn-beijing。需要指定到其他地域(如 cn-shanghai)时再用 --region,或在 login / configure set 时设好(写进 profile 后续无需重复)。解析优先级:--region > 环境变量 > profile > 已 link 的 region > 默认(cn-beijing)。
鉴权自检:能成功跑通 byted-supabase-cli projects list 即说明凭据 + region 可用。
目标定位(workspace / branch)
- workspace(工作区/项目):用
--workspace-id ws-...指定(其别名--project-ref与官方 Supabase CLI 兼容)。 - branch(分支):用
--branch-id br-...指定;不传则落到该 workspace 的默认分支。 - 默认目标(可选):
byted-supabase-cli link --workspace-id ws-... --region <region>把某 workspace 设为本目录的默认链接;之后数据面命令可用--linked(多数命令默认--linked=true)而不必每次重复--workspace-id。 - 输出格式:管理类命令加
-o json便于解析;db query默认即输出 JSON(agent 模式)。 - 列表分页(避免一次取太多):返回列表的命令(
projects list/branches list/projects operations)支持--limit(1-100) +--offset,优先显式分页拉取,别一次性取回过多数据;建议每页 `--limit 10`,需要更多再递增 `--offset` 翻页;尤其 `branches list` 缺省会返回全部,分支多时务必加--limit或用--search过滤。其余 list 命令暂无分页参数,详见 tool-reference。
标准使用流程
1. 先确认目标资源:workspace_id(必要时 branch_id) 2. 优先执行只读查询,确认现状(projects list、db query "select ..." 等) 3. 需要变更时再执行写操作;破坏性操作(删除/停机)在非交互环境需加 --yes,执行前向用户确认。⛔ 尤其 projects stop(暂停 workspace)属高危红线——客户业务会彻底停服且无法靠 IO 自动唤醒,务必先二次确认(见核心原则 5) 4. 变更后再次查询确认结果已生效(对应核心原则 2)
能力范围(动作 → CLI 命令)
完整命令、参数与等价 SQL 见 `references/tool-reference.md`。下表为速查。
工作区(workspace / project)
| 需求 | 命令 |
|---|---|
| 列出 workspace | projects list |
| workspace 详情 / 概览 | projects list --workspace-id ws-... --detail / projects overview |
| 创建 workspace | projects create <name>(region 走全局/env/profile)。创建成功后必须主动说明休眠超时并非阻塞询问,见下方「💤 休眠超时」 |
| 调整算力 / 休眠超时 | projects compute-settings <ref> --min-cu <n> --max-cu <n> [--suspend-timeout-seconds <秒>](底层 ModifyComputeSettings;设休眠超时用 `--service-type Supabase --min-cu 0.5`,见下方💤) |
| ⛔ 暂停 / 恢复整个 workspace(停机/开机,高危,见核心原则 5) | projects stop <ref> / projects start <ref> |
| 删除 workspace | projects delete <ref> --yes |
| 获取 API Keys | projects api-keys --workspace-id ws-... |
| 获取访问地址 / 连接串 | endpoints list --workspace-id ws-... / db connection-string --workspace-id ws-... |
💤 休眠超时(SuspendTimeoutSeconds)—— 创建后务必主动说明、非阻塞询问。
- 含义:分支算力无流量自动休眠(scale-to-zero)前的空闲等待秒数。到点自动休眠,下次访问 exposeURL(或任意请求)即自动唤醒(冷启动)——这是「休眠」而非「暂停 workspace」(区别见下方辨析与核心原则 5)。
- 取值语义:-1= 显式关闭自动休眠(算力常驻 / 永不休眠);0= 未设置的平台默认(行为上等同常驻);300–604800秒(5 分钟–7 天)= 启用,空闲到点休眠。新建 workspace 默认不休眠(projects list --detail显示为0)。想关闭已开启的自动休眠就传-1(不是0)。
- 修改方式:projects compute-settings <ref> --service-type Supabase --min-cu 0.5 --max-cu 2 --suspend-timeout-seconds <秒> --yes。⚠️ 必带--yes+--min-cu/--max-cu,且 Supabase 服务 min CU 下限 0.5——这几个坑照抄即可,细节见 tool-reference 的「💤 休眠超时」。
- 创建后协议(非阻塞):projects create 成功后,主动向用户解释上面的含义并非阻塞地提示是否设置休眠超时,可直接用这段话:「关于空闲休眠:当前算力常驻、不会自动休眠—稳定但更耗资源。如果想节省算力,可配置空闲自动休眠时间(再次访问无需手动操作,Supabase 自动唤醒),推荐 1 小时。」推荐 1 小时(`--suspend-timeout-seconds 3600`);用户没回应或表示不需要就保持默认不休眠,不要因此卡住或阻塞后续流程。分支(branch)
| 需求 | 命令 |
|---|---|
| 列出 / 创建 / 删除分支 | branches list / branches create <name> / branches delete <branch-id> |
| 查看 / 设默认分支 | branches get <id> / branches get-default / branches set-default <id> |
| 重启分支算力(reboot)/ 时间点恢复 | branches restart <id> / branches restore <id> --restore-time <ts> |
⚠️ 暂停 / 重启 / 休眠 三者别混淆(最常见的误判):
- 重启分支算力("重启""reboot""重启算力")→branches restart <id>,一条命令搞定;切勿用projects stop+projects start模拟(那是整个 workspace 停机再开机)。
- 休眠 / 唤醒 = 分支算力的平台自动行为:空闲到点自动 scale-to-zero、访问即自动唤醒(无需手动命令);阈值可配,详见上方💤 休眠超时。想手动重置算力用 branches restart,别用 `projects stop` 代替。- ⛔ 暂停 / 恢复 = workspace 级停机/开机(projects stop/projects start),影响其下所有分支。高危红线:停后客户业务彻底不可用且无法靠 IO 自动唤醒,必须人工projects start——执行projects stop前务必复述影响并二次确认(详见核心原则 5)。
>
一句话记忆:休眠/重启 → 客户访问能自愈或单命令恢复(低危);暂停 → 客户访问救不回来,只能人工 start(高危,先确认)。
数据库(database)
| 需求 | 命令 |
|---|---|
| 执行 SQL(行内 / 文件) | db query "<sql>" --workspace-id ws-... / db query -f file.sql --workspace-id ws-... |
| 列出表 / 扩展 / 已应用迁移 | 用 db query 跑对应 SQL(见 tool-reference) |
| 应用 schema 变更 | db query -f file.sql(即时)/ 声明式见 db schema declarative |
| 生成类型 | gen types --lang typescript --workspace-id ws-... -s public |
| 安全/性能巡检 | db advisors --workspace-id ws-... / inspect db <subcmd> |
⚠️ 本 fork 无独立 migration 子命令;schema 变更与提交流程见下方「数据库 schema 变更与提交」。Auth(认证)
平台支持 Supabase Authentication。Auth 多在应用侧通过 SDK 使用(注册/登录/会话/JWT),管理与配置见火山Authentication 文档;SDK 用法见 `references/app-integration-guide.md`。
🔐 鉴权数据用app_metadata(不可被用户改),绝不用user_metadata做授权判断(见安全 checklist)。
Realtime(实时)
平台支持 Supabase Realtime(Postgres Changes / Broadcast / Presence),在应用侧通过 SDK 订阅。配置见火山Realtime 文档;SDK 用法见 `references/app-integration-guide.md`。
Edge Functions(边缘函数)
| 需求 | 命令 |
|---|---|
| 列出函数 | functions list --workspace-id ws-... |
| 新建函数(本地脚手架) | functions new <name> → 编辑 supabase/functions/<name>/index.ts |
| 部署函数 | functions deploy <name> --workspace-id ws-...(公开访问加 --no-verify-jwt) |
| 下载 / 删除函数 | functions download <name> / functions delete <name> |
部署从本地函数目录进行(functions new→ 编辑 →functions deploy),不再支持单文件/内联代码。编写规范见 `references/edge-function-dev-guide.md`。
Storage(对象存储)
| 需求 | 命令 |
|---|---|
| 列出 / 创建 / 查看 / 删除 bucket | storage buckets list / storage buckets create <id> [--public] / storage buckets get <id> / storage buckets delete <id> |
| 对象列出 / 上传下载 | storage ls <path> / storage cp <src> <dst> |
| 对象移动 / 删除 | storage mv <src> <dst> / storage rm <file...> |
对象路径形如ss:///<bucket>/<path>,目录操作加-r递归。
数据库 schema 变更与提交
做 schema 变更用 `db query`(行内或 `-f file.sql`)直接对库执行,可自由迭代。
- 本 fork 没有独立 `migration` 子命令,不要套用官方
supabase migration new/apply_migration的习惯。 - 变更前后都用只读查询确认(核心原则 2)。
- 改完跑巡检 →
byted-supabase-cli db advisors --workspace-id ws-...,修掉安全/性能告警。 - 若变更涉及视图、函数、触发器或 Storage,回到[安全 checklist](#核心原则)逐条复核。
- 需要版本化 / 可追溯的 schema 演进时,改用声明式管理:
byted-supabase-cli db schema declarative --help。
注意事项
- 本 fork 无全局只读开关;写操作的安全边界依赖 RLS(anon vs service_role)+ 操作纪律。
db query默认连接--linked项目;未 link 时务必显式传--workspace-id(必要时--branch-id)。
在线文档
对应核心原则 1:不确定时先查这些。
- 新功能发布记录(changelog):https://www.volcengine.com/docs/87275/2105759?lang=zh
- API 发布历史 / API 列表:https://www.volcengine.com/docs/87275/2105870?lang=zh / https://www.volcengine.com/docs/87275/2105871?lang=zh
- SDK 发布历史 / 概述:https://www.volcengine.com/docs/87275/2248647?lang=zh / https://www.volcengine.com/docs/87275/2248648?lang=zh
- 使用 Database:https://www.volcengine.com/docs/87275/2385100?lang=zh
- 使用 Authentication:https://www.volcengine.com/docs/87275/2277072?lang=zh
- 使用 Realtime:https://www.volcengine.com/docs/87275/2277058?lang=zh
- 使用 Edge Function:https://www.volcengine.com/docs/87275/2288709?lang=zh
- 使用 Storage:https://www.volcengine.com/docs/87275/2277057?lang=zh
- Go SDK:https://www.volcengine.com/docs/87275/2248650?lang=zh / Python SDK:https://www.volcengine.com/docs/87275/2375511?lang=zh
参考资料
| 文档 | 用途 |
|---|---|
| `references/tool-reference.md` | 命令速查:动作 → CLI + 等价 SQL |
| `references/workflows.md` | 常见操作流程(巡检 / 变更 / 发布) |
| `references/sql-playbook.md` | 常用 SQL 示例(CRUD / JOIN / pgvector / RPC) |
| `references/app-integration-guide.md` | 接入 TS/Python 应用:SDK 初始化 + CRUD + Auth + Realtime |
| `references/schema-guide.md` | 表结构设计与变更规范 |
| `references/rls-guide.md` | 行级安全(RLS)策略配置 |
| `references/security-guide.md` | Supabase 特有安全陷阱 checklist |
| `references/pg-best-practices/index.md` | Postgres 性能与最佳实践(索引 / 连接 / 锁 / 监控等) |
| `references/edge-function-dev-guide.md` | Edge Function 编写与部署 |
📚 Postgres 最佳实践细分(均由上方 `index.md` 索引,可按需直达):`query.md` · `conn.md` · `security.md` · `schema.md` · `lock.md` · `data.md` · `monitor.md` · `advanced.md`
💡 典型工作流:先用 CLI 创建 workspace / 建表 / 配置 RLS,再参考应用集成文档在业务代码中集成 Supabase SDK。
</content> </invoke>
{
"slug": "byted-supabase",
"version": "2.2.7"
}
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
interface:
display_name: "Volcengine Supabase (AI-native BaaS)"
short_description: "Manage Volcengine's AI-native BaaS platform (Supabase edition; differs from official Supabase) — workspaces, branches, databases, Edge Functions, and Storage"
default_prompt: "Use $byted-supabase to complete this Supabase task on Volcengine."
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
应用集成指南
本指南介绍如何将 Supabase 集成到 TypeScript 或 Python 应用中,包括获取连接信息、初始化 SDK 客户端、执行 CRUD 操作,以及 Auth(认证)与 Realtime(实时)的 SDK 用法。
---
目录
- 获取连接信息
- 环境变量配置
- TypeScript 应用接入
- Python 应用接入
- RPC 调用(存储过程)
- Storage 文件操作
- Auth(认证)
- Realtime(实时)
1. 获取连接信息
使用本 Skill 获取 Supabase 实例的连接 URL 和密钥:
# 获取 Supabase API 访问地址
byted-supabase-cli endpoints list --workspace-id ws-xxxx -o json
# 获取 Postgres 连接串
byted-supabase-cli db connection-string --workspace-id ws-xxxx
# 获取 API Keys(anon / service_role 等)
byted-supabase-cli projects api-keys --workspace-id ws-xxxx -o json⚠️ service_role key 拥有完整权限,仅后端使用;非必要不要回显完整密钥。你将获得以下信息,用于应用配置:
| 信息 | 用途 |
|---|---|
| Supabase URL | API 端点地址 |
| anon key | 客户端公开访问密钥(受 RLS 限制) |
| service_role key | 服务端管理密钥(绕过 RLS,仅限后端使用) |
⚠️ 安全提醒:service_role key 拥有完整权限,永远不要暴露给客户端或前端代码。---
2. 环境变量配置
建议在 .env 文件中配置:
SUPABASE_URL=https://your-project-url.supabase.co
SUPABASE_ANON_KEY=eyJ...your-anon-key
SUPABASE_SERVICE_ROLE_KEY=eyJ...your-service-role-key # 仅后端使用---
3. TypeScript 应用接入
安装依赖
npm install @supabase/supabase-js初始化客户端
import { createClient, SupabaseClient } from '@supabase/supabase-js';
// 服务端客户端(用于 API 路由、后端服务)
function getSupabaseClient(token?: string): SupabaseClient {
const url = process.env.SUPABASE_URL!;
const key = process.env.SUPABASE_ANON_KEY!;
if (token) {
// 带用户认证的客户端(RLS 按用户过滤)
return createClient(url, key, {
global: {
headers: { Authorization: `Bearer ${token}` },
},
db: { timeout: 60000 },
auth: { autoRefreshToken: false, persistSession: false },
});
}
// 匿名客户端(RLS 按 anon 角色过滤)
return createClient(url, key, {
db: { timeout: 60000 },
auth: { autoRefreshToken: false, persistSession: false },
});
}CRUD 操作示例
查询数据
const client = getSupabaseClient();
// 基础查询
const { data, error } = await client.from('posts').select('*').limit(10);
// 带过滤条件
const { data: activePosts } = await client
.from('posts')
.select('*')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(20);
// 选择特定列
const { data: titles } = await client.from('posts').select('id, title, created_at');
// 模糊搜索
const { data: results } = await client.from('posts').select('*').ilike('title', '%keyword%');
// 范围查询
const { data: recent } = await client
.from('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lte('created_at', '2024-12-31');
// 分页查询
const { data: page } = await client
.from('posts')
.select('*')
.range(0, 9); // 第 1-10 条
// 关联查询(需要外键关系)
const { data: postsWithAuthor } = await client
.from('posts')
.select('*, users(name, email)');
// 统计数量
const { count } = await client
.from('posts')
.select('*', { count: 'exact', head: true });插入数据
// 单条插入
const { data, error } = await client
.from('posts')
.insert({ title: 'Hello World', content: 'My first post' })
.select(); // 返回插入的数据
// 批量插入
const { data: batch } = await client
.from('posts')
.insert([
{ title: 'Post 1', content: 'Content 1' },
{ title: 'Post 2', content: 'Content 2' },
])
.select();更新数据
// 按条件更新
const { data, error } = await client
.from('posts')
.update({ published: true, updated_at: new Date().toISOString() })
.eq('id', 1)
.select();
// UPSERT(存在则更新,不存在则插入)
const { data: upserted } = await client
.from('settings')
.upsert({ key: 'theme', value: 'dark' }, { onConflict: 'key' })
.select();删除数据
// 按条件删除
const { error } = await client.from('posts').delete().eq('id', 1);
// 多条件删除
const { error: batchError } = await client
.from('posts')
.delete()
.eq('published', false)
.lt('created_at', '2023-01-01');错误处理
const { data, error } = await client.from('posts').select('*');
if (error) {
console.error(`Error code: ${error.code}, message: ${error.message}`);
// 常见错误码:
// PGRST116 - 查询结果为空(使用 .single() 时)
// PGRST200 - 关联查询缺少外键关系
// 42501 - RLS 策略拒绝访问
// 23505 - 唯一约束冲突
return;
}
console.log(data);---
4. Python 应用接入
安装依赖
pip install supabase httpx初始化客户端
import os
from typing import Optional
import httpx
from supabase import create_client, Client, ClientOptions
def get_supabase_client(token: Optional[str] = None) -> Client:
url = os.environ["SUPABASE_URL"]
key = os.environ["SUPABASE_ANON_KEY"]
http_client = httpx.Client(
timeout=httpx.Timeout(connect=20.0, read=60.0, write=60.0, pool=10.0),
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
follow_redirects=True,
)
if token:
# 带用户认证的客户端
options = ClientOptions(
httpx_client=http_client,
headers={"Authorization": f"Bearer {token}"},
auto_refresh_token=False,
)
else:
# 匿名客户端
options = ClientOptions(
httpx_client=http_client,
auto_refresh_token=False,
)
return create_client(url, key, options=options)CRUD 操作示例
查询数据
client = get_supabase_client()
# 基础查询
response = client.table('posts').select('*').limit(10).execute()
print(response.data)
# 带过滤条件
response = (client.table('posts')
.select('*')
.eq('published', True)
.order('created_at', desc=True)
.limit(20)
.execute())
# 选择特定列
response = client.table('posts').select('id, title, created_at').execute()
# 模糊搜索
response = client.table('posts').select('*').ilike('title', '%keyword%').execute()
# 范围查询
response = (client.table('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lte('created_at', '2024-12-31')
.execute())
# 分页查询
response = client.table('posts').select('*').range(0, 9).execute()
# 关联查询(需要外键)
response = client.table('posts').select('*, users(name, email)').execute()
# 统计数量
response = client.table('posts').select('*', count='exact').execute()
print(response.count)插入数据
# 单条插入
response = client.table('posts').insert({
'title': 'Hello World',
'content': 'My first post'
}).execute()
print(response.data)
# 批量插入
response = client.table('posts').insert([
{'title': 'Post 1', 'content': 'Content 1'},
{'title': 'Post 2', 'content': 'Content 2'},
]).execute()更新数据
# 按条件更新
response = (client.table('posts')
.update({'published': True})
.eq('id', 1)
.execute())
# UPSERT
response = (client.table('settings')
.upsert({'key': 'theme', 'value': 'dark'})
.execute())删除数据
# 按条件删除
response = client.table('posts').delete().eq('id', 1).execute()错误处理
try:
response = client.table('posts').select('*').execute()
print(response.data)
except Exception as e:
print(f"Error: {e}")---
5. RPC 调用(存储过程)
如果需要复杂的数据库操作,可以先通过 db query 创建存储过程,然后通过 SDK 调用。
创建存储过程
把下面的 SQL 存为 search_posts.sql 后应用(含 $$ 的函数体用文件方式比行内更稳妥):
CREATE OR REPLACE FUNCTION search_posts(keyword text)
RETURNS SETOF posts AS $$
SELECT * FROM posts
WHERE title ILIKE '%' || keyword || '%'
OR content ILIKE '%' || keyword || '%'
ORDER BY created_at DESC;
$$ LANGUAGE sql STABLE;byted-supabase-cli db query -f ./search_posts.sql --workspace-id ws-xxxxSDK 调用
// TypeScript
const { data, error } = await client.rpc('search_posts', { keyword: 'supabase' });# Python
response = client.rpc('search_posts', {'keyword': 'supabase'}).execute()---
6. Storage 文件操作
通过本 Skill 创建 Storage Bucket 后,可以在应用中使用 SDK 操作文件。
创建 Bucket(通过 CLI)
# 创建公开 bucket
byted-supabase-cli storage buckets create avatars --public --workspace-id ws-xxxx
# 创建私有 bucket
byted-supabase-cli storage buckets create documents --workspace-id ws-xxxxSDK 文件操作(TypeScript)
const client = getSupabaseClient();
// 上传文件
const { data, error } = await client.storage
.from('avatars')
.upload('user1/avatar.png', fileBuffer, {
contentType: 'image/png',
cacheControl: '3600',
});
// 获取公开 URL
const { data: urlData } = client.storage
.from('avatars')
.getPublicUrl('user1/avatar.png');
// 获取签名 URL(临时访问私有文件)
const { data: signedUrl } = await client.storage
.from('documents')
.createSignedUrl('report.pdf', 3600); // 1 小时有效
// 下载文件
const { data: fileData } = await client.storage
.from('documents')
.download('report.pdf');
// 删除文件
const { data: deleteData } = await client.storage
.from('avatars')
.remove(['user1/old-avatar.png']);
// 列出文件
const { data: files } = await client.storage
.from('avatars')
.list('user1/', { limit: 100, offset: 0 });SDK 文件操作(Python)
client = get_supabase_client()
# 上传文件
with open('avatar.png', 'rb') as f:
response = client.storage.from_('avatars').upload('user1/avatar.png', f.read())
# 获取公开 URL
url = client.storage.from_('avatars').get_public_url('user1/avatar.png')
# 获取签名 URL
signed_url = client.storage.from_('documents').create_signed_url('report.pdf', 3600)
# 下载文件
file_data = client.storage.from_('documents').download('report.pdf')
# 删除文件
response = client.storage.from_('avatars').remove(['user1/old-avatar.png'])
# 列出文件
files = client.storage.from_('avatars').list('user1/')---
7. Auth(认证)
平台支持 Supabase Authentication。配置(邮箱/OAuth/匿名登录等)见火山 Authentication 文档。下面是 SDK 侧常用流程。
🔐 安全要点(详见 `security-guide.md`):
- 鉴权/角色数据存app_metadata,绝不用user_metadata做授权判断(用户可改)。
- 服务端验证用户身份用getUser()(会校验 token),不要只信getSession()返回的本地会话。
- 删除用户不会让已签发的 token 失效;敏感场景把 JWT 过期设短并主动 signOut。TypeScript
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
// 凭据应来自用户输入或安全来源(环境变量 / 密钥管理),切勿硬编码
const email = userInput.email;
const password = userInput.password;
// 注册
const { data, error } = await supabase.auth.signUp({ email, password });
// 邮箱密码登录
const { data: session, error: signInErr } = await supabase.auth.signInWithPassword({ email, password });
// 获取当前用户(服务端:会向 Auth 服务校验 token,可信)
const { data: { user } } = await supabase.auth.getUser();
// 获取本地会话(客户端用;不要在服务端用它做鉴权判断)
const { data: { session: local } } = await supabase.auth.getSession();
// 登出
await supabase.auth.signOut();Python
client = get_supabase_client()
# 凭据应来自用户输入或安全来源(环境变量 / 密钥管理),切勿硬编码
email = user_input["email"]
password = user_input["password"]
# 注册
res = client.auth.sign_up({"email": email, "password": password})
# 登录
res = client.auth.sign_in_with_password({"email": email, "password": password})
# 校验并获取当前用户(可信)
user = client.auth.get_user()
# 登出
client.auth.sign_out()---
8. Realtime(实时)
平台支持 Supabase Realtime:监听数据库变更(Postgres Changes)、广播(Broadcast)、在线状态(Presence)。配置见火山 Realtime 文档。
⚠️ Postgres Changes 同样受 RLS 约束——客户端只能收到 RLS 允许它看到的行的变更。要让某表的变更可被订阅,需在平台开启该表的 Realtime 并配好 RLS。
TypeScript
// 1. 监听数据库变更(INSERT / UPDATE / DELETE)
const channel = supabase
.channel('room-1')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'messages' },
(payload) => {
console.log('变更:', payload.eventType, payload.new);
}
)
.subscribe();
// 2. 广播(客户端之间直接收发消息,不落库)
const bc = supabase.channel('cursor');
bc.on('broadcast', { event: 'pos' }, ({ payload }) => console.log(payload))
.subscribe();
await bc.send({ type: 'broadcast', event: 'pos', payload: { x: 1, y: 2 } });
// 3. Presence(在线状态)
const presence = supabase.channel('online');
presence
.on('presence', { event: 'sync' }, () => {
console.log('在线用户:', presence.presenceState());
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await presence.track({ user_id: 'user-001', online_at: new Date().toISOString() });
}
});
// 取消订阅
await supabase.removeChannel(channel);Python SDK 的 Realtime 用法以火山 SDK 文档 为准(接口随版本演进,先查文档再用)。
Edge Function 开发指南
本指南介绍 Edge Function 的编写规范和常见模式。本 fork 的部署从本地函数目录进行:byted-supabase-cli functions new <name> 生成 supabase/functions/<name>/index.ts 脚手架 → 编辑 → byted-supabase-cli functions deploy <name>。
---
目录
- 基础结构
- CORS 配置
- 请求处理
- 带数据库访问
- 常见模式
- 部署与管理
- 注意事项
基础结构
每个 Edge Function 是一个 Deno 函数,使用 Deno.serve() 处理 HTTP 请求:
Deno.serve(async (req: Request) => {
return new Response(JSON.stringify({ message: 'Hello World' }), {
headers: { 'Content-Type': 'application/json' },
});
});部署:
byted-supabase-cli functions new hello # 生成 supabase/functions/hello/index.ts
# 把上面的代码写入该文件后部署
byted-supabase-cli functions deploy hello --workspace-id ws-xxxx---
CORS 配置
大多数前端调用都需要 CORS 支持:
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type, x-client-info, apikey',
};
Deno.serve(async (req: Request) => {
// 处理 CORS 预检请求
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const data = { message: 'Hello World' };
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});---
请求处理
读取请求体
Deno.serve(async (req: Request) => {
// JSON 请求体
const body = await req.json();
const { name, email } = body;
// URL 参数
const url = new URL(req.url);
const page = url.searchParams.get('page') || '1';
// 请求头
const authHeader = req.headers.get('Authorization');
return new Response(JSON.stringify({ name, email, page }), {
headers: { 'Content-Type': 'application/json' },
});
});路由分发
Deno.serve(async (req: Request) => {
const url = new URL(req.url);
const path = url.pathname;
if (req.method === 'GET' && path === '/') {
return handleList(req);
}
if (req.method === 'POST' && path === '/') {
return handleCreate(req);
}
if (req.method === 'PUT') {
return handleUpdate(req);
}
if (req.method === 'DELETE') {
return handleDelete(req);
}
return new Response('Not Found', { status: 404 });
});---
带数据库访问
Edge Function 可以通过 Supabase SDK 或直接使用环境变量中的连接信息访问数据库:
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Authorization, Content-Type, x-client-info, apikey',
};
Deno.serve(async (req: Request) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
// 使用环境变量创建 Supabase 客户端
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const supabase = createClient(supabaseUrl, supabaseKey);
// 如果需要以用户身份访问(尊重 RLS)
const authHeader = req.headers.get('Authorization');
if (authHeader) {
const userToken = authHeader.replace('Bearer ', '');
const userClient = createClient(supabaseUrl, Deno.env.get('SUPABASE_ANON_KEY')!, {
global: { headers: { Authorization: `Bearer ${userToken}` } },
});
// 使用 userClient 进行受 RLS 保护的操作
}
// 示例:查询数据
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.limit(10);
if (error) throw error;
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});---
常见模式
CRUD API
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Authorization, Content-Type, x-client-info, apikey',
};
Deno.serve(async (req: Request) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
try {
let result;
switch (req.method) {
case 'GET': {
const url = new URL(req.url);
const id = url.searchParams.get('id');
if (id) {
result = await supabase.from('items').select('*').eq('id', id).single();
} else {
result = await supabase.from('items').select('*').order('created_at', { ascending: false });
}
break;
}
case 'POST': {
const body = await req.json();
result = await supabase.from('items').insert(body).select();
break;
}
case 'PUT': {
const body = await req.json();
const { id, ...updates } = body;
result = await supabase.from('items').update(updates).eq('id', id).select();
break;
}
case 'DELETE': {
const url = new URL(req.url);
const id = url.searchParams.get('id');
result = await supabase.from('items').delete().eq('id', id!);
break;
}
}
if (result?.error) throw result.error;
return new Response(JSON.stringify(result?.data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});Webhook 处理器
Deno.serve(async (req: Request) => {
if (req.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
try {
const payload = await req.json();
// 验证 Webhook 签名(根据实际来源调整)
const signature = req.headers.get('x-webhook-signature');
if (!signature) {
return new Response('Unauthorized', { status: 401 });
}
// 处理事件
console.log('Received webhook:', payload.type);
switch (payload.type) {
case 'order.created':
// 处理订单创建事件
break;
case 'payment.completed':
// 处理支付完成事件
break;
default:
console.log('Unhandled event type:', payload.type);
}
return new Response(JSON.stringify({ received: true }), {
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
});数据聚合 / 报表
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
Deno.serve(async (req: Request) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
try {
// 使用 RPC 调用数据库函数进行聚合
const { data: stats, error } = await supabase.rpc('get_dashboard_stats');
if (error) throw error;
return new Response(JSON.stringify(stats), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300', // 缓存 5 分钟
},
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
});---
部署与管理
# 1. 新建函数脚手架(本地 supabase/functions/my-api/index.ts)
byted-supabase-cli functions new my-api
# 2. 编辑该文件写入逻辑,然后部署
byted-supabase-cli functions deploy my-api --workspace-id ws-xxxx
# 部署时禁用 JWT 验证(公开 API,如 Webhook)
byted-supabase-cli functions deploy public-api --workspace-id ws-xxxx --no-verify-jwt
# 指定运行时(默认 auto;可选 deno / native-node20/v1 / python3.9|3.10|3.12)
byted-supabase-cli functions deploy my-api --workspace-id ws-xxxx --runtime deno
# 查看已部署函数
byted-supabase-cli functions list --workspace-id ws-xxxx -o json
# 拉取线上函数源码到本地
byted-supabase-cli functions download my-api --workspace-id ws-xxxx
# 删除函数
byted-supabase-cli functions delete my-api --workspace-id ws-xxxx---
注意事项
- JWT 验证:默认启用。如果函数需要公开访问(如 Webhook),使用
--no-verify-jwt - 函数命名:只能使用小写字母、数字和连字符(如
my-api、process-order) - 超时:Edge Function 有执行时间限制,避免长时间阻塞操作
- 日志:使用
console.log()记录日志,可在平台控制台查看 - 依赖导入:使用
https://esm.sh/导入 npm 包,或使用https://deno.land/导入 Deno 标准库
高级特性(advanced)
全文检索、JSONB 优化等 Postgres 高级特性。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;改完跑byted-supabase-cli db advisors巡检。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 全文检索请用 tsvector,别再用 LIKE 模糊匹配
- 给 JSONB 列建索引,避免全表扫描
全文检索请用 tsvector,别再用 LIKE 模糊匹配
影响(Impact): MEDIUM — 比 LIKE 快约 100 倍,还自带相关度排序
LIKE '%关键词%' 这种前后都带通配符的写法,索引根本用不上,每次查询都得全表扫一遍。改用基于 tsvector 的全文检索,性能能甩开几个数量级,而且天生支持按相关度排序。
反面写法(LIKE 模糊匹配):
-- 用不上索引,全表扫描
select * from articles where content like '%postgresql%';
-- 套个 lower() 做大小写不敏感,反而更慢
select * from articles where lower(content) like '%postgresql%';推荐写法(用 tsvector 做全文检索):
-- 加一个 tsvector 生成列,并建索引
alter table articles add column search_vector tsvector
generated always as (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(content,''))) stored;
create index articles_search_idx on articles using gin (search_vector);
-- 高速全文检索
select * from articles
where search_vector @@ to_tsquery('english', 'postgresql & performance');
-- 带相关度排序
select *, ts_rank(search_vector, query) as rank
from articles, to_tsquery('english', 'postgresql') query
where search_vector @@ query
order by rank desc;多关键词的几种组合方式(⚠️ to_tsquery 的 config 必须与 tsvector 列建列时一致,本例都用 'english';若漏掉 config,会落到实例的 default_text_search_config(火山引擎 Supabase 版默认是 simple,不做词干化),与 'english' 向量对不上,可能一行都查不到):
-- AND:两个词都要命中
to_tsquery('english', 'postgresql & performance')
-- OR:命中任意一个即可
to_tsquery('english', 'postgresql | mysql')
-- 前缀匹配
to_tsquery('english', 'post:*')Reference: Full Text Search
给 JSONB 列建索引,避免全表扫描
影响(Impact): MEDIUM — 索引到位后 JSONB 查询提速 10~100 倍
JSONB 列不建索引,每条查询都得把整张表扫一遍。这类按包含关系(containment)过滤的查询,交给 GIN 倒排索引来扛最合适。
反面写法(JSONB 列没建索引):
create table products (
id bigint primary key,
attributes jsonb
);
-- 每条查询都全表扫描
select * from products where attributes @> '{"color": "red"}';
select * from products where attributes->>'brand' = 'Nike';推荐写法(给 JSONB 建 GIN 索引):
-- GIN 索引覆盖包含类操作符(@>, ?, ?&, ?|)
create index products_attrs_gin on products using gin (attributes);
-- 包含关系查询现在能走索引了
select * from products where attributes @> '{"color": "red"}';
-- 只查固定某个 key 时,改用表达式索引
create index products_brand_idx on products ((attributes->>'brand'));
select * from products where attributes->>'brand' = 'Nike';按需挑选合适的操作符类(operator class):
-- jsonb_ops(默认):支持全部操作符,但索引体积偏大
create index idx1 on products using gin (attributes);
-- jsonb_path_ops:只支持 @> 操作符,但索引体积通常更小(幅度随数据而定,常见约 1.5~3 倍)
create index idx2 on products using gin (attributes jsonb_path_ops);Reference: JSONB Indexes
连接管理(conn)
连接池、连接数上限与 serverless 策略——高并发或 serverless 部署的关键。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;改完跑byted-supabase-cli db advisors巡检。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 所有应用都要走连接池
- 把连接数上限调到合适的值
- 给空闲连接配上超时回收
- 连接池下正确使用预处理语句
所有应用都要走连接池
影响(Impact): CRITICAL — 并发承载能力提升 10~100 倍。
Postgres 的连接很贵,每条要吃掉 1-3MB 内存。不走连接池,流量一上来连接很快就被打满。
反例(每个请求都新建连接):
-- 每个请求都开一条新连接
-- 应用侧代码:每次请求都 db.connect()
-- 后果:500 个并发用户 = 500 条连接 = 数据库被打挂
-- 看看当前连接数
select count(*) from pg_stat_activity; -- 487 条连接!正例(接入连接池):
-- 在应用和数据库之间架一层 pooler(如 PgBouncer)
-- 应用只连 pooler,由 pooler 用一小撮连接复用着打到 Postgres
-- pool_size 按这个公式估:(CPU 核数 * 2) + spindle_count
-- 4 核为例:pool_size = 10
-- 效果:500 个并发用户共用 10 条真实连接
select count(*) from pg_stat_activity; -- 10 条连接两种池化模式怎么选:
- 事务模式(transaction mode):每跑完一个事务就把连接还回池子,绝大多数应用都该用这个。
- 会话模式(session mode):整个会话期间独占一条连接,只有用到预处理语句、临时表这类场景才需要。
Reference: Connection Pooling
把连接数上限调到合适的值
影响(Impact): CRITICAL — 避免数据库被拖垮、内存被耗尽。
连接开太多,内存撑爆、性能也跟着下滑。上限得照着机器的实际资源来定,别拍脑袋。
反例(不限或盲目调高):
-- max_connections 默认 100,但常被人不假思索地往上拉
show max_connections; -- 500(对 4GB 内存来说高得离谱)
-- 每条连接吃 1-3MB 内存
-- 500 连接 * 2MB = 光连接就占掉 1GB!
-- 负载一上来就 OOM正例(按资源算出来):
-- 公式:max_connections = (内存 MB 数 / 每连接 5MB) - 预留
-- 4GB 内存:(4096 / 5) - 10 ≈ 理论上限 800
-- 但从查询性能角度看,100-200 才是更实在的取值
-- 4GB 内存的推荐配置
alter system set max_connections = 100;
-- work_mem 也得配套调
-- work_mem * max_connections 不要超过内存的 25%
alter system set work_mem = '8MB'; -- 8MB * 100 = 上限 800MB⚠️ 火山引擎 Supabase 版注意:连接角色postgres非 superuser,上面两条alter system set ...会直接报42501 permission denied,改不动实例级参数。这类参数请走平台控制台调整;只想临时改本会话用set work_mem = '8MB'即可。另外本平台max_connections实测可能已是数百(实测某实例为 901),并非 vanilla PG 的默认 100——动手前先show max_connections看实值。
盯紧连接的使用情况:
select count(*), state from pg_stat_activity group by state;Reference: Database Connections
给空闲连接配上超时回收
影响(Impact): HIGH — 从空闲客户端手里回收 30-50% 的连接槽位。
连接挂着不干活就是在白白占资源。配好空闲超时,让它们到点自动被踢掉、把槽位让出来。
反例(连接一直挂着不回收):
-- 没配任何超时
show idle_in_transaction_session_timeout; -- 0(已禁用)
-- 连接永远不释放,哪怕一直闲着
select pid, state, state_change, query
from pg_stat_activity
where state = 'idle in transaction';
-- 能看到一堆事务空闲好几个小时,锁还死死攥在手里正例(空闲连接自动清理):
-- 事务里空闲超过 30 秒就掐掉连接
alter system set idle_in_transaction_session_timeout = '30s';
-- 彻底空闲超过 10 分钟就掐掉连接
alter system set idle_session_timeout = '10min';
-- 重新加载配置
select pg_reload_conf();⚠️ 火山引擎 Supabase 版注意:上面的alter system set ...和select pg_reload_conf()对postgres角色都会报42501 permission denied,在本平台跑不通。空闲超时这类实例级配置请走平台控制台;本平台idle_in_transaction_session_timeout实测可能已被预设为非 0(实测某实例为 5min),先show看实值。会话级set local idle_in_transaction_session_timeout = '30s'可用。
如果走的是连接池,超时就在 pooler 这一层配:
# pgbouncer.ini
server_idle_timeout = 60
client_idle_timeout = 300Reference: Connection Timeouts
连接池下正确使用预处理语句
影响(Impact): HIGH — 规避池化环境里的预处理语句冲突。
预处理语句是绑死在某一条具体连接上的。可事务模式下连接是大家轮着用的,于是就撞车了。
反例(事务模式池化 + 具名预处理语句):
-- 具名预处理语句
prepare get_user as select * from users where id = $1;
-- 事务模式下,下一个请求很可能落到另一条连接上
execute get_user(123);
-- ERROR: prepared statement "get_user" does not exist正例(改用匿名语句,或切到会话模式):
-- 方案一:用匿名预处理语句(大多数 ORM 默认就这么干)
-- 查询的预备和执行在同一条协议消息里完成
-- 方案二:事务模式下用完即 deallocate
prepare get_user as select * from users where id = $1;
execute get_user(123);
deallocate get_user;
-- 方案三:改用会话模式池化(端口 5432,而非 6543)
-- 连接整个会话期间不释放,预处理语句也就一直在顺手检查一下驱动的配置:
-- 不少驱动默认就开了预处理语句
-- Node.js pg:{ prepare: false } 关掉
-- JDBC:prepareThreshold=0 关掉Reference: Prepared Statements with Pooling
数据访问模式(data)
消除 N+1 查询、批量操作、游标分页与高效取数。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;改完跑byted-supabase-cli db advisors巡检。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 用批量加载干掉 N+1 查询
- 大批量写入用批量 INSERT
- 用游标分页替代 OFFSET
- 插入或更新统一用 UPSERT
用批量加载干掉 N+1 查询
影响(Impact): MEDIUM-HIGH — 数据库往返次数砍到原来的 1/10~1/100。
所谓 N+1 查询,就是在循环里逐条发请求:先查出一批数据,再针对每一条单独发一次查询。正确的做法是合并成一条,靠数组参数或 JOIN 一次取全。
错误做法(N+1 查询):
-- 第一条查询:取出所有用户
select id from users where active = true; -- 返回 100 个 ID
-- 接着 N 条查询,每个用户一条
select * from orders where user_id = 1;
select * from orders where user_id = 2;
select * from orders where user_id = 3;
-- ……还有 97 条!
-- 合计:往返数据库 101 次正确做法(单条批量查询):
-- 收齐 ID,用 ANY 一次查完
select * from orders where user_id = any(array[1, 2, 3, ...]);
-- 或者直接用 JOIN 替代循环
select u.id, u.name, o.*
from users u
left join orders o on o.user_id = u.id
where u.active = true;
-- 合计:往返 1 次落到应用代码里:
-- 不要在应用层这样循环:
-- for user in users: db.query("SELECT * FROM orders WHERE user_id = $1", user.id)
-- 改成传数组参数:
select * from orders where user_id = any($1::bigint[]);
-- 应用层传入:[1, 2, 3, 4, 5, ...]Reference: N+1 Query Problem
大批量写入用批量 INSERT
影响(Impact): MEDIUM — 批量插入快 10~50 倍。
单条单条地 INSERT 开销很高。要么把多行塞进一条语句,要么直接上 COPY。
错误做法(逐条插入):
-- 每条 insert 都是一次独立的事务和一次往返
insert into events (user_id, action) values (1, 'click');
insert into events (user_id, action) values (1, 'view');
insert into events (user_id, action) values (2, 'click');
-- ……还有 1000 条这样的单条插入
-- 1000 条插入 = 1000 次往返 = 慢正确做法(批量插入):
-- 一条语句写多行
insert into events (user_id, action) values
(1, 'click'),
(1, 'view'),
(2, 'click'),
-- ……每批最多约 1000 行
(999, 'view');
-- 1000 行只需一次往返大规模导入,请用 COPY:
-- 批量加载首选 COPY,最快
copy events (user_id, action, created_at)
from '/path/to/data.csv'
with (format csv, header true);
-- 也可以在应用里从标准输入喂数据
copy events (user_id, action) from stdin with (format csv);
1,click
1,view
2,click
\.⚠️ 火山引擎 Supabase 版注意:通过byted-supabase-cli db query(REST 网关)执行 COPY 会被拦截,返回403 COPY is forbidden——COPY FROM 文件、COPY FROM stdin、COPY TO stdout在这条路径上都不可用。在本平台批量导入请改用上面的多行 INSERT;确需 COPY 时,用能直连 Postgres 的客户端(如 psql 的\copy,连接串见db connection-string)。
Reference: COPY
用游标分页替代 OFFSET
影响(Impact): MEDIUM-HIGH — 无论翻到多深,性能都稳定在 O(1)。
OFFSET 分页会把跳过的行全部扫一遍,翻得越深越慢。游标分页(keyset 分页)则是 O(1)。
错误做法(OFFSET 分页):
-- 第 1 页:扫 20 行
select * from products order by id limit 20 offset 0;
-- 第 100 页:要扫 2000 行才能跳过前 1980 行
select * from products order by id limit 20 offset 1980;
-- 第 10000 页:得扫 200,000 行!
select * from products order by id limit 20 offset 199980;正确做法(游标分页 / keyset 分页):
-- 第 1 页:取前 20 条
select * from products order by id limit 20;
-- 应用层记下 last_id = 20
-- 第 2 页:从上一页最后一个 ID 之后接着取
select * from products where id > 20 order by id limit 20;
-- 走索引,翻多深都一样快
-- 第 10000 页:和第 1 页一个速度
select * from products where id > 199980 order by id limit 20;多列排序时:
-- 游标要把所有排序列都带上
select * from products
where (created_at, id) > ('2024-01-15 10:00:00', 12345)
order by created_at, id
limit 20;Reference: Pagination
插入或更新统一用 UPSERT
影响(Impact): MEDIUM — 操作原子化,根除竞态条件。
先 SELECT 再 INSERT/UPDATE 这种分两步的写法会留下竞态隐患。用 INSERT ... ON CONFLICT 一步到位,天然原子。
错误做法(先查后插,存在竞态):
-- 竞态:两个请求同时检查
select * from settings where user_id = 123 and key = 'theme';
-- 两边都查不到记录
-- 两边都尝试插入
insert into settings (user_id, key, value) values (123, 'theme', 'dark');
-- 一个成功,另一个撞上主键冲突报错!正确做法(原子 UPSERT):
-- 单条原子操作
insert into settings (user_id, key, value)
values (123, 'theme', 'dark')
on conflict (user_id, key)
do update set value = excluded.value, updated_at = now();
-- 返回写入 / 更新后的行
insert into settings (user_id, key, value)
values (123, 'theme', 'dark')
on conflict (user_id, key)
do update set value = excluded.value
returning *;存在即跳过的写法:
-- 不存在才插入,已存在就不动(不更新)
insert into page_views (page_id, user_id)
values (1, 123)
on conflict (page_id, user_id) do nothing;Reference: INSERT ON CONFLICT
Postgres 性能与最佳实践
📌 译自 Supabase 官方 agent skills(supabase/agent-skills→supabase-postgres-best-practices,MIT License, © Supabase),翻译为中文并适配火山引擎 Supabase 版。
按影响优先级组织的 Postgres 性能优化与最佳实践规则,共 8 类、31 条。每条含「错误 / 正确」SQL 对比与适用说明。所有 SQL 通过 byted-supabase-cli db query "<sql>" --workspace-id ws-... 执行;改完跑 byted-supabase-cli db advisors --workspace-id ws-... 巡检。
何时查阅
- 写 SQL 查询或设计 schema
- 实现索引或查询优化
- 排查数据库性能问题
- 配置连接池或扩容
- 配置行级安全(RLS)
分类(按影响优先级)
| 优先级 | 分类 | 影响 | 文件 |
|---|---|---|---|
| 1 | 查询性能 | CRITICAL | `query.md` |
| 2 | 连接管理 | CRITICAL | `conn.md` |
| 3 | 安全与 RLS | CRITICAL | `security.md` |
| 4 | Schema 设计 | HIGH | `schema.md` |
| 5 | 并发与锁 | MEDIUM-HIGH | `lock.md` |
| 6 | 数据访问模式 | MEDIUM | `data.md` |
| 7 | 监控与诊断 | LOW-MEDIUM | `monitor.md` |
| 8 | 高级特性 | LOW | `advanced.md` |
相关:Supabase 特有安全陷阱见 `../security-guide.md`,RLS 配置模板见 `../rls-guide.md`,火山命名约定与建表模板见 `../schema-guide.md`。
参考
- https://www.postgresql.org/docs/current/
- https://supabase.com/docs/guides/database/overview
- https://supabase.com/docs/guides/auth/row-level-security
- 火山引擎 Database 文档:https://www.volcengine.com/docs/87275/2385100?lang=zh
</content>
并发与锁(lock)
事务管理、隔离级别、死锁预防与锁竞争模式。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;改完跑byted-supabase-cli db advisors巡检。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 事务越短越好,把锁竞争压到最低
- 统一加锁顺序,从源头掐掉死锁
- 应用级互斥,优先用咨询锁(advisory lock)
- 队列消费用 SKIP LOCKED,让 worker 不再互相干等
事务越短越好,把锁竞争压到最低
影响(Impact): MEDIUM-HIGH — 吞吐提升 3-5 倍,死锁明显减少。
事务一旦拉长,它持有的锁就会把别的查询全堵在门外。原则很简单:事务能多短就多短,只在真正要改数据时才开。
错误做法(事务里夹着外部调用,迟迟不提交):
begin;
select * from orders where id = 1 for update; -- 拿到行锁
-- 应用此时去调支付 API 的 HTTP 接口(耗时 2-5 秒)
-- 这期间,所有访问这一行的查询全被卡住!
update orders set status = 'paid' where id = 1;
commit; -- 锁在整个过程里一直被攥着正确做法(把事务范围收到最小):
-- 数据校验、调外部 API 这些活,统统挪到事务外面去做
-- 应用层先执行:response = await paymentAPI.charge(...)
-- 事务里只留下真正的那条更新
begin;
update orders
set status = 'paid', payment_id = $1
where id = $2 and status = 'pending'
returning *;
commit; -- 锁只持有毫秒级再配一道 statement_timeout 保险,防止失控的事务赖着不走:
-- 超过 30 秒还没跑完的查询直接中止
set statement_timeout = '30s';
-- 或者只对当前会话生效
set local statement_timeout = '5s';Reference: Transaction Management
统一加锁顺序,从源头掐掉死锁
影响(Impact): MEDIUM-HIGH — 消除死锁报错,提升可靠性。
死锁的成因一句话就能说清:两个事务以相反的顺序去抢同一批资源,结果各自攥着一半、互相死等对方手里的锁。破解之道也只有一条——所有事务都按同一个固定顺序加锁。
错误做法(加锁顺序各自为政):
-- Transaction A -- Transaction B
begin; begin;
update accounts update accounts
set balance = balance - 100 set balance = balance - 50
where id = 1; where id = 2; -- B 锁住第 2 行
update accounts update accounts
set balance = balance + 100 set balance = balance + 50
where id = 2; -- A 等 B 放锁 where id = 1; -- B 等 A 放锁
-- 死锁!两边都在死等对方正确做法(动手前先按固定顺序把行锁齐):
-- 更新前,显式地按 ID 顺序一次性把锁加好
begin;
select * from accounts where id in (1, 2) order by id for update;
-- 锁已在手,后面爱怎么更新就怎么更新,顺序无所谓
update accounts set balance = balance - 100 where id = 1;
update accounts set balance = balance + 100 where id = 2;
commit;另一种思路:用单条语句一把搞定,让加锁原子化:
-- 单条语句把所有锁原子地拿全
begin;
update accounts
set balance = balance + case id
when 1 then -100
when 2 then 100
end
where id in (1, 2);
commit;从日志里揪出死锁:
-- 看看最近有没有发生过死锁
select * from pg_stat_database where deadlocks > 0;
-- 打开死锁日志
set log_lock_waits = on;
set deadlock_timeout = '1s';Reference: Deadlocks
应用级互斥,优先用咨询锁(advisory lock)
影响(Impact): MEDIUM — 高效协调,免去行级锁的额外开销。
很多场景要锁的并不是某一行数据,而是一段"逻辑临界区"。这种应用层的协调,咨询锁(advisory lock)正合适——它不需要你真的去锁某条记录。
错误做法(为了加锁专门造几行数据出来):
-- 凭空建表造行,只为有个东西可以锁
create table resource_locks (
resource_name text primary key
);
insert into resource_locks values ('report_generator');
-- 靠 select 这一行来加锁
select * from resource_locks where resource_name = 'report_generator' for update;正确做法(直接用咨询锁):
-- 会话级咨询锁(断连或手动解锁时释放)
select pg_advisory_lock(hashtext('report_generator'));
-- ... 在这里做独占的工作 ...
select pg_advisory_unlock(hashtext('report_generator'));
-- 事务级咨询锁(commit/rollback 时释放)
begin;
select pg_advisory_xact_lock(hashtext('daily_report'));
-- ... 做事 ...
commit; -- 锁随事务自动释放不想干等的场景,用 try-lock:
-- 立刻返回 true/false,不阻塞等待
select pg_try_advisory_lock(hashtext('resource_name'));
-- 在应用里这样用
if (acquired) {
-- 抢到了,干活
select pg_advisory_unlock(hashtext('resource_name'));
} else {
-- 没抢到,跳过或稍后重试
}Reference: Advisory Locks
队列消费用 SKIP LOCKED,让 worker 不再互相干等
影响(Impact): MEDIUM-HIGH — worker 队列吞吐提升 10 倍。
多个 worker 同时啃一个任务队列时,SKIP LOCKED 能让每个 worker 自动跳过已被别人锁住的行,各取各的活,谁也不用排队等谁。
错误做法(worker 之间互相卡死):
-- Worker 1 和 Worker 2 都来抢下一个任务
begin;
select * from jobs where status = 'pending' order by created_at limit 1 for update;
-- Worker 2 只能干等 Worker 1 放锁!正确做法(用 SKIP LOCKED 实现并行消费):
-- 每个 worker 跳过被锁的行,直接领下一个可用任务
begin;
select * from jobs
where status = 'pending'
order by created_at
limit 1
for update skip locked;
-- Worker 1 领走 job 1,Worker 2 领走 job 2,互不等待
update jobs set status = 'processing' where id = $1;
commit;完整的队列消费写法:
-- 单条语句原子地"领取并更新"
update jobs
set status = 'processing', worker_id = $1, started_at = now()
where id = (
select id from jobs
where status = 'pending'
order by created_at
limit 1
for update skip locked
)
returning *;Reference: SELECT FOR UPDATE SKIP LOCKED
监控与诊断(monitor)
用 pg_stat_statements、EXPLAIN ANALYZE、指标采集做性能诊断。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;也可用byted-supabase-cli inspect db <subcmd>(如 long-running-queries / table-stats)。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 开启 pg_stat_statements 分析查询
- 用 EXPLAIN ANALYZE 诊断慢查询
- 用 VACUUM 和 ANALYZE 维护表统计信息
开启 pg_stat_statements 分析查询
影响(Impact): LOW-MEDIUM — 一眼定位最吃资源的查询。
pg_stat_statements 会记录所有查询的执行统计信息,帮你定位运行慢、调用频繁的语句。
错误(对查询行为两眼一抹黑):
-- 数据库很慢,可到底是哪条查询拖的后腿?
-- 没有 pg_stat_statements 根本无从查起正确(开启扩展并查询 pg_stat_statements):
-- 启用扩展
create extension if not exists pg_stat_statements;
-- 按累计耗时找出最慢的查询
select
calls,
round(total_exec_time::numeric, 2) as total_time_ms,
round(mean_exec_time::numeric, 2) as mean_time_ms,
query
from pg_stat_statements
order by total_exec_time desc
limit 10;
-- 找出调用最频繁的查询
select calls, query
from pg_stat_statements
order by calls desc
limit 10;
-- 优化完成后重置统计信息
select pg_stat_statements_reset();重点盯这几个指标:
-- 平均耗时偏高的查询(优化的首选目标)
select query, mean_exec_time, calls
from pg_stat_statements
where mean_exec_time > 100 -- 平均超过 100ms
order by mean_exec_time desc;Reference: pg_stat_statements
用 EXPLAIN ANALYZE 诊断慢查询
影响(Impact): LOW-MEDIUM — 精确锁定查询执行的真实瓶颈。
EXPLAIN ANALYZE 会真正跑一遍查询并给出实测耗时,瓶颈到底卡在哪一目了然,不必靠猜。
错误(凭感觉猜性能问题):
-- 查询慢,可为什么慢?
select * from orders where customer_id = 123 and status = 'pending';
-- "肯定是少了个索引" —— 但到底该建哪个?正确(使用 EXPLAIN ANALYZE):
explain (analyze, buffers, format text)
select * from orders where customer_id = 123 and status = 'pending';
-- 输出直接暴露了问题:
-- Seq Scan on orders (cost=0.00..25000.00 rows=50 width=100) (actual time=0.015..450.123 rows=50 loops=1)
-- Filter: ((customer_id = 123) AND (status = 'pending'::text))
-- Rows Removed by Filter: 999950
-- Buffers: shared hit=5000 read=15000
-- Planning Time: 0.150 ms
-- Execution Time: 450.500 ms读查询计划时重点关注这些信号:
-- 大表上出现 Seq Scan = 缺索引
-- Rows Removed by Filter = 选择性差或缺索引
-- Buffers: read 远大于 hit = 数据没命中缓存,内存需加大
-- Nested Loop 且 loops 很大 = 该换一种 join 策略了
-- Sort Method: external merge = work_mem 给小了Reference: EXPLAIN
用 VACUUM 和 ANALYZE 维护表统计信息
影响(Impact): MEDIUM — 统计信息准确后查询计划质量提升 2-10 倍。
统计信息一旦过时,查询规划器就会做出糟糕的决策。VACUUM 负责回收空间,ANALYZE 负责更新统计信息。
错误(统计信息陈旧):
-- 表里实际有 100 万行,统计信息却说只有 1000 行
-- 查询规划器据此选了错误的策略
explain select * from orders where status = 'pending';
-- 结果走了: Seq Scan(因为统计信息显示这是张小表)
-- 实际上: 走 Index Scan 会快得多正确(让统计信息保持最新):
-- 大批量数据变更后手动 analyze
analyze orders;
-- 针对 WHERE 子句中用到的列做 analyze
analyze orders (status, created_at);
-- 查看各表最近一次 analyze 的时间
select
relname,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
from pg_stat_user_tables
order by last_analyze nulls first;为高写入量的表调优 autovacuum:
-- 给高频变更的表提高触发频率
alter table orders set (
autovacuum_vacuum_scale_factor = 0.05, -- 死元组达 5% 即 vacuum(默认 20%)
autovacuum_analyze_scale_factor = 0.02 -- 变更达 2% 即 analyze(默认 10%)
);
-- 查看 autovacuum 当前进度
select * from pg_stat_progress_vacuum;Reference: VACUUM
查询性能(query)
慢查询、缺失索引、低效查询计划——Postgres 性能问题最常见的来源。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;改完跑byted-supabase-cli db advisors巡检。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 给 WHERE 和 JOIN 的列建索引
- 多列过滤优先上复合索引
- 用覆盖索引省掉回表
- 按数据特征挑对索引类型
- 固定过滤条件就用部分索引
给 WHERE 和 JOIN 的列建索引
影响(Impact): CRITICAL — 大表查询可提速 100 到 1000 倍。
在没有索引的列上做过滤或关联,Postgres 只能走全表扫描。表一大,扫描代价会随之指数级飙升。
写法不对,大表上触发了顺序扫描(全表扫描):
-- customer_id 上没索引,只能全表扫描
select * from orders where customer_id = 123;
-- EXPLAIN shows: Seq Scan on orders (cost=0.00..25000.00 rows=100 width=85)正确做法是让查询走索引扫描:
-- 给高频过滤的列建索引
create index orders_customer_id_idx on orders (customer_id);
select * from orders where customer_id = 123;
-- EXPLAIN shows: Index Scan using orders_customer_id_idx (cost=0.42..8.44 rows=100 width=85)关联查询同理,外键所在的那一侧务必建索引:
-- 给引用方的列建索引
create index orders_customer_id_idx on orders (customer_id);
select c.name, o.total
from customers c
join orders o on o.customer_id = c.id;Reference: Query Optimization
多列过滤优先上复合索引
影响(Impact): HIGH — 多列查询可提速 5 到 10 倍。
查询条件同时命中多个列时,与其给每列各建一个单列索引,不如建一个复合索引来得高效。
写法不对,多个独立索引迫使 Postgres 走 bitmap 扫描把它们拼起来:
-- 两个独立索引
create index orders_status_idx on orders (status);
create index orders_created_idx on orders (created_at);
-- 查询得把两个索引合并起来用,反而更慢
select * from orders where status = 'pending' and created_at > '2024-01-01';正确做法是合并成一个复合索引:
-- 单个复合索引,等值匹配的列放最左
create index orders_status_created_idx on orders (status, created_at);
-- 一次高效的索引扫描就够了
select * from orders where status = 'pending' and created_at > '2024-01-01';列顺序很关键——等值匹配的列靠前,范围匹配的列靠后:
-- 推荐:等值的 status 排在范围的 created_at 之前
create index idx on orders (status, created_at);
-- 高效:WHERE status = 'pending'(前导列等值,可 seek 收窄范围)
-- 高效:WHERE status = 'pending' AND created_at > '2024-01-01'
-- 低效:WHERE created_at > '2024-01-01'(缺前导列 status,无法用最左列收窄;
-- PG 仍可能用该索引,但退化成全索引扫描后过滤,远不如把 created_at 当前导列)Reference: Multicolumn Indexes
用覆盖索引省掉回表
影响(Impact): MEDIUM-HIGH — 免去回表取数,查询可提速 2 到 5 倍。
覆盖索引把查询要用到的列全都带进索引里,于是 Postgres 走 index-only scan 就能拿到结果,根本不必再去碰底层数据表。
写法不对,先走索引扫描,再回表(heap fetch)取剩下的列:
create index users_email_idx on users (email);
-- name 和 created_at 还得回表去取
select email, name, created_at from users where email = 'user@example.com';正确做法是用 INCLUDE 让查询走 index-only scan:
-- 把不参与检索的列一并塞进索引
create index users_email_idx on users (email) include (name, created_at);
-- 所有列都从索引出,不再访问数据表
select email, name, created_at from users where email = 'user@example.com';凡是只在 SELECT 里输出、不参与过滤的列,都适合放进 INCLUDE:
-- 按 status 检索,但还要返回 customer_id 和 total
create index orders_status_idx on orders (status) include (customer_id, total);
select status, customer_id, total from orders where status = 'shipped';Reference: Index-Only Scans
按数据特征挑对索引类型
影响(Impact): HIGH — 选对索引类型能带来 10 到 100 倍的性能提升。
不同的查询模式各有称手的索引类型,默认的 B-tree 并非万能。
写法不对,拿 B-tree 去扛 JSONB 的包含查询:
-- B-tree 优化不了包含类操作符
create index products_attrs_idx on products (attributes);
select * from products where attributes @> '{"color": "red"}';
-- 仍然全表扫描——B-tree 不支持 @> 操作符正确做法是 JSONB 改用 GIN:
-- GIN 支持 @>、?、?&、?| 等操作符
create index products_attrs_idx on products using gin (attributes);
select * from products where attributes @> '{"color": "red"}';索引类型选型速查:
-- B-tree(默认):=、<、>、BETWEEN、IN、IS NULL
create index users_created_idx on users (created_at);
-- GIN:数组、JSONB、全文检索
create index posts_tags_idx on posts using gin (tags);
-- GiST:几何数据、范围类型、最近邻(KNN)查询
create index locations_idx on places using gist (location);
-- BRIN:超大时序表(体积可缩小到 1/10 至 1/100)
create index events_time_idx on events using brin (created_at);
-- Hash:仅等值匹配(= 上比 B-tree 略快)
create index sessions_token_idx on sessions using hash (token);Reference: Index Types
固定过滤条件就用部分索引
影响(Impact): HIGH — 索引体积缩小 5 到 20 倍,写入和查询都更快。
部分索引只收录满足某个 WHERE 条件的行。当查询总是带着同一个过滤条件时,这样的索引更小、更快。
写法不对,整张表都进索引,连软删除的行也一并收录:
-- 索引收录了所有行,连软删除的也算进去
create index users_email_idx on users (email);
-- 查询却始终只看活跃用户
select * from users where email = 'user@example.com' and deleted_at is null;正确做法是让索引条件与查询过滤对齐:
-- 索引只收录活跃用户
create index users_active_email_idx on users (email)
where deleted_at is null;
-- 查询命中这个更小更快的索引
select * from users where email = 'user@example.com' and deleted_at is null;部分索引的几个典型场景:
-- 只索引待处理订单(一旦完成,status 基本不再变)
create index orders_pending_idx on orders (created_at)
where status = 'pending';
-- 只索引非空值
create index products_sku_idx on products (sku)
where sku is not null;Reference: Partial Indexes
Schema 设计(schema)
表设计、索引策略、分区与数据类型选择,是长期性能的基础。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;改完跑byted-supabase-cli db advisors巡检。火山命名约定与建表模板另见../schema-guide.md。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 选对主键策略
- 用对数据类型
- 迁移里安全地加约束
- 给外键列建索引
- 标识符一律小写
- 大表做分区
选对主键策略
影响(Impact): HIGH — 索引局部性更好,碎片更少。
主键怎么选,直接决定了写入性能、索引体积和复制效率,不是随手定个 id 就完事。
错误示范(这几种主键有坑):
-- identity 是 SQL 标准写法
create table users (
id serial primary key -- 能用,但更推荐 IDENTITY
);
-- 随机 UUID(v4)会把索引搞得很碎
create table orders (
id uuid default gen_random_uuid() primary key -- UUIDv4 是随机的,插入位置乱跳
);正确示范(推荐的主键策略):
-- 顺序自增 ID 用 IDENTITY(SQL 标准,绝大多数场景的首选)
create table users (
id bigint generated always as identity primary key
);
-- 分布式系统必须用 UUID 时,选 UUIDv7(带时间序)
-- 需要 pg_uuidv7 扩展:create extension pg_uuidv7;
create table orders (
id uuid default uuid_generate_v7() primary key -- 按时间有序,不产生碎片
);
-- 备选方案:时间前缀 ID,同样可排序、适合分布式,且无需任何扩展
create table events (
id text default concat(
to_char(now() at time zone 'utc', 'YYYYMMDDHH24MISSMS'),
gen_random_uuid()::text
) primary key
);选型要点:
- 单库场景:
bigint identity(顺序、8 字节、SQL 标准)。 - 分布式或主键需要对外暴露:用 UUIDv7(依赖 pg_uuidv7)或 ULID,二者都带时间序、不产生碎片。
serial虽然能跑,但identity才是 SQL 标准,新项目优先用它。- 大表别拿随机 UUID(v4)当主键,否则索引会严重碎片化。
Reference: Identity Columns
用对数据类型
影响(Impact): HIGH — 存储省一半,比较更快。
数据类型选得准,既能压缩存储、加速查询,还能从源头堵住一类 bug。
错误示范(类型选错):
create table users (
id int, -- 21 亿就溢出了
email varchar(255), -- 没必要的长度限制
created_at timestamp, -- 丢了时区信息
is_active varchar(5), -- 拿字符串存布尔值
price varchar(20) -- 拿字符串存数值
);正确示范(类型选对):
create table users (
id bigint generated always as identity primary key, -- 上限约 900 亿亿
email text, -- 不设人为长度上限,性能与 varchar 一致
created_at timestamptz, -- 时间一律带时区
is_active boolean default true, -- 仅 1 字节,强过变长字符串
price numeric(10,2) -- 精确的十进制运算
);核心准则:
-- ID:用 bigint,别用 int(给未来留余量)
-- 字符串:用 text,除非确实要限长才用 varchar(n)
-- 时间:用 timestamptz,别用 timestamp
-- 金额:用 numeric,别用 float(精度是底线)
-- 枚举:用 text 加 check 约束,或单独建 enum 类型Reference: Data Types
迁移里安全地加约束
影响(Impact): HIGH — 避免迁移失败,让 schema 变更可重复执行。
PostgreSQL 不支持 ADD CONSTRAINT IF NOT EXISTS,迁移脚本里这么写一定报错。
错误示范(直接语法报错):
-- ERROR: syntax error at or near "not" (SQLSTATE 42601)
alter table public.profiles
add constraint if not exists profiles_birthchart_id_unique unique (birthchart_id);正确示范(幂等地建约束):
-- 用 DO 块先判断再添加
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'profiles_birthchart_id_unique'
and conrelid = 'public.profiles'::regclass
) then
alter table public.profiles
add constraint profiles_birthchart_id_unique unique (birthchart_id);
end if;
end $$;各类约束都照此办理:
-- Check 约束
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'check_age_positive'
) then
alter table users add constraint check_age_positive check (age > 0);
end if;
end $$;
-- 外键
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'profiles_birthchart_id_fkey'
) then
alter table profiles
add constraint profiles_birthchart_id_fkey
foreign key (birthchart_id) references birthcharts(id);
end if;
end $$;查约束是否已存在:
-- 查询某张表上的所有约束
select conname, contype, pg_get_constraintdef(oid)
from pg_constraint
where conrelid = 'public.profiles'::regclass;
-- contype 取值含义:
-- 'p' = PRIMARY KEY
-- 'f' = FOREIGN KEY
-- 'u' = UNIQUE
-- 'c' = CHECKReference: Constraints
给外键列建索引
影响(Impact): HIGH — JOIN 与级联操作快 10 到 100 倍。
Postgres 不会自动给外键列建索引。少了这个索引,JOIN 和级联(CASCADE)操作都会变慢。
错误示范(外键没建索引):
create table orders (
id bigint generated always as identity primary key,
customer_id bigint references customers(id) on delete cascade,
total numeric(10,2)
);
-- customer_id 上没有索引!
-- JOIN 和 ON DELETE CASCADE 都得全表扫描
select * from orders where customer_id = 123; -- 走 Seq Scan
delete from customers where id = 123; -- 锁表,并扫遍所有 orders正确示范(外键已建索引):
create table orders (
id bigint generated always as identity primary key,
customer_id bigint references customers(id) on delete cascade,
total numeric(10,2)
);
-- 外键列务必建索引
create index orders_customer_id_idx on orders (customer_id);
-- 现在 JOIN 和级联都很快
select * from orders where customer_id = 123; -- 走 Index Scan
delete from customers where id = 123; -- 走索引,级联很快揪出缺索引的外键:
select
conrelid::regclass as table_name,
a.attname as fk_column
from pg_constraint c
join pg_attribute a on a.attrelid = c.conrelid and a.attnum = any(c.conkey)
where c.contype = 'f'
and not exists (
select 1 from pg_index i
where i.indrelid = c.conrelid and a.attnum = any(i.indkey)
);Reference: Foreign Keys
标识符一律小写
影响(Impact): MEDIUM — 规避各类工具、ORM 和 AI 助手上的大小写敏感问题。
PostgreSQL 会把未加引号的标识符统一折叠成小写。一旦用引号包了混合大小写的标识符,从此每次引用都得带引号,而且各种工具、ORM 乃至 AI 助手往往认不出来,平添麻烦。
错误示范(混合大小写标识符):
-- 带引号的标识符虽保留大小写,但处处都得加引号
CREATE TABLE "Users" (
"userId" bigint PRIMARY KEY,
"firstName" text,
"lastName" text
);
-- 永远得加引号,否则查询失败
SELECT "firstName" FROM "Users" WHERE "userId" = 1;
-- 这句会失败——不加引号时 Users 被当成 users
SELECT firstName FROM Users;
-- ERROR: relation "users" does not exist正确示范(小写 snake_case):
-- 不加引号的小写标识符,可移植,对工具友好
CREATE TABLE users (
user_id bigint PRIMARY KEY,
first_name text,
last_name text
);
-- 无需引号,所有工具都认得
SELECT first_name FROM users WHERE user_id = 1;混合大小写常见的来源:
-- ORM 经常生成带引号的 camelCase——把它配置成 snake_case
-- 从其他数据库迁移过来,可能原样保留了大小写
-- 部分 GUI 工具默认给标识符加引号——关掉这个选项
-- 实在被混合大小写套牢,就建视图当兼容层
CREATE VIEW users AS SELECT "userId" AS user_id, "firstName" AS first_name FROM "Users";Reference: Identifiers and Key Words
大表做分区
影响(Impact): MEDIUM-HIGH — 大表上的查询与维护操作快 5 到 20 倍。
分区把一张大表拆成若干小块,查询性能和维护操作都能跟着受益。
错误示范(单张大表):
create table events (
id bigint generated always as identity,
created_at timestamptz,
data jsonb
);
-- 5 亿行,查询每次都扫全表
select * from events where created_at > '2024-01-01'; -- 慢
vacuum events; -- 跑几个小时,还锁表正确示范(按时间范围分区):
create table events (
id bigint generated always as identity,
created_at timestamptz not null,
data jsonb
) partition by range (created_at);
-- 按月建分区
create table events_2024_01 partition of events
for values from ('2024-01-01') to ('2024-02-01');
create table events_2024_02 partition of events
for values from ('2024-02-01') to ('2024-03-01');
-- 查询只扫命中的分区
select * from events where created_at > '2024-01-15'; -- 只扫 events_2024_01 及之后
-- 清旧数据瞬间完成
drop table events_2023_01; -- 秒级,而 DELETE 要几个小时什么时候该分区:
- 表超过 1 亿行。
- 时序数据,查询以日期为条件。
- 需要高效地清理旧数据。
Reference: Table Partitioning
安全与 RLS(security)
行级安全策略、权限管理与认证模式。
本目录 SQL 通过byted-supabase-cli db query "<sql>" --workspace-id ws-...执行;改完跑byted-supabase-cli db advisors巡检。更完整的 Supabase 特有安全陷阱见../security-guide.md,RLS 配置模板见../rls-guide.md。
📌 译自 Supabase 官方 agent skills(MIT, © Supabase),适配火山引擎 Supabase 版。
目录
- 多租户数据务必启用行级安全(RLS)
- 把 RLS 策略写得跑得快
- 坚持最小权限原则
多租户数据务必启用行级安全(RLS)
影响(Impact): CRITICAL — 把租户隔离下沉到数据库强制执行,从根上堵住数据泄露。
多租户系统里,隔离这件事不能只靠应用层那句 where 条件。行级安全(RLS)把访问控制规则交给数据库本身把关,无论查询从哪儿发出,用户都只能拿到属于自己的那部分数据。
错误做法(只靠应用层过滤):
只要应用代码出一个 bug,或者有人绕过了那层过滤逻辑,全表数据就一览无余——这道防线太脆。
-- 仅依赖应用层做过滤
select * from orders where user_id = $current_user_id;
-- 一旦逻辑出错或被绕过,所有数据都会暴露!
select * from orders; -- 会返回全部订单正确做法(由数据库强制实施 RLS):
开启 RLS 后,策略会自动套到每一条查询上;再用 force row level security 把表的 owner(所有者)也一并管住,谁都别想绕过去。
-- 在表上启用 RLS
alter table orders enable row level security;
-- 创建策略,让用户只能看到自己的订单
create policy orders_user_policy on orders
for all
using (user_id = current_setting('app.current_user_id')::bigint);
-- 连表 owner 也强制走 RLS
alter table orders force row level security;
-- 设置用户上下文后再查询
set app.current_user_id = '123';
select * from orders; -- 只返回用户 123 的订单⚠️ 验证 RLS 时别用表 owner / 超级角色直连:force row level security能管住表 owner(所有者),但带BYPASSRLS属性的角色(火山引擎 Supabase 版的postgres连接角色就带这个属性)仍会绕过所有 RLS——你会看到全部行,误以为策略没生效。要验证策略,请切到不带 BYPASSRLS 的角色,例如set local role authenticated;再查,结束reset role;。
针对 authenticated 角色的策略写法:
create policy orders_user_policy on orders
for all
to authenticated
using (user_id = auth.uid());Reference: Row Level Security
把 RLS 策略写得跑得快
影响(Impact): HIGH — 用对写法,RLS 查询能快上 5-10 倍。
RLS 策略写得糙,性能会塌得很惨。关键就两点:把函数调用包进子查询让它只算一次,以及给策略里用到的列加索引。
错误做法(每一行都要调一次函数):
create policy orders_policy on orders
using (auth.uid() = user_id); -- auth.uid() 会被逐行调用!
-- 100 万行就意味着 auth.uid() 被调 100 万次正确做法(用 SELECT 把函数包起来):
包进 (select ...) 之后,规划器会把结果当成常量算一次再缓存下来,逐行重复计算的开销就没了。
create policy orders_policy on orders
using ((select auth.uid()) = user_id); -- 只算一次(计划里体现为 InitPlan),结果被缓存
-- 大表上常能快数倍到一个数量级(实测约 10x,量级取决于是否走索引与计划形态;
-- 注意:PG 较新版本里,裸写 auth.uid() 若能被内联进 Index Cond 也不慢,
-- 逐行重算的惩罚主要出现在 seq scan / filter 场景——但用 (select ...) 包一层总是更稳)复杂的权限判断交给 security definer 函数:
SECURITY DEFINER 函数以创建者的权限运行,会绕过它所访问的任意表上的 RLS——这正是它适合做内部查找的原因,但用错了也同样危险。务必在函数体内部显式校验一次 auth.uid(),把它放在不对外暴露的 schema 里,并对任何不该直接调用它的角色 revoke 掉 EXECUTE 权限。
-- 在私有 schema 里创建辅助函数
create or replace function private.is_team_member(team_id bigint)
returns boolean
language sql
security definer
set search_path = ''
as $$
select exists (
select 1 from public.team_members
-- 始终在函数内部校验调用方的身份
where team_id = $1 and user_id = (select auth.uid())
);
$$;
-- 回收 public 角色的直接执行权限
revoke execute on function private.is_team_member(bigint) from PUBLIC, anon, authenticated, service_role;
-- 在策略中使用(走索引查找,而非逐行判断)
create policy team_orders_policy on orders
using ((select private.is_team_member(team_id)));策略里用到的列,一律记得加索引:
create index orders_user_id_idx on orders (user_id);Reference: RLS Performance
坚持最小权限原则
影响(Impact): MEDIUM — 收窄攻击面,审计链路也更清晰。
只授予完成工作所需的最小权限。应用查询绝不能用超级用户身份去跑。
错误做法(权限给得太宽):
应用直接用超级用户连库,或者把 ALL 一股脑授给应用角色——这种情况下,任何一次 SQL 注入都可能酿成灾难:一句 drop table users 就能顺着级联把整个库带塌。
-- 应用使用超级用户连接
-- 或者把 ALL 权限授给应用角色
grant all privileges on all tables in schema public to app_user;
grant all privileges on all sequences in schema public to app_user;
-- 任何一次 SQL 注入都会演变成灾难
-- drop table users; 会级联波及一切正确做法(精确、按需地授予):
按读、写拆出独立角色,各自只拿够用的权限,再让登录角色从中继承。这样即便某个角色被攻破,影响也被框死在很小的范围内。
-- 创建一个默认没有任何权限的角色
create role app_readonly nologin;
-- 只在特定表上授予 SELECT
grant usage on schema public to app_readonly;
grant select on public.products, public.categories to app_readonly;
-- 创建写入角色,权限范围有限
create role app_writer nologin;
grant usage on schema public to app_writer;
grant select, insert, update on public.orders to app_writer;
grant usage on sequence orders_id_seq to app_writer;
-- 不授予 DELETE 权限
-- 登录角色从上面这些角色继承权限
create role app_user login password 'xxx';
grant app_writer to app_user;回收 public 的默认权限:
-- 回收 public 的默认访问权限
revoke all on schema public from public;
revoke all on all tables in schema public from public;Reference: Roles and Privileges
Row Level Security (RLS) 策略配置指南
🔴 所有暴露 schema(默认含 `public`)里的表必须启用 RLS。即使是公开数据表,也必须启用 RLS 并配置允许公开访问的策略。不启用 RLS 的表,任何人都可以通过 anon key 直接读写所有数据,这是严重的安全隐患。
📌 本文 SQL 写法对齐 Supabase 官方最新安全建议(MIT,Copyright © Supabase)。更完整的安全陷阱见 `security-guide.md`。
---
目录
- 写法要点(务必遵守)
- 策略选择(决策表)
- 操作方式
- 策略 SQL 模板
- user_id 字段定义
- RLS 性能
- 检查当前 RLS 状态
- 常见错误
写法要点(务必遵守)
这几条是 Supabase RLS 的硬规则,违反会静默制造漏洞或性能问题:
1. 用 `TO authenticated` / `TO anon` 指定角色,不要用 `auth.role()`。 auth.role() 已弃用;且开启匿名登录后,匿名用户也带 authenticated 角色,auth.role() = 'authenticated' 会静默失效。 2. `TO authenticated` 必须配所有权谓词。 只写 TO authenticated 是"认证而非授权"(BOLA/IDOR),任何登录用户都能看所有行。 3. 把 `auth.uid()` 包进子查询:`(select auth.uid())`。 否则每行都会调用一次,百万行就调用百万次;包一层后只算一次,大表上快 100×+。 4. UPDATE 策略同时写 `USING` 和 `WITH CHECK`。 缺 WITH CHECK,用户能把行的 user_id 改成别人的。 5. UPDATE 需要 SELECT 策略。 RLS 下 UPDATE 要先 SELECT 到行;缺 SELECT 策略会静默返回 0 行。 6. RLS 策略里用到的列要建索引(如 user_id)。
---
策略选择(决策表)
根据数据访问需求,选择对应的策略场景:
| 场景 | 说明 | 需要 user_id? | 示例 |
|---|---|---|---|
| A. 公开读写 | 所有人可读写 | ❌ | 公告、公共配置 |
| B. 公开读 + 登录写 | 所有人可读,仅登录用户可写 | ❌ | 博客文章、商品展示 |
| C. 仅登录用户 | 登录用户才能读写 | ❌ | 内部数据、会员内容 |
| D. 用户私有数据 | 用户只能操作自己的数据 | ✅ | 用户订单、个人笔记、私人设置 |
⚠️ 常见误解:user_id字段不是 RLS 的前提条件。只有场景 D(用户私有数据)才需要user_id。
---
操作方式
所有 RLS SQL 均可通过 byted-supabase-cli db query 直接执行:
byted-supabase-cli db query "ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;" --workspace-id ws-xxxx对于成组、需要复用的 RLS 变更,建议写入 .sql 文件后整体应用:
# rls_migration.sql 内含 ENABLE RLS + 一组 CREATE POLICY
byted-supabase-cli db query -f ./rls_migration.sql --workspace-id ws-xxxx改完跑巡检:byted-supabase-cli db advisors --workspace-id ws-xxxx。需要版本化 / 可追溯的策略演进时,改用声明式管理:byted-supabase-cli db schema declarative --help。
---
策略 SQL 模板
💡 命名规范:Policy 名称应包含表名前缀(如posts_allow_public_read),便于在多表环境中区分管理。以下模板使用<table_name>作为占位符,实际使用时替换为真实表名。
⚠️ 幂等性:CREATE POLICY 在策略已存在时会报错。如需重新配置,先删除再创建:```sql
DROP POLICY IF EXISTS "policy_name" ON <table_name>;
CREATE POLICY "policy_name" ON <table_name> ...;
```
修改已有表(如加字段)时,已有的 RLS 策略仍然生效,无需重复配置。
场景 A:公开读写
公开数据也要启用 RLS,并显式开放给 anon 和 authenticated:
ALTER TABLE <table_name> ENABLE ROW LEVEL SECURITY;
CREATE POLICY "<table_name>_public_select" ON <table_name>
FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "<table_name>_public_insert" ON <table_name>
FOR INSERT TO anon, authenticated WITH CHECK (true);
CREATE POLICY "<table_name>_public_update" ON <table_name>
FOR UPDATE TO anon, authenticated USING (true) WITH CHECK (true);
CREATE POLICY "<table_name>_public_delete" ON <table_name>
FOR DELETE TO anon, authenticated USING (true);场景 B:公开读 + 登录写
ALTER TABLE <table_name> ENABLE ROW LEVEL SECURITY;
CREATE POLICY "<table_name>_public_select" ON <table_name>
FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "<table_name>_auth_insert" ON <table_name>
FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "<table_name>_auth_update" ON <table_name>
FOR UPDATE TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "<table_name>_auth_delete" ON <table_name>
FOR DELETE TO authenticated USING (true);场景 C:仅登录用户
ALTER TABLE <table_name> ENABLE ROW LEVEL SECURITY;
CREATE POLICY "<table_name>_auth_select" ON <table_name>
FOR SELECT TO authenticated USING (true);
CREATE POLICY "<table_name>_auth_insert" ON <table_name>
FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "<table_name>_auth_update" ON <table_name>
FOR UPDATE TO authenticated USING (true) WITH CHECK (true);
CREATE POLICY "<table_name>_auth_delete" ON <table_name>
FOR DELETE TO authenticated USING (true);⚠️ 场景 C 只校验"是否登录"。若还要限制"只能操作自己的数据",用场景 D 的所有权谓词。
场景 D:用户私有数据
⚠️ 表中必须包含user_id字段(见下方 user_id 字段定义)。注意auth.uid()包进(select ...),并对 UPDATE 写全USING+WITH CHECK。
ALTER TABLE <table_name> ENABLE ROW LEVEL SECURITY;
CREATE POLICY "<table_name>_owner_select" ON <table_name>
FOR SELECT TO authenticated
USING ( (select auth.uid()) = user_id );
CREATE POLICY "<table_name>_owner_insert" ON <table_name>
FOR INSERT TO authenticated
WITH CHECK ( (select auth.uid()) = user_id );
CREATE POLICY "<table_name>_owner_update" ON <table_name>
FOR UPDATE TO authenticated
USING ( (select auth.uid()) = user_id )
WITH CHECK ( (select auth.uid()) = user_id );
CREATE POLICY "<table_name>_owner_delete" ON <table_name>
FOR DELETE TO authenticated
USING ( (select auth.uid()) = user_id );---
user_id 字段定义
仅在场景 D(用户私有数据)时,需要在表中添加 user_id 字段。
建表时添加
CREATE TABLE <table_name> (
id bigserial PRIMARY KEY,
user_id uuid NOT NULL DEFAULT auth.uid(),
-- 其他字段 ...
created_at timestamptz NOT NULL DEFAULT now()
);
-- 务必给 RLS 谓词用到的列建索引
CREATE INDEX IF NOT EXISTS ix_<table_name>_user_id ON <table_name>(user_id);为已有表添加
ALTER TABLE <table_name>
ADD COLUMN user_id uuid NOT NULL DEFAULT auth.uid();
CREATE INDEX IF NOT EXISTS ix_<table_name>_user_id ON <table_name>(user_id);💡 使用 auth.uid() 作为默认值,Supabase 会在插入时自动填充当前用户 ID,防止客户端伪造。---
RLS 性能
RLS 谓词每行都会求值,写不好会严重拖慢查询:
- 把函数包进子查询:
(select auth.uid()) = user_id而非auth.uid() = user_id(只算一次,大表快 100×+)。 - 给谓词列建索引:
CREATE INDEX ... ON <table>(user_id);。 - 复杂多表判断用 `SECURITY DEFINER` 辅助函数(放进未暴露 schema、函数体内做
auth.uid()检查、收回多余EXECUTE),避免在策略里跨表关联导致的逐行开销。详见 `security-guide.md` 与 `pg-best-practices/security.md`。
---
检查当前 RLS 状态
查看哪些表已启用 RLS:
byted-supabase-cli db query "SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename;" --workspace-id ws-xxxx查看已有的 Policy:
byted-supabase-cli db query "SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check FROM pg_policies WHERE schemaname = 'public' ORDER BY tablename, policyname;" --workspace-id ws-xxxx---
常见错误
-- ❌ 错误:忘记启用 RLS(表对所有人完全开放)
CREATE TABLE posts (id serial PRIMARY KEY, title text);
-- 缺少 ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- ❌ 错误:启用了 RLS 但没有创建策略(所有人都无法访问)
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 缺少 CREATE POLICY ...
-- ❌ 错误:用已弃用的 auth.role()(开启匿名登录后静默失效)
CREATE POLICY "auth_write" ON posts FOR INSERT
WITH CHECK (auth.role() = 'authenticated');
-- ✅ 正确:用 TO 子句
CREATE POLICY "auth_write" ON posts FOR INSERT
TO authenticated WITH CHECK (true);
-- ❌ 错误:只写 TO authenticated,任何登录用户都能改所有行(IDOR)
CREATE POLICY "bad_update" ON notes FOR UPDATE TO authenticated USING (true);
-- ✅ 正确:配所有权谓词 + WITH CHECK
CREATE POLICY "owner_update" ON notes FOR UPDATE TO authenticated
USING ((select auth.uid()) = user_id)
WITH CHECK ((select auth.uid()) = user_id);
-- ❌ 错误:公开表也要求 user_id(增加了不必要的复杂度)
CREATE POLICY "public_read" ON announcements FOR SELECT
USING (auth.uid() = user_id);
-- ✅ 正确:公开表用 USING (true) + 指定角色
CREATE POLICY "public_read" ON announcements FOR SELECT
TO anon, authenticated USING (true);</content>
Supabase 安全 checklist(特有陷阱)
📌 本文内容改编并翻译自 Supabase 官方 agent skills(MIT License, Copyright © Supabase),并适配火山引擎 Supabase 版(byted-supabase-cli、workspace/branch 模型)。署名见文末。凡涉及 Auth、RLS、视图、Storage 或用户数据的任务,落地前逐条过一遍。这些都是 Supabase 特有、会静默制造漏洞的陷阱 —— 不报错,但门是开的。
所有 SQL 通过 byted-supabase-cli db query "<sql>" --workspace-id ws-...(或 -f file.sql)执行;改完务必跑 byted-supabase-cli db advisors --workspace-id ws-... 巡检。
---
目录
- 认证与会话安全
- API Key 与客户端暴露
- RLS、视图与特权数据库代码
- Storage 访问控制
- 依赖与供应链安全
- 兜底
1. 认证与会话安全
绝不用 user_metadata 做鉴权判断
在 Supabase 中,raw_user_meta_data(即 auth.jwt() 里的 user_metadata)用户自己可改,把它用于 RLS 策略或任何授权逻辑都可被伪造。鉴权数据一律存 `raw_app_meta_data` / `app_metadata`。
-- ❌ 危险:user_metadata 用户可改
create policy admin_only on reports for select
using ( (auth.jwt() -> 'user_metadata' ->> 'role') = 'admin' );
-- ✅ 正确:用 app_metadata(用户不可改)
create policy admin_only on reports for select
to authenticated
using ( (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' );删除用户不会让已签发的 access token 失效
删用户不会吊销其现有 token。敏感操作前先 sign out / 吊销会话;JWT 过期时间设短;要严格保证时,在敏感操作里校验 session_id 是否还在 auth.sessions 中。
app_metadata / auth.jwt() 的声明不一定是最新的
只有用户的 token 刷新后,JWT 里的声明才会更新。授权依赖这些声明时要意识到这点。
---
2. API Key 与客户端暴露
service_role / 密钥绝不进前端
前端用 publishable key(旧的 anon key 仅作兼容)。service_role 拥有完整权限、绕过 RLS,仅后端使用。
- 在 Next.js 里,任何
NEXT_PUBLIC_前缀的环境变量都会发到浏览器,绝不把service_role放进去。 byted-supabase-cli projects api-keys返回的service_role不要回显给前端或写进客户端代码;非必要不要打印完整密钥。
---
3. RLS、视图与特权数据库代码
视图默认绕过 RLS
视图以视图创建者的权限运行,会绕过底层表的 RLS。
-- ✅ PG15+:让视图以调用者权限执行,从而尊重 RLS
create view public.my_orders with (security_invoker = true) as
select * from public.orders;更低版本的 Postgres:从 anon / authenticated 收回视图权限,或把视图放进未暴露的 schema。
auth.role() 已弃用 —— 改用 TO 子句
Supabase 已弃用 auth.role(),改为在策略上直接用 TO authenticated / TO anon 指定目标角色。
除弃用外更危险的是:一旦开启匿名登录,匿名用户也携带 authenticated 这个 Postgres 角色,auth.role() = 'authenticated' 会静默通过,无论用户是否真正登录。
-- ❌ 弃用且危险,不要用
create policy "example" on table_name for select
using ( auth.role() = 'authenticated' );
-- ✅ 用 TO 子句
create policy "example" on table_name for select
to authenticated
using ( true );只写 TO authenticated 是"认证而非授权"(BOLA / IDOR)
TO authenticated 只校验角色,不限制能访问哪些行。正确做法是配上所有权谓词:
create policy "example" on table_name for select
to authenticated
using ( (select auth.uid()) = user_id );UPDATE 策略要同时有 USING 和 WITH CHECK
缺 WITH CHECK,用户能把某行的 user_id 改成别人的:
create policy "example" on table_name for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );UPDATE 需要 SELECT 策略
RLS 下 UPDATE 要先 SELECT 到行。没有 SELECT 策略时,更新静默返回 0 行——不报错,也不改任何东西。
SECURITY DEFINER 函数绕过 RLS
SECURITY DEFINER 函数以创建者权限运行(通常是带 bypassrls 的 postgres)。绝不用加 SECURITY DEFINER 的方式去"解决"权限报错——它会静默移除访问控制却不解决根因。优先 SECURITY INVOKER。
public 下的 SECURITY DEFINER 函数对所有角色可调用
Postgres 默认对每个新函数把 EXECUTE 授给 PUBLIC,所以 public schema 下任何 SECURITY DEFINER 函数都是 anon / authenticated(继承自 PUBLIC)可直接调用的公开端点。确实需要 SECURITY DEFINER 时(如绕过 RLS 查内部表):
- 函数放进未暴露 schema;
- 函数体内始终做 `auth.uid()` 检查;
- 从不该直接调用的角色收回
EXECUTE; - 改完跑
db advisors。
create or replace function private.is_team_member(team_id bigint)
returns boolean
language sql
security definer
set search_path = ''
as $$
select exists (
select 1 from public.team_members
where team_id = $1 and user_id = (select auth.uid()) -- 始终校验调用者身份
);
$$;
revoke execute on function private.is_team_member(bigint) from PUBLIC, anon, authenticated, service_role;---
4. Storage 访问控制
Storage upsert 需要 INSERT + SELECT + UPDATE 三个权限
只给 INSERT 能新上传,但覆盖上传(upsert)会静默失败。三个权限都要给。
---
5. 依赖与供应链安全
固定依赖版本并提交 lockfile
安装 Supabase 相关包(@supabase/supabase-js、@supabase/ssr、supabase-py 等)时固定版本、提交 lockfile。
---
兜底
上面没覆盖的安全问题,查 Supabase 官方产品安全索引: https://supabase.com/docs/guides/security/product-security.md,以及火山引擎 Database 文档。
---
Attribution:本文改编自 Supabase 官方 agent skills(supabase/agent-skills,MIT License,Copyright © Supabase),翻译为中文并适配火山引擎 Supabase 版。</content>