Skip to content
Platform

使用命令行进行备份和恢复

Learn how to backup and restore projects using the Supabase CLI

迁移数据库 #

🌐 Migrating the database

使用命令行备份数据库 #

🌐 Back up database using the CLI

1
Install the Supabase CLI

安装 Supabase CLI。

2
Install Docker Desktop

为你的平台安装 Docker Desktop。

3
Get the new database connection string

在你的项目仪表板上,点击 连接。

会话池连接字符串:

postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres

直接连接字符串:

postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.com:5432/postgres
4
Get the database password

在数据库设置中重置密码。

用数据库密码替换连接字符串中的[YOUR-PASSWORD]。

5
Backup database

在运行这些命令之前,请将 [CONNECTION_STRING] 替换为上一步中的连接字符串:

supabase db dump --db-url [CONNECTION_STRING] -f roles.sql --role-only
supabase db dump --db-url [CONNECTION_STRING] -f schema.sql
supabase db dump --db-url [CONNECTION_STRING] -f data.sql --use-copy --data-only -x "storage.buckets_vectors" -x "storage.vector_indexes"

在你开始之前 #

🌐 Before you begin

使用命令行恢复备份 #

🌐 Restore backup using CLI

1
Create project

创建一个new project

2
Configure newly created project

在新项目中:

  • 如果旧数据库中使用了 Webhooks,请启用 数据库 Webhooks。
  • 如果旧数据库中使用了任何非默认扩展,请启用 扩展。
3
Get the new database connection string

前往连接面板获取连接字符串。

会话池连接字符串:

postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres

直接连接字符串:

postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.com:5432/postgres
4
Get the database password

在连接字符串中将 [YOUR-PASSWORD] 替换为数据库密码。如果你不记得密码,可以在仪表板的 数据库 > 设置 页面重置。

5
Restore your Project with PSQL

在运行这些命令之前,把 [CONNECTION_STRING] 替换成你在前面步骤中得到的连接字符串:

psql \
--single-transaction \
--variable ON_ERROR_STOP=1 \
--file roles.sql \
--file schema.sql \
--command 'SET session_replication_role = replica' \
--file data.sql \
--dbname [CONNECTION_STRING]
6
Reactivate Database publications

如果在旧数据库中使用了 Supabase Realtime 的复制,请在仪表板的 数据库 > 发布 部分对必要的表启用发布。

特别注意事项 #

🌐 Special considerations

保存迁移历史 #

🌐 Preserving migration history

如果你以前使用 Supabase CLI 来管理旧数据库的迁移,并且想在新恢复的项目中保留迁移历史,你需要使用以下命令单独插入迁移记录。

🌐 If you were using Supabase CLI for managing migrations on your old database and would like to preserve the migration history in your newly restored project, you need to insert the migration records separately using the following commands.

supabase db dump --db-url "$OLD_DB_URL" -f history_schema.sql --schema supabase_migrations
supabase db dump --db-url "$OLD_DB_URL" -f history_data.sql --use-copy --data-only --schema supabase_migrations
psql \
--single-transaction \
--variable ON_ERROR_STOP=1 \
--file history_schema.sql \
--file history_data.sql \
--dbname "$NEW_DB_URL"

对 auth 和 storage#

🌐 Schema changes to auth and storage

如果你在旧项目中修改了 auth 和 storage 模式,比如添加了触发器或行级安全 (RLS) 策略,你需要单独恢复它们。Supabase CLI 可以帮你对比旧项目和你本地的迁移。具体命令取决于你项目使用的 diff 引擎。

🌐 If you have modified the auth and storage schemas in your old project, such as adding triggers or Row Level Security (RLS) policies, you have to restore them separately. The Supabase CLI can help you diff the old project against your local migrations. The command depends on your project's diff engine.

你的项目使用哪种差异引擎取决于 config.toml。如果它包含一个带有 enabled = true 的 [experimental.pgdelta] 部分,你的项目将使用 pg-delta。如果该部分缺失或 enabled 是 false,你的项目则会使用传统的 migra 引擎。查看 差异引擎 了解两者的区别以及如何切换。

