Skip to content

Upgrade a Supabase project to a new Postgres major version, end to end

Upgrade a Supabase project to a new Postgres major version (e.g. 15 -> 17) with minimal risk, in two tracks:

  1. Rehearse on a throwaway clone made with Restore to a New Project, upgraded to the target version, validated with your app.
  2. Run the real thing as an in-place upgrade on production - same project, same URL, same keys, zero reconfiguration.

A third option exists for the large-database case where even the in-place upgrade’s downtime window is unacceptable: logical replication to a new PG 17 project (schema dump pre-step, replicate/watch, checksum reconcile, write-stop cutover). Near-zero downtime and the only cross-region path, but it lands on a new project - new ref, new API keys, new JWT secret, the same reconfiguration burden as the clone column below. Shown in the diagram for completeness; the step-by-step here covers the two dashboard tracks.

Prerequisites: a paid plan (physical backups enabled; PITR recommended), the Supabase dashboard, and optionally rclone or aws-cli if your rehearsal needs real storage files. Behaviour in this guide was validated against the live platform on 2026-07-29 (clone from PITR, then byte-level and config-level checks on the clone); anything not re-verifiable by you in an hour is marked as such.

The most important thing to understand before starting: the clone flow and the in-place upgrade are completely different animals. A clone is a new project - new project ref, new API keys, new JWT secret, and a pile of project-level settings that do not come along. The in-place upgrade swaps the instance underneath your existing project - nothing about how clients connect changes. Do not mix the two checklists.

production project(PG 15)clone(Restore to a New Project)PITR /physical backupproduction on PG 17(in-place upgrade)in place:Settings > Infrastructure >Upgrade projectnew project on PG 17(sbshift cutover)sbshift: schema dump +logical replication,write-stop cutoverclone on PG 17+ app validationreconfigure+ upgradegreen?schedule window

Validated 2026-07-29 by seeding a source project (public + private buckets, custom schema exposed in the Data API, pgjwt, a live pg_cron job, SMTP markers) and restoring it to a new project. The “clone” column is measured; the “in-place” column follows from the upgrade being a same-project instance swap.12

ItemIn-place upgrade (prod)Restore to a New Project (clone)
Project ref / API URLunchangedNEW - update client env
anon / service API keysunchangedNEW
JWT signing secretunchangedNEW (existing sessions invalid on the clone)
Schema, data, indexes, rolesupgraded in placecopied
auth.users (accounts, hashed passwords)keptcopied
Vault / encryption root keykeptcopied
Compute + disk attributesunchangedmirrors source at clone time
Regionunchangedsame as source
Storage buckets + object metadatauntouchedcopied
Storage file bytesuntouchedNOT copied - pre-existing files 404
Auth settings (SMTP, templates, site/redirect URLs)untouchedNOT copied - reset to defaults
Data API exposed schemasuntouchedNOT copied - reset to public,graphql_public
Edge Functions + secretsuntouchedNOT copied
Realtime settingsuntouchedNOT copied
Read replicasmust DELETE before, recreate afterNOT copied
Deprecated extensions (e.g. pgjwt on PG 17)must drop beforecarried - drop on the clone too
pg_cron / pg_net jobsn/acarried and still active - disable on clones
Custom roles with md5 passwordsreset or they fail post-upgradesame on the clone after its upgrade
Logical replication slotsmust drop beforen/a

Three of these deserve their own sections because they surprise everyone.

Object URLs are constructed from the project that serves the request - they are not stored as absolute URLs - so on a clone every file appears under the new project ref and the dashboard looks complete. But the bytes live in S3 keyed by the source project and are not copied:3

  • pre-existing files return 404 under the clone’s ref (public URLs, nested paths, and signed URLs alike);
  • files you upload to the clone work fine;
  • deleting the source project breaks every old file on the clone.

If your rehearsal needs the real files, sync them over the S3-compatible endpoint (per-project access keys under Settings > Storage):

