Supabase → Mortar 迁移手册
状态:cookbook(文档)。不提供自动化 import 工具——pg_dump 已经是好工具,迁移更多是配置 + DNS 切换问题,不是数据问题。
适用人群:现在跑在 Supabase 上的 China-market RN 应用,被 Supabase 中国大陆访问稳定性 + 海外结算问题逼着迁移。
一键评估
# Mortar CLI 自带 evaluator(规划中;现在手动跑):
mortar migrate evaluate --supabase-url=$SUPABASE_URL --supabase-key=$SUPABASE_KEY
# 输出:项目里用了哪些 Supabase feature + 对应 Mortar 等价物 + 风险点
Feature 映射表
| Supabase | Mortar 等价 | 难度 |
|---|---|---|
| Database (PostgREST API) | mortar.from(...) (@mortar/client) | 易 — 1:1 |
| Auth (邮箱 / OAuth) | mortar.auth | 易 — 邮箱 OK,OAuth 各家 provider 自己接 |
| Storage (S3-compatible) | mortar.storage | 易 — API 形状一致,buckets/keys 直接搬 |
| Realtime (postgres_changes / broadcast / presence) | mortar.realtime (subscribe / channel().on('broadcast') / channel().track()) | 易 — 三件套齐全,channel 名见下方坑 |
| Edge Functions | mortar.compute | 中 — Deno → Node/FC 重新写 |
| Vector (pgvector) | 同上 (pgvector) | 易 — 同扩展 |
| Cron | 规划中 | 暂时用 Mortar compute + 第三方 cron 调度 |
| GraphQL | ❌ 不支持 | Mortar 走 REST + mortar.from(table) row API;Supabase 用 pg_graphql 的项目需要迁到 REST |
| Triggers / Webhooks | Postgres triggers + mortar.compute | 中 |
| Branching | 规划中 | 暂时手动 pg_dump |
| Stripe billing | WeChat Pay / Alipay | 不一样 — 大陆客户走中国支付 |
数据迁移:三步法
1. 拷数据库
# 从 Supabase pull
pg_dump $SUPABASE_DB_URL \
--no-owner --no-acl --clean --if-exists \
-f supabase.sql
# Push 到 Mortar
psql $MORTAR_DB_URL -f supabase.sql
注意:
- Supabase 用
auth.usersschema 存用户;Mortar 用app_users表。 自己写一个 INSERT SELECT 把字段映射过来。Hash 算法都是 bcrypt, 直接搬可用,不用让用户重设密码。 - Supabase 的
storage.objects表是元数据;真实文件在 S3-兼容 bucket 里,用aws s3 sync或rclone把对象拉到本地再 push 到 OSS。 - RLS:两边隔离模型不同。Supabase 给每个 project 一个独立数据库,
租户隔离靠物理分库,库内再用 per-user RLS(
auth.uid())。Mortar 是 共享库 +tenant_id列 + RLS:隔离是平台自动加的 tenant_id 级 policy(tenant_id = auth.tenant_id(),auth.tenant_id()是 Mortar 版的 Supabaseauth.uid(),其值就是 project 的 UUID),不是你自己 per-table 写的。所以 Supabase 里基于auth.uid() = user_id的 per-user policy 不能逐字搬——Mortar V0 数据走通用app_rowsJSONB 模型,RLS 粒度是 project 的 tenant_id 而非 end-user。把原来的行级授权 意图挪到应用层(end-user JWT + 专用 endpoint),tenant_id 隔离由平台 兜底。
2. 拷存储对象
# Supabase 用 S3-compatible (内部走 R2); 用 rclone
rclone copy \
:s3,provider=Other,access_key_id=X,secret_access_key=Y,endpoint=https://supa.r2.cloudflarestorage.com/:bucket \
:s3,provider=Aliyun,access_key_id=X,secret_access_key=Y,endpoint=https://oss-cn-hangzhou.aliyuncs.com:keel-bundles/
3. DNS 切换 + 客户端 SDK 替换
// Before
import { createClient } from '@supabase/supabase-js';
const supa = createClient(SUPABASE_URL, SUPABASE_KEY);
const { data } = await supa.from('users').select('*');
// After
import { createClient } from '@mortar/client';
// tenant 在 host 子域名里(Supabase 的 <ref>.supabase.co 模型)——没有单独的 tenant 选项
const m = createClient({ url: 'https://<tenant>.api.mortar.appunvs.com', apiKey: '...' });
const { data } = await m.from('users').select('*');
@mortar/client 表面故意跟 supabase-js 接近——.from(table).select() / .insert() / .update() / .delete() / .eq() / .gte() 等链式 API 都
一样。
常见坑
1. Realtime channel 命名 + 收发分离
- 行变更:Supabase 用
postgres_changes:public:tablename;Mortar 用mortar.realtime.subscribe({table})(订阅一张表的行变更)。 - broadcast / presence:Supabase 是
channel.on('broadcast'…)+channel.track(),全在一条 WebSocket 上双向收发。Mortar 的mortar.realtime.channel(name)API 形状一致,但底层是 SSE 收 + HTTP POST 发(Mortar 不用 WebSocket)——对调用方透明,行为等价。迁移时channel.send({type:'broadcast',…})/track()/presenceState()基本照搬即可。
2. Auth provider list
Supabase 内置 20+ OAuth provider;Mortar 目前只内置 邮箱 + 微信 + Apple Sign-In。需要 Google / Facebook / GitHub 的:
- 用
@keel-ai/auth-session在客户端跑 PKCE flow - 把拿到的 OAuth code POST 到 Mortar
auth/oauth/callback自己存
内置 OAuth 编排在规划中。
3. Edge Function 运行时
Supabase Edge Functions = Deno on Cloudflare Workers.
Mortar compute = Aliyun FC (Node 20) / Tencent SCF (Node 16/18).
需要:
- Deno-specific import (e.g.
https://deno.land/x/*) 改成 npm Deno.env.get()→process.env.X- TypeScript 顶级 await — Node 20 也支持,OK 不动
4. 计费模型差异
Supabase 按月固定费 ($25/月起) + over-quota;Mortar 按用量 (¥99/月起)
- tier 上调。文档里
pricing.md详细对比。
真实迁移案例
下面是一个真实客户迁移 timeline(早期 dogfood 期一个 alpha 客户):
| 步骤 | 时间 | 备注 |
|---|---|---|
| Schema dump + restore | 30 min | 12 张表 5 GB |
| 文件对象 sync | 4 h | 80 GB OSS upload |
| SDK 替换 | 半天 | 3000 行 supabase-js 调用 → @mortar/client,主要是 channel 名 |
| Realtime cutover | 30 min | DNS TTL + 等 5 min |
| 灰度 (10% → 50% → 100%) | 一周 | 中间没事故 |
| 总用时 | 8 工作日 | 一个工程师 |
反向迁移
Mortar → Supabase 也支持(脱钩责任)。 数据用 pg_dump,元数据用
Mortar mortar export(规划中)导出。没有 vendor lock-in。
后续会减少的坑
- Cron → Supabase 等价物
- Database branches → 跟 Supabase 同样 git-style