之前我用 Supabase 的方式很野。
基本就是:
- 去 Dashboard 里建表、改 SQL
- 去网页端贴 Edge Function
- 改完能跑就算结束
这种方式前期很爽,后期就会越来越乱。因为你会慢慢发现:
- 线上到底改了什么,仓库里没有真值
- 本地代码和线上函数会漂
- 下次想继续改,不知道该从哪里接手
- 想做 migration、回滚、review,都会很别扭
于是我决定把项目正式接回本地,用 Supabase CLI 来管理。
这篇文章记录的是一个非常具体的场景:
- 项目之前已经在线上跑起来了
- 数据库和函数很多都是在网页端改的
- 现在想把它们拉回本地,作为 Git 真值继续维护
如果你也是这个状态,这篇就够用了。
先说结论
我最后确认下来的最稳流程是:
- 安装 Node.js 和 Docker Desktop
- 用
npx supabase跑 CLI supabase link绑定线上项目supabase db pull拉远端 schema 到本地 migration- 如果是第一次接管旧项目,把这次 pull 下来的 schema 记成 baseline migration
supabase functions listsupabase functions download <name>把线上函数全部拉到本地- 从这一刻开始,以本地
supabase/目录作为唯一真值
一句话总结就是:
先把远端现状完整拉下来,再开始继续开发。
我这次的目标是什么
我当前项目是 AddrGO,线上项目已经存在,项目 ref 是:
njcmytzzgduawiepvpdy
我做这件事的目标不是“新建一个 Supabase 项目”,而是:
- 保留当前线上数据库
- 保留当前线上 Edge Functions
- 把线上当前状态拉回本地
- 后面继续通过 CLI 和 Git 去维护
这点一定要想清楚。
因为如果你的目标不同,比如你是要全新初始化项目,那流程会简单很多。
第一步:先把 CLI 跑起来
我没有先全局安装 supabase,而是直接用 npx:
npx supabase --help
这样有几个好处:
- 不污染全局环境
- 版本跟着项目走
- 换机器时也更方便
如果你要固定在项目里,也可以:
npm install -D supabase
然后继续用:
npx supabase ...
第二步:先安装 Docker Desktop
这个坑我一开始就踩了。
我第一次执行:
npx supabase db pull
结果直接报错,说连不上 Docker daemon。
后来才明白,db pull 这类数据库同步流程会依赖 Docker。
所以本地想认真用 Supabase CLI,Docker Desktop 基本是必备项。
安装:
brew install --cask docker
open -a Docker
验证:
docker ps
只要 docker ps 能正常返回,不管是空列表还是容器列表,都说明 Docker 基本起来了。
我在 Docker 这里还踩了一个旧环境坑
我的机器以前装过 Docker Desktop,于是 /usr/local/bin 里残留了很多旧链接,比如:
/usr/local/bin/hub-tool
/usr/local/bin/kubectl.docker
/usr/local/bin/docker-credential-ecr-login
/usr/local/bin/docker-index
/usr/local/bin/vpnkit
它们都还指向旧的:
/Applications/Docker.app/Contents/Resources/bin/...
结果 Homebrew 每次安装 Docker Desktop,装到一半就因为这些旧链接冲突而回滚。
解决方式就是把这些旧的软链接清掉,再重装 Docker Desktop。
所以如果你也遇到:
Error: It seems there is already a Binary at '/usr/local/bin/...'
先别慌,十有八九就是旧 Docker 残留。
第三步:登录并 link 到线上项目
先登录:
npx supabase login
然后列出你账号下的项目:
npx supabase projects list
我这次看到的是:
AddrGO -> njcmytzzgduawiepvpdy
StillHere -> rcxgkzmxywcpkhkvqvza
明确要用 AddrGO 之后,执行:
npx supabase link --project-ref njcmytzzgduawiepvpdy
这一步成功之后,本地仓库里的 supabase/.temp/project-ref 就会写入当前项目。
第四步:第一次接管旧项目,先做 db pull
这是最关键的一步:
npx supabase db pull
如果你之前一直在 Dashboard 里改数据库,那么这一步的意义就是:
- 把远端 schema 导出成一份本地 migration
- 让本地仓库第一次拥有“数据库结构真值”
我这次拉下来之后,最终生成的是:
supabase/migrations/20260313072854_remote_schema.sql
这个文件就是我当前线上数据库的 baseline。
第五步:如果第一次接管,要把 baseline 标记成已应用
这里我又踩了两个坑。
坑 1:上一次失败留下了空 migration 文件
我第一次 db pull 中途失败,结果在本地留下了一个空文件:
supabase/migrations/20260313071414_remote_schema.sql
它是 0 字节。
这会导致本地和远端 migration history 看起来不一致。
所以如果你碰到类似问题,先检查:
ls -l supabase/migrations
如果发现某个新 migration 文件是 0 字节,先删掉这个空文件,再重新 db pull。
坑 2:Update remote migration history table? [Y/n] 之后报 EOF
我这次 db pull 成功后,CLI 询问我:
Update remote migration history table? [Y/n]
我按了 Y,结果报:
failed to create migration table: unexpected EOF
后来开 --debug 看,真正原因不是 schema 拉取失败,而是:
password authentication failed for user "cli_login_postgres"
也就是说,数据库密码没配对。
注意,这里的数据库密码:
- 不是
anon key - 不是
service_role key - 不是 API Keys
它是数据库自己的 password。
如果你需要重新设置,可以先导出环境变量:
export SUPABASE_DB_PASSWORD='<你的数据库密码>'
然后重新 link:
npx supabase link --project-ref njcmytzzgduawiepvpdy
之后再把刚才那份 migration 标记为已应用:
npx supabase migration repair --linked --status applied 20260313072854
验证:
npx supabase migration list --linked
我最后看到的是:
Local | Remote | Time (UTC)
20260313072854 | 20260313072854 | 2026-03-13 07:28:54
看到这里,说明数据库 schema 的 CLI 接管已经完成。
第六步:把线上 Edge Functions 也拉回本地
数据库搞定后,接下来就是函数。
先看线上现在有哪些函数:
npx supabase functions list
我这次线上看到的是:
log-translationtranslate-addresscode-authissue-access-code-batchweb-historyweb-translate-address
然后把它们全部下载到本地:
npx supabase functions download translate-address
npx supabase functions download code-auth
npx supabase functions download issue-access-code-batch
npx supabase functions download web-history
npx supabase functions download web-translate-address
npx supabase functions download log-translation
下载后,本地目录就会变成:
supabase/functions/code-auth/index.ts
supabase/functions/issue-access-code-batch/index.ts
supabase/functions/log-translation/index.ts
supabase/functions/translate-address/index.ts
supabase/functions/web-history/index.ts
supabase/functions/web-translate-address/index.ts
到这一步,线上函数真值也正式回到了本地。
第七步:从这一刻开始,本地仓库就是唯一真值
这是最重要的认知切换。
之前我的习惯是:
- 本地改一点
- 网页端再补一点
- SQL Editor 再改一点
以后最好不要这样了。
一旦本地接管完成,后面就应该以仓库里的这些目录为真值:
supabase/migrations/
supabase/functions/
后续数据库改动:
npx supabase db push
后续函数部署:
npx supabase functions deploy <function-name>
如果你又忍不住回 Dashboard 里直接改了数据库或函数,那就必须立刻同步回来:
- 数据库改了:再做一次
db pull - 函数改了:立刻
functions download <name>
否则仓库和线上很快又会漂。
第八步:我给自己总结的一套最稳工作流
我后面准备固定按这套来:
1. 初始化一台新机器
brew install node
brew install --cask docker
open -a Docker
2. 项目初始化
cd <repo>
npx supabase login
npx supabase link --project-ref <project-ref>
3. 接管已有线上项目
npx supabase db pull
npx supabase migration repair --linked --status applied <timestamp>
npx supabase functions list
npx supabase functions download <function-name>
4. 日常开发
- 数据库结构变更:先本地 migration,再
db push - Edge Function 修改:改本地
supabase/functions/*,再 deploy - 全部内容进入 Git
最后说一句
我这次最深的感受其实不是“Supabase CLI 真难”,而是:
一旦项目开始正式跑起来,就一定要尽快收口真值。
前期在网页端改来改去,真的很爽。 但是一旦项目开始变复杂,函数一多、SQL 一多、环境一多,迟早会陷入一种状态:
“好像哪都能改,但其实已经没人知道真相到底在哪。”
所以这次接回本地,不只是工具切换,而是工程习惯切换。
从现在开始:
- 数据库真值在
supabase/migrations/ - 函数真值在
supabase/functions/ - 线上只是运行态,不再是唯一记忆体
这样后面再继续扩展,我心里就踏实多了。