🌐 Which diff engine your project uses depends on config.toml. If it contains an [experimental.pgdelta] section with enabled = true, your project uses pg-delta. If the section is missing or enabled is false, your project uses the legacy migra engine. See Diff engines for how the two differ and how to switch.

在 pg-delta 上,普通差异会包括你在托管模式中的自定义内容:

🌐 On pg-delta, a plain diff includes your customizations in the managed schemas:

supabase link --project-ref "$OLD_PROJECT_REF"
supabase db diff --linked > changes.sql

在 pg-delta 引擎下,diff 会排除 Supabase 管理的 schema 中的平台对象,比如 auth 和 storage。它会捕捉你在这些对象上的自定义设置,比如管理表上的触发器、auth 中任何表的 RLS 策略,以及 storage.objects、storage.buckets 和 realtime.messages 上的 RLS 策略。只有当触发器的函数位于管理的 schema 之外时,它才算是你的触发器(例如,auth.users 上的触发器调用 public 中的函数)。如果触发器的函数位于 auth 或 storage 中,即使你创建了这个函数,它也会被排除。diff 还会跳过你在这些 schema 中创建的其他对象,比如你自己的函数或索引。

🌐 Under the pg-delta engine, the diff excludes platform objects in Supabase-managed schemas such as auth and storage. It captures your customizations on top of them, such as triggers on managed tables, RLS policies on any table in auth, and RLS policies on storage.objects, storage.buckets, and realtime.messages. A trigger counts as yours only when its function lives outside the managed schemas (for example, a trigger on auth.users calling a function in public). A trigger whose function lives inside auth or storage is excluded, even if you created the function. The diff also skips other objects you create inside these schemas, such as your own functions or indexes.

在老旧的 migra 引擎上,明确指定模式名称:

🌐 On the legacy migra engine, name the schemas explicitly:

supabase link --project-ref "$OLD_PROJECT_REF"
supabase db diff --linked --schema auth,storage > changes.sql

在新项目中通过你自己的迁移文件或通过仪表板重新创建那些对象。

🌐 Recreate those objects on the new project from your own migration files or through the Dashboard.

故障排查注意 #

🌐 Troubleshooting notes

恢复期间禁用触发器: #

🌐 Disabling triggers during restore:

将 session_replication_role 设置为 replica 会在迁移过程中禁用触发器,防止列被重复加密。

🌐 Setting session_replication_role to replica disables triggers during the migration, preventing columns from being double encrypted.

自定义角色需要密码 #

🌐 Custom roles require passwords

如果你创建了任何带有 LOGIN 属性的 自定义角色,你必须在新项目中手动设置它们的密码。这可以通过 SQL 命令来完成:

🌐 If you created any custom roles with the LOGIN attribute, you must manually set their passwords in the new project. This can be done with the SQL command:

alter user "YOUR_USER" with password 'SOME_NEW_PASSWORD';

supabase_admin#

🌐 supabase_admin permission errors

如果在恢复过程中遇到与 supabase_admin 相关的权限错误:

🌐 If you encounter permission errors related to supabase_admin during restore:

  • 打开 schema.sql
  • 注释掉包含以下内容的任何行:
ALTER ... OWNER TO "supabase_admin"

cli_login_postgres#

🌐 cli_login_postgres role grant error

如果你遇到这个错误:

🌐 If you encounter the error:

ERROR: permission denied to grant role "postgres"
DETAIL: Only roles with the ADMIN option on role "postgres" may grant this role.
  • 打开 roles.sql
  • 把这一行注释掉:
GRANT "postgres" TO "cli_login_postgres" WITH INHERIT FALSE GRANTED BY "supabase_admin";

克隆后cli_login_postgres角色问题#

🌐 cli_login_postgres role issues after cloning

cli_login_role 必须由 supabase_admin 角色创建。如果在 CLI 能生成自己的版本之前迁移过程克隆了该角色,可能会遇到以下错误:

🌐 The cli_login_role must be created by the supabase_admin role. If the migration process cloned over the role before the CLI could generate its own version, it may encounter the error:

"message":"Failed to create login role:
ERROR: 0LP01: role "postgres" is a member of role "cli_login_postgres"

