The decision to repair a Supabase backend
Learn when to repair a Supabase backend or migrate it by measuring query debt, RLS sprawl, Edge Functions, compliance, and projected cost.

That sounds obvious until a broken production app makes every defect look like a platform defect. I have seen teams plan a six week migration because one missing index made a dashboard crawl. I have also seen them spend months polishing policies around a data model that could never express the permissions promised to customers. The decision needs evidence from the running system, not frustration with the last incident.
Use five tests: query complexity, RLS sprawl, dependence on Edge Functions, compliance requirements, and projected cost. None works alone. A complex query can be perfectly healthy. Fifty policies can be easier to trust than five vague ones. A low hosting bill can hide expensive manual recovery. The useful question is whether each source of difficulty is repairable in place and whether the repaired system still fits the next two years of expected use.
Repair defects when the architecture still fits
Repair is the better choice when the backend's intended boundaries still match the product. If Postgres remains a sensible system of record, direct client access still suits the application, and Supabase Auth, Storage, or Realtime meet the actual requirements, moving platforms replaces known defects with a new set of unknown ones.
Classify every complaint before discussing a destination. Put it in one of four buckets: implementation defect, missing operational discipline, architectural mismatch, or external requirement. A missing foreign key, an exposed service role secret, and an unindexed policy predicate are implementation defects. Schema changes made only through the production dashboard point to missing discipline. A workflow that requires long CPU intensive jobs may be an architectural mismatch for Edge Functions. A contract demanding a deployment region or audit control that the selected plan cannot supply is an external requirement.
Only the last two buckets make migration likely. The first two usually call for repair. This classification prevents a familiar failure: a team migrates the tables, then recreates the same permissive authorization, absent migrations, and weak monitoring on a different provider.
Repair also wins when the team cannot state a verified target architecture. "A custom backend" is not a target. It is an obligation to choose an API framework, identity system, database hosting, connection strategy, object storage, background job runner, secrets system, logging stack, backup process, and deployment path. Each choice needs an owner. If those decisions have not been made, the migration estimate contains mostly blank space.
Set a repair boundary before work starts. For example: restore migration history, remove browser access to privileged credentials, make authorization tests pass, bring the slowest user journeys within an agreed latency budget, and document restore drills. If that bounded repair produces a backend the team can operate, migration has no business case. If the boundary keeps expanding because every fix exposes an incompatible assumption, that evidence supports moving.
Query complexity needs plans, not opinions
Complex SQL does not justify migration by itself. PostgreSQL runs the database underneath Supabase, so moving the same schema and queries to another managed PostgreSQL service will not make bad joins, missing indexes, wide row fetches, or chatty client access disappear.
Start with workload evidence. The Supabase documentation recommends pg_stat_statements for finding frequent and expensive statements. Capture calls, total execution time, mean execution time, rows, and the normalized query. Do not sort only by mean time. A query that takes 40 milliseconds and runs a million times can consume more capacity than a two second report opened twice a day.
This query creates a useful first cut:
select
queryid,
calls,
round(total_exec_time::numeric, 1) as total_ms,
round(mean_exec_time::numeric, 1) as mean_ms,
rows,
left(query, 180) as sample
from pg_stat_statements
where calls >= 20
order by total_exec_time desc
limit 25;
Its output has one row per normalized statement, with the query identifier, call count, accumulated time, average time, affected rows, and a shortened sample. Save that result during a representative business period. A quiet staging database proves little about production traffic.
For the statements that dominate latency or database time, run EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) against safe representative data. ANALYZE executes the statement, so use a transaction you can roll back for writes and avoid running it blindly against production. Look for sequential scans over large relations, severe differences between estimated and actual rows, repeated loops, disk reads, and sorts that spill. Those findings point to indexes, rewritten predicates, better statistics, narrower selections, or a changed data model.
Repair when a small number of identifiable plans account for most of the pain and ordinary Postgres changes resolve them. Consider migration when the workload itself conflicts with the chosen service shape: sustained analytical scans interfere with transactional traffic, required extensions are unavailable, connection behavior cannot meet the application's concurrency model, or data must live close to another system and measured network time dominates requests.
Do not confuse API request count with query complexity. An AI generated frontend often fetches a list, then requests related data once per row. The database may execute simple queries while the page suffers from round trips. Consolidating the access into a view, an RPC function, or one server endpoint is a repair. Moving that pattern intact only changes where the latency bill arrives.
Test repairs against a workload, not one hand selected query. Capture a replayable set of read and write operations for the busiest user journeys, remove personal data from fixtures, and run it before and after each change. Record median and slow request time, database CPU, connection count, rows read, and error rate. A new index that helps one filter can slow writes or consume enough storage to change the cost calculation. A materialized view can make a report fast while introducing refresh lag the product cannot accept.
Connection pressure needs the same diagnosis. Browser clients, server processes, transaction poolers, and direct database sessions have different behavior. Count active and waiting sessions during bursts and find which component owns them. If a generated app opens new server connections per request, repair connection reuse before buying larger compute. Move only when the verified concurrency and transaction requirements remain incompatible after the client and pool configuration are corrected.
RLS sprawl is measured by behavior
RLS sprawl becomes a migration signal when nobody can reliably predict or test who may read and change each row. Policy count alone tells you almost nothing. A larger set of narrow policies can be safer than a compact policy full of nested membership checks and mutable JWT claims.
Inventory policies from the database instead of trusting a diagram:
select
schemaname,
tablename,
policyname,
roles,
cmd,
permissive,
qual,
with_check
from pg_policies
order by schemaname, tablename, cmd, policyname;
Review the output as an authorization matrix. For every exposed table and operation, name the acting roles, the row condition, the insert or update check, and the expected denial. Flag duplicated membership subqueries, policies that depend on user editable metadata, broad true conditions, inconsistent tenant columns, and tables exposed through the API without an intentional policy.
Supabase's RLS guide makes a sharp distinction that many generated apps miss. Users can update raw_user_meta_data, so it is not a safe place for authorization claims. raw_app_meta_data cannot be changed by the user and can hold authorization data, though JWT contents may remain stale until token refresh. That is not a minor naming detail. Put a role in the wrong claim and a user may grant themselves access.
Performance fixes belong after correctness. Index columns used in policy predicates. Where semantics allow it, wrapping auth.uid() in a scalar subquery lets the planner create an init plan and avoid evaluating the function for every row. Keep explicit filters in application queries even when a policy already enforces the same tenant boundary, because the filter helps the planner choose a narrower path. None of those changes repairs an authorization model that the team cannot describe.
Build denial tests before changing policies. Run the same select, insert, update, and delete attempts as an anonymous user, an ordinary member, a member of another tenant, an administrator, and a suspended account if that state exists. Assert both allowed rows and forbidden rows. Service role tests do not replace user tests because privileged access can bypass RLS.
Repair RLS when the product has a stable tenant model and policies can be reduced to named reusable predicates with a complete test matrix. Migrate or insert a dedicated authorization layer when permissions depend on relationships that change rapidly, decisions require context outside Postgres, customers need policy explanations your current model cannot produce, or every new feature forces edits across dozens of unrelated policies. Even then, migration does not remove the need for database protection. It changes where the primary decision happens.
Pay attention to ownership during writes. A select policy may correctly hide another tenant's rows while an incomplete with check condition lets a user insert a row with someone else's tenant identifier. Updates need both a rule for which existing rows the user may target and a rule for what the new row may contain. Test those paths separately. Generated applications often exercise only successful reads, so write escalation survives until production.
Central helper functions can reduce repeated predicates, but they can also conceal privilege. Inspect the owner, schema search path, execution mode, and grants of every security definer function referenced by a policy. Qualify relation names and keep the callable surface narrow. If the team cannot explain why a helper runs with elevated rights, expanding that helper across the schema increases risk rather than simplifying the system.
Edge Function dependence sets the size of a move
Edge Functions matter because they reveal how much of the backend is more than a database. A project with two webhook handlers is a different migration from one where every privileged write, payment callback, scheduled task, email action, and third party integration runs through Deno functions.
Create a function ledger with one row per deployed function. Record its caller, authentication method, secrets, database operations, external services, timeout behavior, retry policy, idempotency mechanism, deployment command, and average and peak invocations. Then trace which clients depend on its URL and response shape. Source files alone will not reveal a provider dashboard secret or an external webhook configured months ago.
Supabase documents Edge Functions as TypeScript functions on a Deno compatible runtime. That gives the code some portability, but surrounding behavior still needs work: gateway routing, JWT handling, environment secrets, scheduled invocations, log delivery, deployment, and regional execution. Treat "the code is TypeScript" as a helpful fact, not as a migration plan.
Repair when functions are thin adapters. A good repair extracts business rules into plain modules, validates inputs at the boundary, uses explicit timeouts for external calls, makes webhook processing idempotent, and moves long work to a queue or worker that fits its duration. The official limits include bounded memory, CPU time, wall clock time, request idle time, bundle size, and secret counts. Read the current limits for the active plan rather than copying a number into an architecture document that will go stale.
Migration becomes attractive when the runtime limit is part of the normal workload rather than an occasional defect. Image processing, large document conversion, long AI jobs, heavy data exports, and durable workflows often need workers with controlled concurrency, persistent queues, and explicit retry state. You can keep Supabase for Postgres and Auth while moving only those jobs. A partial migration often removes the constraint without replacing the database.
Estimate the move by dependency, not file count. One 80 line payment webhook with undocumented replay behavior can carry more risk than twenty read only endpoints. For each function, require a contract test that sends the same request to old and new implementations, normalizes provider generated fields, and compares status, body, database effects, and outbound calls. Without that harness, teams discover semantic differences through customer reports.
Compliance can overrule technical convenience
Compliance is a migration reason only when a written requirement cannot be met by the available service, plan, configuration, and operating process. Vague anxiety about regulated data wastes time. A signed customer clause, an auditor's control statement, or a legal restriction gives you something testable.
Turn the requirement into a control matrix. Name the data in scope, permitted regions, encryption expectations, retention period, deletion procedure, recovery objective, staff access rules, audit evidence, incident notification terms, subprocessors, and contract documents. Assign each row to the platform or to your team. Managed hosting never transfers all responsibility to the vendor.
Check the current Supabase plan documentation and contractual documents for the required controls. Features such as audit logs, log retention, point in time recovery, single sign on, project roles, private networking, region choices, and regulated workload support can vary by plan and agreement. A missing control on the current plan may require an upgrade, not a migration. Compare the upgrade with a realistic destination that includes the same evidence and support obligations.
Backups deserve special care. Supabase's backup documentation says database backups do not contain the objects stored through the Storage API, only their metadata. A database restore therefore does not restore a deleted object. If your recovery plan assumes one button rewinds both, the plan is wrong. Repair it with separate object protection and a restore drill, or choose an architecture whose recovery controls match the requirement.
Migration is justified when the vendor cannot sign the required agreement, the required region or network boundary is unavailable, evidence retention cannot meet the contract, or the organization must control infrastructure in a way the hosted service does not permit. Write down the failed control and the destination's proof before moving. Self hosting may provide control, but it also makes the team responsible for patching, monitoring, backup integrity, access reviews, and incident evidence.
Do not use migration to avoid understanding data flows. You need the same inventory to migrate safely: tables, object buckets, authentication records, logs, secrets, replicas, analytics exports, and third party processors. Compliance work often exposes undocumented flows. That discovery can lead to a smaller repair or confirm that the present design must change.
Projected cost includes people and transition risk
Projected cost should compare the repaired system, the migrated steady state, and the transition itself over the same demand forecast. Comparing today's Supabase invoice with the database line item from another provider produces a fiction.
Model cost by workload driver. Use monthly active users where it affects authentication, database compute and storage growth, egress by origin and destination, Realtime messages and peak connections, Storage volume and operations, Edge Function invocations, log volume and retention, backup or recovery features, support, and required plan add ons. Pull current unit prices from both vendors when the decision is made. Keep each price and included quota in an assumptions sheet with a capture date because pricing changes.
Use three demand cases instead of one precise forecast: expected, high, and contraction. The formula can stay simple:
monthly platform cost =
base plans
+ database compute and storage
+ network egress
+ authentication usage
+ realtime usage
+ function usage
+ logs, backups, and support
monthly operating cost =
engineering hours
+ incident response
+ security and compliance work
+ vendor management
Estimate engineering hours from actual work: failed deployments, manual policy reviews, database tuning, function debugging, restore tests, and support escalations. Do not assign zero to work absorbed by a founder. It still delays product and sales work.
Transition cost includes parallel environments, data copy, object copy, change data capture if required, dual writes if chosen, contract tests, client updates, observability, security review, cutover support, and rollback capacity. Add the revenue or contractual exposure of downtime and inconsistent data as a range, not an invented exact value. A migration that saves a few hundred dollars per month can take years to repay.
Calculate a break even month:
break_even_month =
transition_cost /
(repaired_monthly_cost - migrated_monthly_cost)
If the denominator is zero or negative, cost does not support migration. If the result exceeds the architecture's likely useful life, the saving is theoretical. Run the formula again under the high demand case, because a destination may become cheaper only after a threshold, and under contraction, because fixed infrastructure and staffing can punish lower usage.
Repair when tuning, right sizing, archiving, or moving one workload changes the curve enough. Migrate when a structural cost driver remains after repair, such as unavoidable egress, a required premium plan dominated by one control, or labor spent compensating for a persistent platform mismatch.
A scored decision exposes weak assumptions
A scorecard helps only when every score points to evidence. It should not hide judgment behind arithmetic. Use it to expose which assumptions change the recommendation and which questions remain unanswered.
Score each factor from 0 to 3 for both repair and migration. Zero means the option does not satisfy the requirement. Three means it satisfies it with demonstrated evidence. Weight compliance and security as gates: if an option cannot meet a mandatory control, its total score does not rescue it.
Use these factors:
- Query workload fit, supported by statement statistics and execution plans.
- Authorization clarity, supported by the policy inventory and denial tests.
- Runtime fit, supported by the Edge Function ledger and workload duration.
- Compliance fit, supported by the control matrix and contract documents.
- Cost over the forecast period, including transition and labor.
Add operability, team skill, rollback quality, and delivery interruption if they can change the choice. Record a confidence level beside each score. A cost score built from one month of incomplete usage data should not look equal to a compliance score confirmed in a signed agreement.
Set decision rules before scoring. One practical rule is to repair when every mandatory gate passes, the repair scope is bounded, and the migration payback falls outside the forecast period. Migrate when a mandatory gate fails with no credible plan or when several measured constraints survive a time boxed repair. Choose a hybrid when one service, usually long running compute or analytics, causes most of the mismatch.
The scorecard also stops sunk cost arguments. Prior effort does not make the current backend suitable, and annoyance does not make a new one suitable. Evidence can change during the assessment. If adding two indexes and fixing a tenant predicate removes the alleged scaling problem, update the score without defending the original migration idea.
FixMyMess uses a codebase diagnosis and expert verification to separate repairable defects in AI generated applications from architecture that needs rebuilding, and its free code audit can supply that first bounded inventory. The decision still belongs to the application's owners, especially where contracts, risk tolerance, and future workload are concerned.
Cut over only with a proved rollback
A migration decision is incomplete until the team can describe data movement, validation, cutover, and rollback. "Export and import Postgres" covers only part of a Supabase backend.
Inventory database schemas, extensions, roles, RLS policies, functions, triggers, scheduled jobs, Auth users and identity mappings, Storage objects and metadata, Realtime subscriptions, Edge Functions, secrets, webhook registrations, DNS, and every client configuration. Decide what moves, what stays, and what gets retired. Preserve stable user identifiers where possible, or map them explicitly because authorization and ownership rows often depend on them.
Choose a downtime migration for a small product when a maintenance window is acceptable. It is easier to reason about than dual writes. For a low downtime move, establish an initial copy, replicate later database changes, copy objects with checksums, freeze schema changes, validate lag, and plan how final writes switch. Dual writing from application code sounds safe but creates conflict rules and failure combinations that many small teams cannot test.
Define acceptance checks before copying data. Compare row counts by tenant and table, sums or hashes for selected stable fields, orphan counts, object counts and checksums, policy test results, API contract tests, and a sample of critical user journeys. Totals can match while ownership is wrong, so validate relationships and access through real user roles.
Keep the source read only or otherwise recoverable for an agreed window after cutover. State the rollback trigger in measurable terms: error rate, missing records, authentication failure, payment callback mismatch, or unacceptable replication lag. State who can call rollback and how writes made after cutover will return to the old system. A rollback plan that loses new writes is an emergency trade, not a full rollback.
For a repair, use the same discipline at smaller scale. Take a verified backup, apply migrations through versioned files, run policy and contract tests, watch database and application errors, and prepare a reversible change where PostgreSQL permits it. Supabase's migration guide warns that remote dashboard changes bypass local migration history; pull existing remote state, reconcile the history, and stop making untracked production edits.
Authentication needs its own rehearsal. Password hashes, social identity links, multifactor enrollment, refresh tokens, email templates, redirect rules, and session duration do not automatically transfer with database tables. Decide whether users keep active sessions, sign in again, or reset credentials. Test invitation, password recovery, account linking, and account deletion on the destination. A migration that preserves profile rows but locks out their owners has failed.
Storage also needs two validations. First compare object inventory, byte size, content type, and checksum. Then exercise access as the application does, including signed access, public objects, replacement, and deletion. Database rows can point to objects that were never copied, while copied objects can become public through a changed bucket rule. Keep object transfer logs long enough to investigate a customer report after cutover.
Run a dress rehearsal with a recent sanitized copy and record the duration of every phase. The rehearsal should produce the commands, owners, checkpoints, and abort conditions used on the real day. If the final data sync takes longer than the maintenance window or validation cannot finish before reopening writes, change the plan before production rather than hoping operators work faster under pressure.
The choice is ready when one option has passed mandatory controls, survived representative tests, and shown a lower total burden under believable forecasts. If neither option has done that, keep investigating. Production data does not reward confidence that outruns proof.
FAQ
Is it cheaper to repair a Supabase backend than migrate it?
Usually, if the faults are missing indexes, broken policies, exposed secrets, or untracked schema changes. Compare repair labor with migration build cost, parallel hosting, data transfer, validation, cutover, and the ongoing work of operating the destination.
How many RLS policies are too many?
There is no useful fixed number. Policies have become too complex when the team cannot state and test the allowed and denied behavior for each role, table, and operation.
Can slow Supabase queries be fixed without migrating?
Yes, in many cases. Use pg_stat_statements and execution plans to find expensive scans, poor estimates, repeated calls, and missing indexes before blaming the hosting platform.
Can I migrate only Supabase Edge Functions?
Yes. Moving long jobs or specialized compute to workers while keeping Supabase for Postgres, Auth, or Storage can remove the main constraint with less risk than a full migration.
Does moving to another Postgres host remove RLS complexity?
No. PostgreSQL policies and the underlying permission model still need design and tests, or the authorization decision must move into a separate application layer that you also have to operate.
What should I audit before a Supabase migration?
Audit schemas, extensions, roles, policies, Auth identities, Storage objects, Realtime use, functions, secrets, webhooks, backups, clients, and data flows. Include undocumented dashboard configuration because source control will not show it.
Is a custom backend safer than Supabase?
Not automatically. A custom backend gives you different controls and more responsibility, and weak authorization or secrets handling stays weak after a rewrite.
When do compliance requirements force a migration?
They force a move when a mandatory written control cannot be met through the available plan, configuration, process, or contract. Verify the destination satisfies that control before treating migration as the answer.
How long should a migration payback period be?
It should be shorter than the expected useful life of the destination architecture and acceptable to the business. Test the result under expected, high, and contracting demand instead of trusting one forecast.
What is the safest Supabase migration strategy?
Use the simplest strategy that meets the downtime requirement, with explicit inventories, contract tests, data validation, and a rollback trigger. Small products often reduce risk with a planned maintenance window instead of poorly tested dual writes.