Terminal window
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...
# download from source, then upload to clone (or rclone remote-to-remote)
aws s3 sync s3://my-bucket ./restore-staging \
--endpoint-url https://SOURCE_REF.supabase.co/storage/v1/s3
aws s3 sync ./restore-staging s3://my-bucket \
--endpoint-url https://CLONE_REF.supabase.co/storage/v1/s3

Bucket metadata is already on the clone, so the targets exist. If the rehearsal only needs the API surface (upload + retrieval paths), skip the sync and treat old-file 404s as expected.

Data API schema exposure is project config, not data

Section titled “Data API schema exposure is project config, not data”

The Exposed schemas setting (Settings > API) is PostgREST db_schema configuration, which is project-level config - it is not in Postgres, so a physical restore cannot carry it. Your schemas, tables, and data all arrive; the clone just does not expose them until you re-add them under Settings > API. Symptom if you forget: PGRST106 / “schema not exposed” errors from the Data API against an otherwise perfectly restored database. The same applies to every Auth setting - SMTP credentials, email templates, site URL, redirect URLs - all reset to defaults on a clone.

One nearby toggle does NOT save you here: Automatically expose new tables (Integrations > Data API settings, a.k.a. “Default privileges for new entities”) only auto-grants new tables and functions in an already-exposed schema - it never exposes a new schema. Its mechanism is ALTER DEFAULT PRIVILEGES, which lives in Postgres, so those grants do survive a clone; the schema list does not.4

Extensions themselves are in the database, so they copy - including deprecated ones like pgjwt (which then blocks the clone’s own PG 17 upgrade until dropped). More dangerously, pg_cron jobs copy active: a clone of production will happily fire production cron jobs a second time. Disable them before anything else:

select cron.unschedule(jobid) from cron.job; -- or selectively:
select cron.alter_job(<jobid>, active := false);

The same caution applies to pg_net, wrappers, and anything else that performs external side effects.2

Zero user impact. Do this even if the production run feels trivial - the clone upgrade previews the exact blocker list and time estimate the production upgrade will show you.

  1. Clone. Source project -> Database -> Backups -> Restore to a New Project -> pick the latest PITR point (or a daily backup). Note the cost preview: the clone mirrors your source’s current compute and bills until deleted. A clone cannot itself be a clone source.2
  2. Sanitize the clone (before it does anything):
    • unschedule / deactivate pg_cron jobs (they copied over active),
    • drop pgjwt and any other extension deprecated on your target version,
    • drop leftover logical replication slots, if any.
  3. Reconfigure the clone (the part that is always manual):
    • new API keys -> your preview/staging deployment env,
    • SMTP provider + sender details (Auth > SMTP),
    • re-expose custom schemas (Settings > API),
    • email templates, site URL, redirect URLs.
  4. Optionally sync storage bytes (see above) if validation needs them.
  5. Upgrade the clone: Settings > Infrastructure > Upgrade project -> target version. Read the blocker list and the downtime estimate before confirming - this is the same pre-flight the production project will get.
  6. Validate with your app against the target Postgres version:
    • login flow + JWKS endpoint,
    • your heaviest write paths,
    • storage upload and retrieval,
    • any custom-schema Data API calls,
    • watch the query logs for plan regressions.
  7. Green -> schedule the production window. Red -> you just saved your production window; debug on the clone.

If you want a production-data seed for preview branches instead of the clone flow, the documented one-liner is supabase db dump --data-only --linked > supabase/seed.sql - branches run seed.sql once at creation.56

