# Restore a Platform Project to Self-Hosted

Restore your database from the Supabase platform to a self-hosted instance.

This guide walks you through restoring your database from a Supabase platform project to a [self-hosted Docker instance](/docs/guides/self-hosting/docker). Transferring storage objects or redeploying edge functions is not covered here.

## Before you begin

You need:

- A new self-hosted Supabase instance ([Docker setup guide](/docs/guides/self-hosting/docker))
- [Supabase CLI](/docs/guides/local-development/cli/getting-started) installed (or use `npx supabase`)
- [Docker Desktop](https://docs.docker.com/get-started/get-docker/) installed (required by the CLI)
- `psql` installed ([official installation guide](https://www.postgresql.org/download/))
- Your Supabase database passwords (for platform and self-hosted)

## Step 1: Get your platform connection string

On your managed Supabase project dashboard, click [**Connect**](/dashboard/project/_?showConnect=true) and copy the connection string (use the session pooler or direct connection).

## Step 2: Back up your platform database

Export roles, schema, and data as three separate SQL files:

```sh
supabase db dump --db-url "[CONNECTION_STRING]" -f roles.sql --role-only
```

```sh
supabase db dump --db-url "[CONNECTION_STRING]" -f schema.sql
```

```sh
supabase db dump --db-url "[CONNECTION_STRING]" -f data.sql --use-copy --data-only
```

This produces SQL files that are compatible across Postgres versions.

Using `supabase db dump` executes `pg_dump` under the hood but applies Supabase-specific filtering - it excludes internal schemas, strips reserved roles, and adds idempotent `IF NOT EXISTS` clauses. Using raw `pg_dump` directly will include Supabase internals and cause permission errors during restore. CLI requires Docker because it runs `pg_dump` inside a container from the Supabase Postgres image rather than requiring a local Postgres installation.

## Step 3: Prepare your self-hosted instance

Before restoring, check the following on your self-hosted instance:

- **Extensions**: Enable any non-default extensions your Supabase project uses. You can check which extensions are active by querying `select * from pg_extension;` on your managed database (or check Database Extensions in Dashboard).

## Step 4: Restore to your self-hosted database

Connect to your self-hosted Postgres and restore the dump files. The [default](/docs/guides/self-hosting/docker#accessing-postgres) connection string for self-hosted Supabase is:

```
postgres://postgres.your-tenant-id:[POSTGRES_PASSWORD]@[your-domain]:5432/postgres
```

Where `[POSTGRES_PASSWORD]` is the value of `POSTGRES_PASSWORD` in your self-hosted `.env` file.

Use your domain name, your server IP, or localhost for `[your-domain]` depending on whether you are running self-hosted Supabase on a VPS, or locally.

Run `psql` to restore:

```sh
psql \
  --single-transaction \
  --variable ON_ERROR_STOP=1 \
  --file roles.sql \
  --file schema.sql \
  --command 'SET session_replication_role = replica' \
  --file data.sql \
  --dbname "postgres://postgres.your-tenant-id:[POSTGRES_PASSWORD]@[your-domain]:5432/postgres"
```

Setting `session_replication_role` to `replica` disables triggers during the data import, preventing issues like double-encryption of columns.

## Step 5: Verify the restore

Connect to your self-hosted database and run a few checks:

```sh
psql "postgres://postgres.your-tenant-id:[POSTGRES_PASSWORD]@[your-domain]:5432/postgres"
```

```sql
-- Check your tables are present
\dt public.*

-- Verify row counts on key tables
SELECT count(*) FROM auth.users;

-- Check extensions
SELECT * FROM pg_extension;
```

## What's included in the restore and what's not

The database dump includes your schema, data, roles, RLS policies, database functions, triggers, and `auth.users`. However, several things require separate configuration on your self-hosted instance:

| Requires manual setup                       | How to configure                                  |
| ------------------------------------------- | ------------------------------------------------- |
| JWT secrets and API keys                    | Generate new ones and update `.env`               |
| Auth provider settings (OAuth, Apple, etc.) | Configure `GOTRUE_EXTERNAL_*` variables in `.env` |
| Edge functions                              | Manually copy to your self-hosted instance        |
| Storage objects                             | Transfer separately (not covered in this guide)   |
| SMTP / email settings                       | Configure `SMTP_*` variables in `.env`            |
| Custom domains and DNS                      | Point your DNS to the self-hosted server          |

## Auth considerations

Your `auth.users` table and related data are included in the database dump, so user accounts are preserved. However:

- **JWT secrets differ** between your platform and self-hosted instances. Existing tokens issued by the platform project will not be valid. Users will need to re-authenticate.
- **Social auth providers** (Apple, Google, GitHub, etc.) need to be configured in your self-hosted `.env` file. Set the relevant `GOTRUE_EXTERNAL_*` variables. See the Auth repository [README](https://github.com/supabase/auth) for all available options.
- **Redirect URLs** in your OAuth provider consoles (Apple Developer, Google Cloud Console, etc.) must be updated to point to your self-hosted hostname instead of `*.supabase.co`.

## Postgres version compatibility

Managed Supabase may run a newer Postgres version (Postgres 17) than the self-hosted Docker image (currently it's Postgres 15 by default). The `supabase db dump` command produces plain SQL files that work across major Postgres versions.

If your managed project runs Postgres 17, consider starting your self-hosted deployment with Postgres 17 as well. See the [Postgres 17 guide](/docs/guides/self-hosting/postgres-upgrade-17) for setup instructions.

Keep in mind:

- The data dump may include Postgres 17-only settings or reference tables/columns from newer Auth and Storage versions that don't exist on self-hosted yet. See [Version mismatches](#version-mismatches-between-platform-and-self-hosted) in the troubleshooting section.
- Run the restore on a test self-hosted instance first to identify any incompatibilities.
- Check that all extensions you use are available on the self-hosted Postgres version.

## Troubleshooting

### Version mismatches between platform and self-hosted

The platform may run a newer Postgres version (17 vs 15) and newer Auth service versions than self-hosted. The data dump can contain settings, tables, or columns that don't exist on your new self-hosted instance.

**Common issues in `data.sql`:**

- `SET transaction_timeout = 0` - a Postgres 17-only setting that fails on Postgres 15
- `COPY` statements for tables that don't exist on self-hosted (e.g., `auth.oauth_clients`, `storage.buckets_vectors`, `storage.vector_indexes`)
- `COPY` statements with columns added in newer Auth versions (e.g., `auth.flow_state` with `oauth_client_state_id`, `linking_target_id`)

**Workaround:** Edit `data.sql` before restoring:

```sh
# Comment out PG17-only transaction_timeout
sed -i 's/^SET transaction_timeout/-- &/' data.sql
```

For missing tables or column mismatches, comment out the relevant `COPY ... FROM stdin;` line and its corresponding `\.` terminator. Run the restore without `--single-transaction` first to identify all failures, then fix them and run the final restore with `--single-transaction`.

Keeping your self-hosted configuration [up to date](https://github.com/supabase/supabase/blob/master/docker/CHANGELOG.md) will minimize these gaps.

### Extension not available

If the restore fails because an extension isn't available, check whether it's supported on your self-hosted Postgres version. You can list available extensions with:

```sql
select * from pg_available_extensions;
```

### Connection refused

Make sure your self-hosted Postgres port is accessible. In the default [self-hosted Supabase](/docs/guides/self-hosting/docker#accessing-postgres) setup, the user is `postgres.your-tenant-id` with Supavisor on port `5432`.

### Legacy Studio configuration

Studio in self-hosted Supabase historically used `supabase_admin` role (superuser) instead of `postgres`. Objects created via Studio UI were owned by `supabase_admin`. Check your `docker-compose.yml` [configuration](https://github.com/supabase/supabase/blob/2cb5befaa377a42b6d6ca152b98105b59054f2f4/docker/docker-compose.yml#L30) to see if `POSTGRES_USER_READ_WRITE` is set to `postgres`.

### Custom roles missing passwords

If you created custom database roles with the `LOGIN` attribute on your platform project, their passwords are not included in the dump. Set them manually after restore:

```sql
ALTER ROLE your_custom_role WITH PASSWORD 'new-password';
```

### Additional resources

- [Backup and Restore using the CLI](/docs/guides/platform/migrating-within-supabase/backup-restore)
- [Restore Dashboard backup](/docs/guides/platform/migrating-within-supabase/dashboard-restore)