# Sample Supabase Self-Hosted Migration Report

Fictional project: `Acme Cloud Exit`

This sample shows the report shape for one redacted Supabase Cloud to self-hosted migration packet. It is not a report about a real buyer, company, repository, database, or private app.

## Scope

Reviewed one redacted packet covering:

- Supabase Cloud to Docker self-hosted restore notes
- `roles.sql`, `schema.sql`, and `data.sql` restore evidence
- `auth.users`, JWT/session, OAuth redirect, and first-login notes
- Storage bucket and object-copy plan
- Docker `.env` runtime settings, generated API keys, SMTP, external URLs, reverse proxy, and TLS notes
- Data API grants, RLS, RPC/function reachability, and rollback plan

No dashboard access, source-code access, database connection, database URL, service-role key, JWT secret, SMTP secret, OAuth material, customer records, private screenshots, payment data, or live tool calls were reviewed.

## Executive Summary

The migration packet is not ready for production traffic. The database restore plan is close, but the packet does not yet prove Storage object transfer, first-login behavior after JWT key rotation, Docker runtime secret replacement, or a clean rollback path if the self-hosted stack fails after DNS cutover.

Recommended launch decision: **BLOCK public cutover until restore, auth, Storage, runtime, grants, and rollback evidence are captured in one redacted launch packet.**

## Findings

### High: `restore_files_missing_single_transaction_proof`

Evidence: the packet mentions `pg_dump` and a successful local import, but does not show separate `roles.sql`, `schema.sql`, and `data.sql` files or a final `psql --single-transaction --variable ON_ERROR_STOP=1` restore run.

Why it matters: Supabase's platform-to-self-hosted restore guide recommends separate role, schema, and data dumps using the Supabase CLI because raw `pg_dump` can include Supabase internals and restore-time permission problems.

Fix direction:

- Export `roles.sql`, `schema.sql`, and `data.sql` with `supabase db dump`.
- Run the restore on a disposable self-hosted instance before production.
- Record only non-sensitive file names, pass/fail state, Postgres version, and extension mismatch notes.

### High: `auth_session_cutover_gap`

Evidence: `auth.users` row counts are listed, but the packet does not show JWT secret behavior, OAuth redirect updates, or first-login proof after the switch.

Why it matters: user rows can be preserved while existing platform-issued tokens stop working. Social login provider callbacks also need to point at the self-hosted hostname.

Fix direction:

- Plan for users to re-authenticate after cutover.
- Confirm OAuth provider callback URLs for the self-hosted host.
- Test first login, password reset, and one profile load with non-sensitive test accounts.

### High: `storage_objects_not_transferred`

Evidence: the database restore includes Storage metadata, but the packet does not prove that bucket objects were copied or that public/signed URLs were rewritten and tested.

Why it matters: the database restore path does not copy Storage object contents. An app can pass table checks while images, uploads, signed downloads, or object overwrites fail after launch.

Fix direction:

- Copy Storage objects separately.
- Test upload, overwrite/upsert, list, download, delete, and wrong-path access.
- Record only redacted bucket names, path patterns, and pass/fail outcomes.

### High: `docker_runtime_secret_reuse`

Evidence: the packet lists copied `.env` keys but does not prove that `POSTGRES_PASSWORD`, JWT secrets, API keys, SMTP credentials, and external URLs were regenerated or reviewed for the self-hosted instance.

Why it matters: self-hosted Docker examples ship with placeholder values and require new secrets before production. Runtime URL mismatch can also break Auth, Storage, and API clients after DNS cutover.

Fix direction:

- Generate fresh self-hosted secrets and API keys.
- Confirm `API_EXTERNAL_URL`, `SITE_URL`, SMTP, reverse proxy, and TLS values.
- Keep actual secret values out of the review packet.

### Medium: `data_api_grants_role_matrix_missing`

Evidence: RLS policies are mentioned, but the packet does not show explicit Data API grants or owner/wrong-user role-matrix tests after the restore.

Why it matters: restored tables can have policies but still fail through the Data API when explicit grants are missing, or pass for the wrong reason when broad grants remain.

Fix direction:

- Capture narrow grants beside table migrations.
- Run owner, wrong-owner, anonymous, and no-session Data API tests.
- Include RPC/function reachability checks for browser-facing roles.

### Medium: `version_extension_mismatch_unresolved`

Evidence: the packet does not compare managed Supabase Postgres/Auth/Storage versions, enabled extensions, or restore-time missing table/column notes against the self-hosted stack.

Why it matters: managed projects can run newer Postgres/Auth/Storage versions than a default self-hosted image. Restore errors may hide in version-only differences.

Fix direction:

- Record Postgres major versions and active extensions.
- Note any edited `data.sql` lines for missing Auth or Storage tables/columns.
- Re-run a final restore after edits with errors treated as launch blockers.

## Minimum Cutover Matrix

| Scenario | Expected result |
| --- | --- |
| Disposable self-hosted restore | roles, schema, data, extensions, and row counts match expected redacted counts |
| Existing user after cutover | user re-authenticates and can load own dashboard/profile rows |
| Different user | denied for other user rows and Storage paths |
| Storage object read/write | upload, overwrite, list, download, and delete match expected bucket policy |
| OAuth callback | provider redirects to the self-hosted hostname |
| Data API grants | owner paths work, wrong-user paths fail, and `42501` errors are explained |
| Rollback | frontend/DNS can point back to the previous backend or maintenance mode |

## Suggested `migration.md` Launch Gate

```md
# migration.md

Status: BLOCK

Before cutover:
- export roles.sql, schema.sql, and data.sql with supabase db dump
- restore into disposable self-hosted stack with ON_ERROR_STOP=1
- copy Storage objects separately and test URL/path behavior
- regenerate self-hosted Docker secrets and API keys
- update OAuth redirects, SITE_URL, API_EXTERNAL_URL, SMTP, reverse proxy, and TLS
- run owner/wrong-owner/no-session role matrix

Fallback:
- pause writes
- keep prior frontend target ready
- preserve Cloud backup and DNS rollback notes
- monitor auth, storage, API, and database errors for 24 hours
```

## Buyer Action List

1. Re-run the free self-hosted migration trap checker with the updated redacted packet.
2. Add missing restore evidence and version/extension notes.
3. Prove auth first-login and OAuth callback behavior with test accounts.
4. Prove Storage object transfer plus upload/overwrite/download behavior.
5. Keep actual secrets, customer rows, private screenshots, and payment data out of the review packet.

## Safety Boundary

This report is a planning aid, not legal advice, compliance certification, penetration testing, incident response, managed hosting advice, or a guarantee that a migration is safe. It should receive only redacted packet text and non-sensitive pass/fail notes.