Everything in the reconfiguration list above is irrelevant here - the project, URL, keys, JWT secret, auth settings, and storage stay exactly as they are. What changes is the Postgres version (and the rest of the service stack images). Mechanics: the project goes offline, a new instance is provisioned, data is copied (GP3 default: ~100 Mbps, so ~1 minute per GB of disk), pg_upgrade runs, platform validations run, a base backup is taken, and the project comes back. If the upgrade fails, the original database is brought back online automatically.1

  • Announce the maintenance window to users (for small databases the real downtime is minutes, but budget for validation).
  • Pick the window: your users’ quietest hours; avoid Friday/weekend.
  • Run your own readiness review: enabled extensions vs the deprecated list, reg* data types referencing system OIDs, logical replication slots, custom roles with md5 passwords (pg_authid.rolpassword LIKE 'md5%'), unused indexes / bloat (smaller DB = shorter window). The dashboard surfaces hard blockers on the upgrade screen itself.1
  • Drop deprecated extensions (for PG 17: pgjwt, plcoffee, plls, plv8 - drop them from the dashboard’s database extensions page).
  • Delete read replicas (projects with replicas cannot be upgraded; you will recreate them after). Make sure the app tolerates reading from the primary in the meantime.
  • Take a fresh pg_dump logical backup - belt and braces on top of physical backups / PITR.
  • Prepare your write-stop (maintenance page, deploy freeze, queue pause - whatever stops writes cleanly).
  • Stop application writes.
  • Settings > Infrastructure > Upgrade project -> target version. The dashboard’s estimate is the authoritative downtime figure.
  • Watch it through; keep the rollback fact in mind (failure restores the original DB).
  • Validate auth first (login + JWKS), then your heaviest write paths.
  • Recreate read replicas -> they get new connection strings -> update app env.
  • Reset passwords of any custom md5 roles to rehash as scram-sha-256.
  • Check extension versions and the slow-query log for plan regressions.
  • Re-enable writes.
  • Keep monitoring (advisors, slow queries, error rates) before declaring done.
  • Delete the rehearsal clone - it bills at mirrored compute until you do.
CheckHowExpected
Clone is a faithful data copycompare row counts / spot-check marker rowsidentical
Clone storage metadatalist buckets + objects via APIall present
Clone storage bytesGET a pre-existing object via clone ref404 (expected, not a bug)
Clone upload pathPUT then GET a new object200 / 200
Clone config gapsSettings > API, Auth > SMTPdefaults; re-entered values stick
Clone upgrade pre-flightupgrade screen blocker listonly known blockers (e.g. pgjwt)
Prod post-upgradeshow server_version via psqltarget version
Prod identityAPI URL, keys, JWT secretunchanged
Replica recreatedapp env holds new replica stringreads flowing
  • Do not confuse the two flows. Clone = new project + reconfiguration. In-place upgrade = same project, no reconfiguration. Nearly every “upgrade checklist” mistake is applying one flow’s steps to the other.
  • A clone’s dashboard looks complete for storage. It is not - bytes are missing. Test a GET, not the listing.
  • pg_cron on clones is a production hazard. Jobs copy active and will double-fire (emails, webhooks, billing jobs) until unscheduled.
  • Schema exposure and SMTP are the two most-forgotten reconfigurations - both fail silently-ish: the API 404s on your custom schema, and auth emails fall back to the rate-limited built-in sender.
  • Disk only grows. Auto-resize never shrinks back down; if the upgrade resizes your disk upward, that is your new floor.7
  • The clone mirrors compute at clone time. If you resized production after your last clone, your next clone lands on the new size and bills accordingly.
  • The upgrade keeps your compute size. It is a version change, not a resize.
  • The same in-place process covers minor and future major versions (e.g. PG 18 when it becomes available as a target).1
  • Restore-to-new-project is dashboard-only - there is no Management API or CLI path for it as of 2026-07 (the API only restores in place). Re-verify before scripting around it.
  1. Supabase, “Upgrading,” Supabase Docs. https://supabase.com/docs/guides/platform/upgrading 2 3 4

  2. Supabase, “Restore to a new project,” Supabase Docs. https://supabase.com/docs/guides/platform/clone-project 2 3

  3. Supabase, “Restore Dashboard backup - Migrate storage objects,” Supabase Docs. https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore

  4. Supabase, “Using Custom Schemas,” Supabase Docs. https://supabase.com/docs/guides/api/using-custom-schemas

  5. Supabase, “CLI workflows,” Supabase Docs. https://supabase.com/docs/guides/local-development/cli-workflows

  6. Supabase, “Working with branches - Seeding behavior,” Supabase Docs. https://supabase.com/docs/guides/deployment/branching/working-with-branches

  7. Supabase, “Compute and Disk,” Supabase Docs. https://supabase.com/docs/guides/platform/compute-and-disk