Nov 7, New York: Romaric speaks at SREday NYC: SRE in the era of AI

How to Migrate a Supabase App (Postgres, Auth, Edge Functions) to Kubernetes on Your Own Cloud Account

A step-by-step guide to moving a Supabase app off the hosted platform: dump and restore Postgres (including auth.users), run GoTrue or import your bcrypt hashes into an IdP without password resets, and turn Deno Edge Functions into containers on Kubernetes in your own AWS, GCP, Azure, or Scaleway account.

Romaric Philogene
CEO & Co-founder
NOV 1, 2026 · 15 MIN
How to Migrate a Supabase App (Postgres, Auth, Edge Functions) to Kubernetes on Your Own Cloud Account

Key Points:

  • A Supabase migration is three migrations in one. You move Postgres (including the auth and storage schemas), the Auth service (GoTrue) with its JWT signing key, and the Deno Edge Functions. Run them as three separate cutovers with three rollback plans, never in the same window.
  • Postgres moves with a dump and restore or with logical replication. A dump and restore needs a planned write freeze of roughly 15 to 60 minutes for most apps. Logical replication gets the cutover down to seconds. Restore into managed Postgres (Amazon RDS or Aurora, Cloud SQL, Azure Database for PostgreSQL Flexible Server, Scaleway Managed Database), not a StatefulSet.
  • Nobody has to reset their password. Supabase Auth is the open-source GoTrue service, and the bcrypt hashes in auth.users.encrypted_password are ordinary table data. You can run supabase/auth yourself, or bulk-import the hashes into Auth0, Clerk, Amazon Cognito or SuperTokens.
  • Edge Functions do not run on Kubernetes unchanged. Each Deno function becomes a route in your API, a small Deno container with its own Deployment, or a serverless function. In the client, supabase.functions.invoke() becomes a plain fetch.
  • If you drop PostgREST, every supabase-js .from().select() call in your frontend breaks. Run PostgREST as a container to keep the client code, or budget for an API rewrite before you touch the database.
  • Qovery deploys the migrated stack into your own AWS, GCP, Azure or Scaleway account, or your existing Kubernetes cluster. That covers your API, GoTrue, each former Edge Function and per-PR preview environments, with the database on a managed cloud service. The bill, the VPC and the data residency stay in your name, and you don't need to hire a DevOps engineer.

Qovery · Infrastructure control plane
Kubernetes, operated through one governed API
Learn more

A Supabase project looks like one product behind one URL. Under the hood it runs at least six open-source services: Postgres, PostgREST, GoTrue, Realtime, Storage and an API gateway, plus a Deno runtime for Edge Functions. That changes how you plan the migration: you are moving a handful of well-known open-source components, and each one can move on its own schedule. Moving all of it in one weekend is how migrations end up rolled back, so this guide breaks the job into the pieces that actually move.

What exactly are you migrating when you leave Supabase?

A Supabase project is a set of open-source services behind one URL: Postgres, PostgREST, GoTrue (Auth), Realtime, Storage and a gateway, plus Edge Functions on supabase/edge-runtime. Because each piece is a separate project, you can move them one at a time instead of in one big-bang cutover.

All of this is open source. The supabase/supabase repo is Apache-2.0, and both supabase/auth and supabase/edge-runtime are MIT. The official self-hosted docker-compose file lists every container you'd be replacing. It now uses Envoy as the gateway, where older setups used Kong.

Supabase componentOpen-source projectRecommended target on your cloudClient-code impact
DatabasePostgres (supabase/postgres image)Managed Postgres: RDS/Aurora, Cloud SQL, Azure Flexible Server, ScalewayConfig only (connection string)
Data APIPostgRESTPostgREST container on Kubernetes, or fold into your APINone if kept, rewrite if dropped
Authsupabase/auth (GoTrue)GoTrue container, or an IdPConfig only with GoTrue, rewrite with an IdP
Realtimesupabase/realtime (Elixir)Container, or your own WebSocket/pub-subConfig only if kept
Storagesupabase/storage-apiS3, GCS, Azure Blob or Scaleway Object Storage behind your APIRewrite of upload/download calls
Edge Functionssupabase/edge-runtime (Deno)API routes, Deno containers or serverlessfunctions.invoke() becomes fetch
GatewayEnvoy (Kong in older setups)Your ingress controller or cloud load balancerNone

The usual reasons to leave are about ownership: data residency and compliance (GDPR, HIPAA, regional rules), cost at scale, one consolidated cloud bill with your own discounts, VPC-private networking, or a company-wide Kubernetes standard.

Run this inventory before you plan anything. It takes five minutes:

SQL|Users and auth footprint
SELECT count(*) AS users FROM auth.users;
SELECT provider, count(*) FROM auth.identities GROUP BY provider;

-- Extensions you depend on
SELECT extname, extversion FROM pg_extension ORDER BY extname;

-- RLS policies and the tables they protect
SELECT schemaname, tablename, policyname, cmd FROM pg_policies ORDER BY 1, 2;

-- Storage footprint per bucket
SELECT bucket_id, count(*) AS objects,
       pg_size_pretty(sum((metadata->>'size')::bigint)) AS size
FROM storage.objects GROUP BY bucket_id;

Also count your Edge Functions, OAuth providers, cron jobs and database triggers. For effort: a small app takes a weekend. An app with deep RLS, Storage and a dozen functions takes several weeks of calendar time across three cutovers.

How do you migrate Supabase Postgres to your own cloud account without downtime?

Use a dump and restore when you can afford a short planned write freeze (15 to 60 minutes for most apps). When you can't, use Postgres logical replication or a managed migration service such as AWS DMS or Google's Database Migration Service. Either way, restore into a managed Postgres service rather than a pod.

Before you touch the database, answer the PostgREST question, because it decides how much of your frontend changes. supabase-js calls like .from('orders').select() go to PostgREST, not to Postgres directly. If you keep PostgREST as a container pointed at the new database, the client code stays the same. If you drop it, you're rewriting those calls against your own API.

The dump and restore path

Supabase documents a three-file dump with its CLI, which wraps pg_dump: roles, then schema, then data (Supabase backup and restore guide):

BASH
supabase db dump --db-url "$SUPABASE_DB_URL" -f roles.sql --role-only
supabase db dump --db-url "$SUPABASE_DB_URL" -f schema.sql
supabase db dump --db-url "$SUPABASE_DB_URL" -f data.sql --use-copy --data-only \
  -x "storage.buckets_vectors" -x "storage.vector_indexes"

The restore runs in one transaction and stops on the first error. It sets session_replication_role = replica so triggers don't fire twice during the data load:

BASH
psql --single-transaction --variable ON_ERROR_STOP=1 \
  --file roles.sql --file schema.sql \
  --command 'SET session_replication_role = replica' \
  --file data.sql --dbname "$TARGET_DB_URL"

The same guide flags what the dump leaves out. Storage files are not in the database dump, custom triggers and policies on the auth and storage schemas need a separate diff, and the Vault root key isn't in any backup. On a non-Supabase target, expect to edit supabase_admin ownership lines. Also check that your managed service's admin role is allowed to set session_replication_role.

Check your extensions before you pick a target

pg_cron, pgvector and PostGIS are on the supported lists of Amazon RDS, Cloud SQL and Azure Flexible Server. pgsodium, supabase_vault, pg_graphql, pg_net and pgjwt are not on any of the three lists when I checked. Plan application-side replacements for those: KMS or your secrets manager instead of Vault, an HTTP call from your API instead of pg_net. Re-check the lists on the day you migrate, because they change.

Near-zero downtime with logical replication

After restoring roles and schema on the target, publish on Supabase and subscribe from the target:

SQL|On Supabase
CREATE PUBLICATION app_migration FOR TABLES IN SCHEMA public, auth;

-- On the target
CREATE SUBSCRIPTION app_migration
  CONNECTION 'host=db.<ref>.supabase.co port=5432 dbname=postgres user=postgres password=...'
  PUBLICATION app_migration;

-- On Supabase, watch lag until it is near zero
SELECT application_name, state, replay_lag FROM pg_stat_replication;

Logical replication has gaps you must cover by hand. DDL and sequence values are not replicated, large objects are not replicated "and there is no workaround", and tables need a primary key or replica identity for updates and deletes (PostgreSQL docs). TRUNCATE does replicate, but it fails if foreign keys point at tables outside the subscription.

Cutover runbook: freeze writes, compare row counts per table, bump every sequence with setval to the source's latest value, set passwords for roles that have LOGIN, switch DATABASE_URL, then drop the subscription. Keep the Supabase project read-only for 7 days as your rollback path.

MethodWrite freezeNot carried overExtensionsRollbackPick it when
supabase db dump + psql15-60 min for most appsStorage files, Vault key, some auth/storage customizationsEnable on target before restorePoint back at SupabaseSmall to mid-size DBs, freeze acceptable
Logical replicationSeconds at cutoverSequences, DDL, large objectsEnable on target before subscribingReverse publication or point backLarger DBs, no freeze window
AWS DMS / Google DMSSeconds to minutesCheck each service's Postgres source limitationsManual on targetPoint back at SupabaseYou want a managed console and monitoring