要解决这个问题,先删除自定义的 cli_login_postgres 角色。然后 CLI 就可以用正确的权限重新创建它:

🌐 To resolve the issue, drop the custom cli_login_postgres role. Then the CLI can recreate it with the right privileges:

DROP ROLE IF EXISTS cli_login_postgres;

迁移边缘函数 #

🌐 Migrating edge functions

步骤(使用 Supabase CLI): #

🌐 Steps (using the Supabase CLI):

1
Sign in to your Supabase Account

使用 Supabase CLI Supabase CLI,运行:

supabase login
2
List your edge functions
supabase functions list --project-ref your_project_ref
3
Download your functions

You can download an individual function with the following command:

supabase functions download YOUR_FUNCTION_NAME --project-ref your_project_ref
4
Deploy the functions
supabase functions deploy --project-ref your_target_project_ref

This deploys all functions within the supabase/functions to the target project. You can confirm by checking your Edge Functions on the project dashboard

步骤(使用 Supabase 控制面板): #

🌐 Steps (using the Supabase Dashboard):

1

在源项目中,从侧边菜单进入 Edge Functions

2

使用 Download 按钮,将你想要的功能下载为 zip 文件:下载 Edge Function

3

在目标项目中,从侧边菜单进入 Edge Functions

4

点击 Deploy a new function 按钮,选择 通过编辑器 操作

5

将你下载的功能(第 2 步的 zip 文件)拖放到编辑器中

6

添加你的函数名称,然后点击 Deploy function 按钮部署函数:上传 Edge Function

迁移存储对象 #

🌐 Migrating storage objects

1
On your machine, create a javascript repository

使用你喜欢的 JavaScript 包管理器,创建一个包含 supabase 客户端包的新项目

npm init -y
npm install @supabase/supabase-js
2
Create an index.js file in your Node.js project

把示例脚本加进去。

