# Supabase Region Migration

> 把 Supabase 项目迁移到另一个区域（如 ca-central-1 → ap-northeast-1），全程不需要数据库密码。适用于「项目延迟高、想换区域」「国内访问慢」等场景。Supabase 不支持原地改区域，只能新建项目 + 迁数据。

- Skill: `paloma333/supabase-region-migration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paloma333/supabase-region-migration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paloma333/supabase-region-migration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Paloma333 (https://skillmd.com/u/paloma333)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/paloma333/supabase-region-migration

---


# Supabase 跨区域迁移

## 核心认知（先确认，别走弯路）

Supabase **不支持原地改区域**——项目在基础设施层绑定区域。官方原文：
"Each Supabase project is provisioned on hardware in the chosen region, so it is bound
to a region at the infrastructure level."

所以迁移永远是：**目标区新建项目 → 迁 schema → 迁数据 → 改指向**。

## 为什么不需要数据库密码

传统方案用 `pg_dump`，需要数据库密码。但本项目验证过一条更好的路：

| 环节 | 传统做法 | 本方案 |
|---|---|---|
| schema | pg_dump（需密码 + Docker） | `supabase db push`（用 migrations，密码自建） |
| 数据 | pg_dump / pg_restore | PostgREST + service_role（绕过 RLS） |
| auth 用户 | 导出 users 表 | Admin API 指定 UUID 重建 |

新项目的 `db_pass` 由我们在创建时指定，所以密码始终掌握在自己手里。

## 流程

### 0. 前置检查
```bash
supabase projects list              # 确认源项目 ref / 当前区域 / 登录态
supabase --version                  # CLI ≥ 2.x
```
记下 `organization_id`（第二列 ORG ID）。

### 1. 在目标区域创建项目
```bash
TOKEN=$(cat ~/.supabase/access-token)
curl -X POST "https://api.supabase.com/v1/projects" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"organization_id":"<org>","name":"HIM-Tokyo","region":"ap-northeast-1","db_pass":"<自设>","plan":"free"}'
```
⚠️ 密码**不要以 `!` 结尾**——shell 会做历史扩展吃掉它。后续连接一律让 node
直接读密码文件 + `encodeURIComponent`，别用 shell 变量传。

常用区域：`ap-northeast-1`（东京）、`ap-southeast-1`（新加坡）、`ca-central-1`。

### 2. 推 schema（走 pooler，别走直连）
```bash
supabase db push --yes --db-url \
  "postgresql://postgres.<NEW_REF>:<PW>@aws-0-<REGION>.pooler.supabase.com:5432/postgres"
```
- ⚠️ **必须加 `--yes`**，否则卡在交互确认（表现为空日志 + 进程 SIGKILL 137）
- ⚠️ 新项目初期 `db.<ref>.supabase.co` 直连 DNS 未生效（ENOTFOUND），**用 pooler**
- pooler 5432 = session 模式（DDL 用这个）；6543 = transaction 模式

### 3. 建 storage bucket
先查源项目有哪些 bucket：
```bash
curl -s "$SRC_URL/storage/v1/bucket" -H "Authorization: Bearer $SRC_SERVICE_ROLE_KEY"
```
再在目标项目同参数创建（id / public / file_size_limit / allowed_mime_types）。

### 4. 迁 auth 用户（必须先做）
`public.users.user_id` 外键引用 `auth.users.id`，不先建 auth 用户会导致后续全表级联失败。

**Admin API 支持指定 UUID 建用户**（已验证），这是保住数据链的关键：
```bash
POST /auth/v1/admin/users
{ "id":"<原 UUID>", "email":"...", "email_confirm":true, "password":"<随机>", "user_metadata":{...} }
```
密码哈希无法导出，建完用 `generate_link` 生成重置链接：
```bash
POST /auth/v1/admin/generate_link   { "type":"recovery", "email":"..." }
```

### 5. 迁数据
用 `scripts/migrate_supabase_region.mjs`（本项目内，支持 `--dry` 预演）：
```bash
export SRC_URL=... SRC_KEY=<源 service_role>
export TGT_URL=... TGT_KEY=<目标 service_role>
node scripts/migrate_supabase_region.mjs --dry    # 先预演
node scripts/migrate_supabase_region.mjs          # 再实跑
```
按外键顺序：categories → users → households → items → inventory_events → ...

### 6. 对齐分类 UUID（高频坑）
migration 预置的种子分类与源库**同名不同 UUID**，`items.category_id` 引用源 UUID
会撞外键。解法：清空目标库 categories，用源库数据重建以对齐 UUID。
（此时目标库无引用，安全。注意主键是 `category_id` 不是 `id`。）

### 7. 同步 auth 配置
```bash
curl -X PATCH "https://api.supabase.com/v1/projects/<NEW_REF>/config/auth" \
  -d '{"site_url":"...","uri_allow_list":"..."}'
```
⚠️ `uri_allow_list` 是**逗号分隔字符串**，传数组只会存第一个。

### 8. 切环境变量 + 同步远端
改 `.env.local` 前先备份。远端（Vercel / EdgeOne）也要同步，否则线上仍连旧库。

## 验证
逐表对比源/目标行数，用 `Prefer: count=exact` 读 `content-range` 的总数。

## 迁移后须知
- 所有用户会话失效（新 JWT secret），密码需重设
- storage 文件不随数据库迁移，需另行 `rclone`/`aws s3 sync`
- Edge Functions、Cron、SMTP 需手动重建
- 免费版注意活跃项目数上限（INACTIVE 项目不占额度）