On why managed Postgres beats a StatefulSet: RDS keeps automated backups for up to 35 days with point-in-time restore (AWS docs). Azure Flexible Server keeps 7 to 35 days (Microsoft Learn). Cloud SQL Enterprise Plus keeps PITR logs for up to 35 days, and Enterprise for up to 7 (Google Cloud docs). CloudNativePG is a good operator, and it entered the CNCF as a Sandbox project in January 2025 (CNCF). Only take that route if someone on the team actually does DBA work.

How do you migrate Supabase Auth without forcing every user to reset their password?

You can keep every existing login working. Supabase Auth is the open-source GoTrue service, and the bcrypt hashes in auth.users.encrypted_password move over as ordinary table data, so neither path needs a reset email.

Option A: run GoTrue yourself. Deploy the supabase/auth image next to your API. The minimum configuration, taken from the supabase/auth README and the official compose file:

BASH
GOTRUE_DB_DRIVER=postgres
GOTRUE_DB_DATABASE_URL=postgres://supabase_auth_admin:...@your-db:5432/postgres
GOTRUE_JWT_SECRET=...            # same secret as the Supabase project
GOTRUE_SITE_URL=https://app.example.com
API_EXTERNAL_URL=https://auth.example.com
GOTRUE_SMTP_HOST=... GOTRUE_SMTP_PORT=... GOTRUE_SMTP_ADMIN_EMAIL=...

Check which signing key your project uses first. Supabase now recommends asymmetric keys (ES256 or RS256), and the legacy shared JWT secret "is the only key that can be extracted from Supabase" (Supabase signing keys docs). With the legacy secret, existing access tokens keep validating on your GoTrue. With asymmetric keys, you have to issue new keys, so access tokens signed by Supabase stop validating. Plan for users to refresh or sign in once.

Option B: move to an identity provider. Bcrypt import support differs by vendor:

TargetBcrypt hash importauth.uid() / RLS compatibilityWhere it runsPricing model
Self-hosted GoTrueNative, same tableUnchangedYour clusterYour compute only
KeycloakNo built-in bcrypt: ships Argon2 and PBKDF2 (Keycloak docs), needs a custom hash providerMap claims, rewrite policiesYour clusterOpen source, your compute
Auth0Yes, $2a$, $2b$, $2y$ prefixes (Auth0 docs)Map claims via ActionsAuth0 cloudPer-MAU tiers
ClerkYes, password_hasher: "bcrypt" (Clerk docs)Map claims via JWT templatesClerk cloudPer-MAU tiers
Amazon CognitoYes, BCRYPT import jobs, max cost factor 12 (AWS docs)Map claims, rewrite policiesYour AWS accountEssentials $0.015/MAU after 10,000 free (AWS)
SuperTokensYes, bcrypt via bulk import API (SuperTokens docs)Map claims, rewrite policiesSelf-hosted or SaaSSelf-hosted open source, or SaaS

Whichever issuer you choose, protect your RLS policies. auth.uid() reads the sub claim that PostgREST puts into the request settings. Before you switch issuers, list every claim your policies read (sub, role, email, app_metadata). If a new token lacks one, the policy doesn't error. It denies access without telling you.

Then re-register every OAuth provider's redirect URL against the new auth domain, and re-provision SMTP, email templates and magic links. Refresh tokens live in auth.refresh_tokens. Copy them over and users stay logged in. Drop them and everyone signs in once. Pick one and say so in your release notes. Before the DNS cutover, test sign-in with a sample of real accounts against the new service.

Ship faster on infrastructure you control.
Qovery gives your team self-service deployments on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.

What replaces Supabase Edge Functions on Kubernetes?

Edge Functions are Deno scripts that run on supabase/edge-runtime, so Kubernetes can't run them as they are. Each function becomes a route in your existing API, its own Deno container with a Deployment and Service, or a serverless function if you need scale-to-zero.

Moving also lifts the hosted limits. Supabase caps each function at 256 MB of memory, 2 s of CPU time per request and 400 s of wall clock on paid plans (Supabase docs). In your own container, the only ceilings are the resource requests you set.

TargetCode changesCold startScale to zeroExecution ceilingOps burden
Fold into your APIPort to your backend language or runtimeNoneNoYour API timeoutsLowest
Deno container on KubernetesMinimal: imports, env, clientNone (always-on)No by defaultYour pod limitsOne Deployment per function
AWS Lambda / Cloud Run / Azure Container AppsWrap handler or containerizeYesYesProvider limitsLow, but one more platform