index.js
// npm install @supabase/supabase-js@2
const { createClient } = require('@supabase/supabase-js')
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
const oldSupabase = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY)
const newSupabase = createClient(NEW_PROJECT_URL, NEW_PROJECT_SERVICE_KEY)
function createLoadingAnimation(message) {
const readline = require('readline')
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
let i = 0
let timer
let stopped = false
const animate = () => {
if (stopped) return
process.stdout.write(`\r${frames[i]} ${message}`)
i = (i + 1) % frames.length
timer = setTimeout(animate, 80)
}
animate()
return {
stop: (finalMessage = '') => {
stopped = true
clearTimeout(timer)
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
process.stdout.write(`✓ ${finalMessage || message}\n`)
},
}
}
/**
* Lists all files in a bucket, handling nested folders recursively.
*/
async function listAllFiles(bucket, path = '') {
const loader = createLoadingAnimation(`Listing files in '${bucket}${path ? '/' + path : ''}'...`)
try {
const { data, error } = await oldSupabase.storage.from(bucket).list(path, { limit: 1000 })
if (error) {
loader.stop(`Error listing files in '${bucket}${path ? '/' + path : ''}'`)
throw new Error(`❌ Error listing files in bucket '${bucket}': ${error.message}`)
}
if (!data || data.length === 0) {
loader.stop(`No files found in '${bucket}${path ? '/' + path : ''}'`)
return []
}
let files = []
for (const item of data) {
if (!item.metadata) {
loader.stop(`Found folder '${item.name}' in '${bucket}${path ? '/' + path : ''}'`)
const subFiles = await listAllFiles(bucket, `${path}${item.name}/`)
files = files.concat(subFiles)
} else {
files.push({ fullPath: `${path}${item.name}`, metadata: item.metadata })
}
}
loader.stop(`Found ${files.length} files in '${bucket}${path ? '/' + path : ''}'`)
return files
} catch (error) {
loader.stop()
throw error
}
}
/**
* Creates a bucket in the new Supabase project if it doesn't exist.
*/
async function ensureBucketExists(bucketName, options = {}) {
const { data: existingBucket, error: getBucketError } =
await newSupabase.storage.getBucket(bucketName)
if (getBucketError && !getBucketError.message.includes('not found')) {
throw new Error(`❌ Error checking if bucket '${bucketName}' exists: ${getBucketError.message}`)
}
if (!existingBucket) {
console.log(`🪣 Creating bucket '${bucketName}' in new project...`)
const { error } = await newSupabase.storage.createBucket(bucketName, options)
if (error) throw new Error(`❌ Failed to create bucket '${bucketName}': ${error.message}`)
console.log(`✅ Created bucket '${bucketName}'`)
} else {
console.log(`ℹ️ Bucket '${bucketName}' already exists in new project`)
}
}
/**
* Migrates a single file from the old project to the new one.
*/
async function migrateFile(sourceBucketName, targetBucketName, file) {
const loader = createLoadingAnimation(
`Migrating ${file.fullPath} in bucket '${sourceBucketName}' to '${targetBucketName}'...`
)
try {
const { data, error: downloadError } = await oldSupabase.storage
.from(sourceBucketName)
.download(file.fullPath)
if (downloadError) {
loader.stop(`Failed to migrate ${file.fullPath}: Download error`)
throw new Error(`Download failed: ${downloadError.message}`)
}
// Preserve all available metadata from the original file
const uploadOptions = {
upsert: true,
contentType: file.metadata?.mimetype,
cacheControl: file.metadata?.cacheControl,
}
const { error: uploadError } = await newSupabase.storage
.from(targetBucketName)
.upload(file.fullPath, data, uploadOptions)
if (uploadError) {
loader.stop(`Failed to migrate ${file.fullPath}: Upload error`)
throw new Error(`Upload failed: ${uploadError.message}`)
}
loader.stop(
`Migrated ${file.fullPath} in bucket '${sourceBucketName}' to '${targetBucketName}'`
)
return { success: true, path: file.fullPath }
} catch (err) {
console.error(
`❌ Error migrating ${file.fullPath} in bucket '${targetBucketName}':`,
err.message
)
return { success: false, path: file.fullPath, error: err.message }
}
}
function chunkArray(array, size) {
const chunks = []
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size))
}
return chunks
}
/**
* Migrates all buckets and files from the old Supabase project to the new one.
* Processes files in parallel within batches for efficiency.
*/
async function migrateBuckets() {
console.log('🔄 Starting Supabase Storage migration...')
console.log(`📦 Source project: ${OLD_PROJECT_URL}`)
console.log(`📦 Target project: ${NEW_PROJECT_URL}`)
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
})
console.log(
'\n⚠️ WARNING: This migration may overwrite files in the target project if they have the same paths.'
)
console.log('⚠️ It is recommended to back up your target project before proceeding.')
const answer = await new Promise((resolve) => {
readline.question('Do you want to proceed with the migration? (yes/no): ', resolve)
})
readline.close()
if (answer.toLowerCase() !== 'yes') {
console.log('Migration canceled by user.')
return { canceled: true }
}
console.log('\n📦 Fetching all buckets from old project...')
const { data: oldBuckets, error: bucketListError } = await oldSupabase.storage.listBuckets()
if (bucketListError) throw new Error(`❌ Error fetching buckets: ${bucketListError.message}`)
console.log(`✅ Found ${oldBuckets.length} buckets to migrate.`)
const { data: existingBuckets, error: existingBucketsError } =
await newSupabase.storage.listBuckets()
if (existingBucketsError)
throw new Error(`❌ Error fetching existing buckets: ${existingBucketsError.message}`)
const existingBucketNames = existingBuckets.map((b) => b.name)
const conflictingBuckets = oldBuckets.filter((b) => existingBucketNames.includes(b.name))
let conflictStrategy = 2
if (conflictingBuckets.length > 0) {
console.log('\n⚠️ The following buckets already exist in the target project:')
conflictingBuckets.forEach((b) => console.log(` - ${b.name}`))
const conflictAnswer = await new Promise((resolve) => {
const rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
})
rl.question(
'\nHow do you want to handle existing buckets?\n' +
'1. Skip existing buckets\n' +
'2. Merge files (may overwrite existing files)\n' +
'3. Rename buckets in target (add suffix "_migrated")\n' +
'4. Cancel migration\n' +
'Enter your choice (1-4): ',
(answer) => {
rl.close()
resolve(answer)
}
)
})
if (conflictAnswer === '4') {
console.log('Migration canceled by user.')
return { canceled: true }
}
conflictStrategy = parseInt(conflictAnswer)
if (isNaN(conflictStrategy) || conflictStrategy < 1 || conflictStrategy > 3) {
console.log('Invalid choice. Migration canceled.')
return { canceled: true }
}
}
const migrationStats = {
totalBuckets: oldBuckets.length,
processedBuckets: 0,
skippedBuckets: 0,
totalFiles: 0,
successfulFiles: 0,
failedFiles: 0,
failedFilesList: [],
}
for (const bucket of oldBuckets) {
const bucketName = bucket.name
console.log(`\n📁 Processing bucket: ${bucketName}`)
let targetBucketName = bucketName
if (existingBucketNames.includes(bucketName)) {
if (conflictStrategy === 1) {
console.log(`⏩ Skipping bucket '${bucketName}' as it already exists in target project`)
migrationStats.skippedBuckets++
continue
} else if (conflictStrategy === 3) {
targetBucketName = `${bucketName}_migrated`
console.log(`🔄 Renaming bucket to '${targetBucketName}' in target project`)
} else {
console.log(`🔄 Merging files into existing bucket '${bucketName}' in target project`)
}
}
// Preserve bucket configuration when creating in the new project
if (targetBucketName !== bucketName || !existingBucketNames.includes(bucketName)) {
await ensureBucketExists(targetBucketName, {
public: bucket.public,
fileSizeLimit: bucket.file_size_limit,
allowedMimeTypes: bucket.allowed_mime_types,
})
}
const files = await listAllFiles(bucketName)
console.log(`✅ Found ${files.length} files in bucket '${bucketName}'.`)
migrationStats.totalFiles += files.length
const batches = chunkArray(files, 10)
for (let i = 0; i < batches.length; i++) {
console.log(`\n🚀 Processing batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
const results = await Promise.all(
batches[i].map((file) => migrateFile(bucketName, targetBucketName, file))
)
const batchSuccesses = results.filter((r) => r.success).length
const batchFailures = results.filter((r) => !r.success)
migrationStats.successfulFiles += batchSuccesses
migrationStats.failedFiles += batchFailures.length
migrationStats.failedFilesList.push(...batchFailures.map((f) => f.path))
console.log(
`✅ Completed batch ${i + 1}/${batches.length}: ${batchSuccesses} succeeded, ${batchFailures.length} failed`
)
}
migrationStats.processedBuckets++
console.log(`✅ Completed bucket '${bucketName}' migration`)
}
console.log('\n📊 Migration Summary:')
console.log(
`Buckets: ${migrationStats.processedBuckets}/${migrationStats.totalBuckets} processed, ${migrationStats.skippedBuckets} skipped`
)
console.log(
`Files: ${migrationStats.successfulFiles} succeeded, ${migrationStats.failedFiles} failed (${migrationStats.totalFiles} total)`
)
if (migrationStats.failedFiles > 0) {
console.log('\n⚠️ Failed files:')
migrationStats.failedFilesList.forEach((path) => console.log(` - ${path}`))
return migrationStats
}
return migrationStats
}
migrateBuckets()
.then((stats) => {
if (stats.failedFiles > 0) {
console.log(`\n⚠️ Migration completed with ${stats.failedFiles} failed files.`)
process.exit(1)
} else {
console.log('\n🎉 Migration completed successfully!')
process.exit(0)
}
})
.catch((err) => {
console.error('❌ Fatal error during migration:', err.message)
process.exit(1)
})
3
Add the relevant project variables to the script

获取你新项目和旧项目的秘密密钥或服务角色密钥,然后将它们替换到脚本中。
从数据 API 设置中复制你的项目 URL 并添加到脚本里。

'index.js'
//rest of code
...
// add relevant details for old project
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
// add relevant details for new project
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
...
//rest of code
4
Run the script from your command line
node index.js

资源 #

🌐 Resources