Code changes are smaller than they look. Deno.serve keeps working, and Deno.env.get() reads ordinary container env vars. Review npm: and jsr: imports, and replace the service-role supabase-js client with a real Postgres role and internal service auth. Function secrets become Kubernetes Secrets.

DOCKERFILE
FROM denoland/deno:latest
WORKDIR /app
COPY . .
RUN deno cache main.ts
EXPOSE 8000
CMD ["run", "--allow-net", "--allow-env", "main.ts"]

Pin the image tag in production. The Deployment needs a readiness probe on a cheap /health route, which is the line people forget. Without it, Kubernetes routes traffic to the pod before Deno is listening. In the client, replace supabase.functions.invoke('name') with a fetch to your own domain behind one constant, and keep both endpoints live for a week so rollback is a one-line change. Scheduled functions become Kubernetes CronJobs, or stay in pg_cron, which RDS, Cloud SQL and Azure all support.

One latency caveat: Supabase runs functions close to users in many regions, so a single-region cluster adds round-trip time for distant users. Put a CDN in front before you consider multi-region.

What does Supabase cost versus running the same stack on your own cloud account?

Self-hosting rarely wins for small projects. A Kubernetes control plane, managed Postgres and engineer time already cost more than the Supabase Pro base price. It starts to win once compute, storage and egress are big enough that your own cloud discounts apply and you can stop non-production environments outside working hours.

Supabase Pro starts at $25/month and Team at $599/month. Both include $10 of compute credits, 8 GB of disk per project, 250 GB of egress and 100 GB of file storage. Overages are $0.125/GB for disk and $0.09/GB for egress. The Large compute add-on (2 dedicated vCPUs, 8 GB RAM) is $110/month (Supabase pricing).

On your own account, the fixed floor is the cluster: $0.10 per cluster per hour on EKS standard support (AWS) and on GKE (Google Cloud), while AKS has a free tier without an SLA (Azure). Add the engineer-days for upgrades, restore drills, on-call and observability. The upside is discounts that only apply in your own account: up to 66% with Compute Savings Plans and 72% with EC2 Instance Savings Plans (AWS).

Supabase hosted (Pro/Team)Self-hosted Supabase on KubernetesDecomposed stack on your cloud with Qovery
PostgresIncluded, compute add-onssupabase/postgres in-cluster, you run itManaged RDS / Cloud SQL / Azure / Scaleway
Backups and PITRManaged, PITR as a paid add-on (Supabase pricing)You build it (not included)Provider-managed, up to 35 days
Egress250 GB, then $0.09/GBYour cloud's ratesYour cloud's rates; AWS includes 100 GB/month free (AWS)
Who runs upgradesSupabaseYouQovery runs cluster upgrades, cloud provider patches the DB
Savings Plans / CUDsNoYesYes
Whose account the bill lands inSupabase'sYoursYours

Should you self-host the full Supabase stack or rebuild on standard cloud services?

If you self-host all of Supabase, supabase-js keeps working everywhere, but you now operate every service yourself on a community-supported distribution. If you decompose onto managed Postgres and your own API, the lock-in goes away, but your frontend changes. The deciding factor is almost always how heavily you use PostgREST and RLS.

Supabase is clear about the self-hosted trade-off. Self-hosted Supabase "is community-supported". It has no branching, managed backups, PITR or Management API, and you are responsible for "high availability and scalability" and "backups and disaster recovery" (Supabase self-hosting docs). The Kubernetes Helm charts are community-maintained.

Path A: self-host SupabasePath B: decomposePath C: hybrid, Postgres first
Frontend changesNoneData, auth, storage callsGradual
Services you operatePostgres, PostgREST, GoTrue, Realtime, Storage, gateway, edge-runtime, StudioYour API, GoTrue or IdPGrows per phase
Upgrade burdenHigh, every componentLow, mostly managedLow to medium
Parity with hostedMissing branching, managed PITR, Management APINot applicableFull until the final phase
Rollback difficultyMediumHigh once frontend shipsLow per phase
Typical durationDaysWeeksWeeks, low risk

With a live customer base, I'd pick Path C. Leave Auth and Storage on Supabase at first, move Postgres (with PostgREST in front of it), then the functions, then Auth last. The catch is foreign keys to auth.users: keep replicating the auth schema from Supabase into the new database until Auth moves. Never run the Postgres, Auth and Functions cutovers in the same window.

For Postgres alone, Neon, RDS/Aurora and Cloud SQL are all fair options. For the application side, teams usually compare Render, Northflank, Porter and Coolify.

How does Qovery handle the Kubernetes side of a Supabase migration?

Qovery creates and runs the Kubernetes cluster and deploys every piece of the migrated stack into your own AWS, GCP, Azure or Scaleway account, or into your existing Kubernetes cluster. A small team can finish the migration without hiring a DevOps engineer.

Your API, the GoTrue container, PostgREST if you keep it, and each former Edge Function become Qovery services that deploy on git push. Each environment gets its own env vars and secrets, which replace Supabase function secrets. Databases run on managed cloud services (Amazon RDS, Cloud SQL, Azure Database for PostgreSQL, Scaleway Managed Database), so your Postgres keeps managed backups and failover.

Two features matter most for a multi-week migration. Preview environments give every pull request a full copy of the stack, so you can test the auth flow and rewritten functions before the cutover. Environment auto-stop shuts those copies down outside working hours. Qovery also runs the cluster upgrades and provides per-environment RBAC, the operational work that lands on your team the day you leave a hosted platform. The cloud bill, your committed-use discounts, the VPC and data residency all stay in your name, which is usually why the team left in the first place.

If you're a solo developer on a side project, stay on Supabase. It's a good product, and the migration isn't worth your weekend. This path is for teams that need the data and the bill in their own account.

How do I run this migration with Claude Code or Cursor?

Install the Qovery Agent Skills once. Your coding agent can then connect your cloud account, deploy the migrated services and create preview environments from plain-language instructions, and every action waits for your approval.

BASH|Install the Qovery Agent Skills globally (Claude Code, Cursor, Codex, OpenCode, Gemini CLI)
curl -fsSL https://skill.qovery.com/install.sh | bash

# Same install, scoped to the current repository
curl -fsSL https://skill.qovery.com/install.sh | bash -s -- --project

# Connect Claude Code to the Qovery MCP Server (OAuth, read-only by default)
claude mcp add --transport http qovery https://mcp.qovery.com/mcp --callback-port 4242

# Write access, only when you want the agent to change infrastructure (also enable it in the Qovery Console)
claude mcp add --transport http qovery "https://mcp.qovery.com/mcp?read_write=true" --callback-port 4242

During the migration, qovery-onboard connects the cloud account and cluster, and qovery-deploy ships the API, GoTrue and function containers. qovery-preview creates per-PR copies to test auth and function rewrites, and qovery-troubleshoot debugs failing deployments. qovery-optimize, qovery-speedup and qovery-terraform cover the rest. Then ask something like "deploy this Deno function as a service on my GCP account with Qovery and wire it to the migrated Postgres". Setup details are in the AI agent quickstart and on the Claude Code integration page.

Frequently asked questions
Can I migrate Supabase Auth users without making them reset their passwords?

Yes. The bcrypt hashes in auth.users.encrypted_password move over with the rest of the data, so self-hosted GoTrue accepts existing passwords as they are. Auth0, Clerk, Amazon Cognito and SuperTokens all document bcrypt hash import. Keycloak needs a custom hash provider because it ships only Argon2 and PBKDF2.

Can Supabase Edge Functions run on Kubernetes unchanged?

No. They run on supabase/edge-runtime, so each function needs a small Deno container, a route in your API or a serverless target. The code changes are usually small: imports, env vars and replacing the service-role client.

Should I run Postgres inside Kubernetes or use a managed database after leaving Supabase?

Use a managed database unless someone on your team does real DBA work. RDS, Azure Flexible Server and Cloud SQL Enterprise Plus all offer up to 35 days of point-in-time recovery out of the box. CloudNativePG is a solid operator, but backups, failover and upgrades become your job.

How long does a Supabase to Kubernetes migration take, and how much downtime should I plan?

A small app takes a weekend. An app with deep RLS, Storage and a dozen functions takes several weeks across three cutovers. Plan a 15 to 60 minute write freeze for a dump and restore, or seconds of downtime with logical replication.

Is self-hosting Supabase on Kubernetes cheaper than Supabase Pro?

Not for small projects. Supabase Pro starts at $25/month, while a cluster, managed Postgres and the engineer time to run them cost more than that before any traffic. Self-hosting pays off at scale, when your own Savings Plans or committed-use discounts and auto-stopped staging environments apply.

Do I still need supabase-js and PostgREST after migrating off Supabase?

Only if you want your frontend to stay the same. Keep PostgREST as a container and supabase-js data calls keep working against your new database. Drop it, and every .from().select() call has to move to your own API.

Romaric Philogene
About the author
Romaric Philogene

Romaric founded Qovery to make Kubernetes accessible to every engineering team. He writes about platform strategy, developer experience, and the future of cloud infrastructure.

Next step

Ship faster on infrastructure you control.

Qovery gives your team self-service deployments on your own AWS, GCP, Azure, or Scaleway account - or your existing Kubernetes cluster. Start deploying in under 10 minutes.