Skip to content

Deleting a Supabase wrapper in the Dashboard: what one row's drop takes with it

A wrapper connection created in the Dashboard owns its foreign data wrapper, so deleting the row removes the connection and nothing else. The list also renders connections that were created in SQL, where several remote servers can sit on one foreign data wrapper - and there the same Delete is a statement about every one of them.

TL;DR:

  • The list is one row per remote server, named after its wrapper. getFDWsSql reads one row per pg_foreign_server and labels it with w.fdwname, so five servers on one foreign data wrapper render as five rows with the same name and nothing on the page distinguishes them (X01b).1
  • Delete runs drop foreign data wrapper if exists <name> cascade, then deletes one Vault secret named after the wrapper.1 On a shared wrapper that is a statement about all five connections: servers 5 -> 0, foreign tables 5 -> 0, and the view and materialized view built on the siblings’ tables 2 -> 0 (X01b).
  • The dialog does warn, and the SQL is not refused. It names the wrapper and reads “This will also remove all tables created with this wrapper”; the cascade then removes what a shared wrapper holds beyond that (X01b).1
  • The Vault secrets are left behind. The delete removes only the secret named after the wrapper, so in the shared case all five credentials stayed in Vault after every server that used them was gone (X01b).
  • Edit is a delete plus a create, so saving a row unchanged is as destructive as deleting it. Editing one of the five rows and changing no field left 1 server, 1 foreign table, 0 of the 2 views, and added a sixth secret <fdw>_sa_key_id beside the five originals (X01c).
  • Removing exactly one connection is a SQL sequence, and Postgres refuses the shortcuts. The default RESTRICT rejected drop foreign data wrapper, drop server and drop foreign table with 2BP01 ... because other objects depend on it until the dependent object was gone first; dropping view, then table, then server removed one connection and left the siblings, their view and their materialized view intact (X01d).
  • The Dashboard cannot create a shared wrapper. Create always emits create foreign data wrapper <wrapper_name> and names the server <wrapper_name>_server, and reusing an existing wrapper’s name is refused 42710: foreign-data wrapper "..." already exists with the transaction rolled back; a connection made in the Dashboard was removed alone, with its secret (X01e, X01a).
one shared wrapper (SQL-created)Wrappers listone row per pg_foreign_serverlabelled with w.fdwnameforeign data wrapper(the row's name)Delete: DROP ... CASCADEVault secrets5, untoucheddeletes one secret named after the wrapperremote servers5 of them, shared caseforeign tables5view + matview2

Reading the diagram as text:

  1. Each list row is a pg_foreign_server row, labelled with its wrapper’s name rather than the server’s, so a shared wrapper produces several identically labelled rows (X01b).
  2. Delete drops the wrapper with cascade, which takes every server on it, then every foreign table on those servers, then any view or materialized view over those tables (X01b).
  3. The Delete’s Vault step removes one secret, named after the wrapper, and leaves the per-connection secrets in place (X01b).
  4. A wrapper created by the Dashboard owns its single server, so for those rows the path above stops after step 2’s first level, and the 5 -> 4 counts in X01a are what a Dashboard-created connection looks like when removed.

Read from the pinned source (2026-09-23), which is what the replay used:1

ActionStatementConfirmation textVault
Deletedrop foreign data wrapper if exists <name> cascade”This will also remove all tables created with this wrapper”, naming the wrapperdeletes the secret named <name>_<encrypted option>, e.g. <name>_sa_key_id
Editthe same delete, then a create for the edited row only”Saving changes will drop the existing wrapper and recreate it.”the delete’s secret removal, then the new one
Createcreate foreign data wrapper <wrapper_name> plus a server named <wrapper_name>_server-creates the secret the new server references

Two consequences follow from the shapes rather than from the wording. Delete and Edit are statements about a wrapper, while the row you clicked is a server; and Edit re-creates only the row you edited, so its Delete half takes the same scope as a Delete.

Five BigQuery connections per sub-case, one foreign table each, a Vault secret per server, a view on server 2’s table and a materialized view (with no data) on server 3’s (X01, 2026-09-23):

CaseSetupActionList rows / distinct namesServersForeign tablesViews + matviewsVault secrets
X01aone wrapper per connection (Dashboard create x5)Dashboard delete5 / 55 -> 45 -> 4-5 -> 4
X01bone shared wrapperDashboard delete5 / 15 -> 05 -> 02 -> 05 -> 5
X01cone shared wrapperDashboard edit, saved unchanged5 / 15 -> 15 -> 12 -> 05 -> 6
X01done shared wrapperdrop view / drop foreign table / drop server5 / 15 -> 45 -> 43 -> 25 -> 5
X01eone shared wrapperDashboard create reusing the wrapper’s name5 / 15 -> 55 -> 52 -> 25 -> 5

The distinct-name column is the one to read first: with five servers on one wrapper, the list gives five rows that all say the same thing, so the page cannot show a user which rows their Delete is about.

X01c is the row that catches people who think of Edit as a safer action than Delete. Nothing was changed in the form; the save still ran the delete half, and the surviving connection is the one being edited. The extra secret is the re-create’s, named <fdw>_sa_key_id, while the five originals stayed.

X01e explains where a shared wrapper comes from. The Dashboard refuses the name of an existing wrapper (42710: foreign-data wrapper "..." already exists) and rolls back with no secret left behind, so the shared shape is only reachable through SQL - which is also the shape a migration guide reaches for when it writes create server ... foreign data wrapper <name> against a wrapper created once.

The same removal, done in SQL, is refused three times before it is allowed. Postgres takes RESTRICT as the default for the drop commands, and each over-reach came back with the same SQLSTATE (X01d):2

StatementResult while dependants exist
drop foreign data wrapper <name> (no cascade)2BP01 ... because other objects depend on it, since servers exist3
drop server <name>2BP01, since its foreign table exists
drop foreign table <table>2BP01, since a view depends on it

In that order - dependants first, cascade never - one connection goes and the other four keep working, with the siblings’ view and materialized view intact (5 -> 4 servers, 5 -> 4 tables, 3 -> 2 views, 5 secrets untouched). The two catalog queries a DBA needs to answer “what else is on this wrapper” also ran in X01d and returned the expected rows: a connection-to-wrapper query, and the foreign tables belonging to one server via pg_foreign_table.ftserver.

The wrapper you leave in place is the account of what the project can still reach. A server removed without its wrapper leaves the wrapper in the list with no server behind it, and its Vault secret in place.

  • No browser session was used. Studio’s SQL is imported from pg-meta at a pinned commit and replayed through the Management API, so what a click does in a later Studio release is that release’s file, not this measurement.
  • Run 1’s secret handling is not evidence: its hand copy deleted <fdw>_sa_key where Studio deletes <fdw>_sa_key_id. Only Run 2’s counts are cited.
  • The legacy branch was not run. Projects on an older Postgres with the pgsodium-based secret handling (wrappers at or below the last version in Studio’s legacy list) are governed by different code, and Postgres 15 was not exercised.
  • Whether a cascade reaches a function body that references a foreign table was not tested, so the blast radius above is bounded by servers, foreign tables, views and materialized views.
  • Only BigQuery wrappers were used, with dummy credentials; the counts are catalog counts and no remote endpoint was ever contacted.
  • One project per sub-case on a free plan, ap-southeast-1, one run day (2026-09-23).
  • The counts are catalog reads taken before and after each action - servers, foreign tables, views and materialized views, and Vault secrets - on one project per sub-case, and three consecutive executions agreed.
  • The X01d view count starts at 3 rather than 2 because that sub-case adds a view of its own to drive the RESTRICT refusals, then drops it; the two views X01b and X01c destroy are the shared setup’s.
  • “Left behind” for Vault is a count of rows in the secrets store, not a claim about how long they live: nothing here expired, rotated or was revoked.
  • The 2BP01 and 42710 SQLSTATEs are verbatim from the run; the dialog sentences are verbatim from the pinned Studio source, not from a rendered page.
PracticeEvidenceModule
Do not delete a wrapper from the Dashboard until you know how many servers are on itDelete is drop foreign data wrapper ... cascade, and the list labels every row with the wrapper’s name, so five connections can read as five different rowsX01b
Ask the catalog before the click, not the pageA connection-to-wrapper query and a foreign-table query via pg_foreign_table.ftserver both returned the expected rows in the runX01d
Remove one connection with the dependants first and no cascadeRESTRICT refused all three over-reaches with 2BP01, and the ordered path left the siblings runningX01d
Treat Edit as a delete of the row plus a re-create, and do not save one to “look at it”Saving one of five rows unchanged left 1 server and 1 foreign table and destroyed the siblings’ view and materialized viewX01c
Delete the Vault secrets yourself after a Dashboard deleteThe delete removes only the secret named after the wrapper; in the shared case all five connection secrets survivedX01b
Keep Dashboard-created connections as the default, and prefer one connection per wrapperA Dashboard-created connection is removed alone with its secret; the shared shape only arrives through SQL, and reusing a wrapper’s name is refused 42710X01a, X01e

The experiment is experiments/wrappers-delete-scope in supabase-lab. The Studio SQL is regenerated from source, which is what keeps the replay honest:

git -C <supabase-checkout> archive origin/master packages/pg-meta | tar -x -C <dir>
bun experiments/wrappers-delete-scope/scripts/gen-studio-sql.ts <dir>/packages/pg-meta/src <commit>

Then each sub-case runs against a disposable project through the query endpoint with --experiment wrappers-delete-scope --only X01 --destructive. Every project is created and deleted within the hour, because a free-plan organization cannot take the instance size the other experiments pass.

ClaimHow it was checked
The list is one row per remote server, labelled with the wrapper’s nameRead from the pinned Studio source, getFDWsSql; the 5 rows / 1 distinct name in X01b is the measurement of it1
Delete runs drop foreign data wrapper if exists <name> cascade and one Vault secret deletePinned Studio source, replayed as generated SQL; the counts are X01b
One Delete removed 5 servers, 5 foreign tables, a view and a materialized view, and left 5 secretsMeasured, X01b, catalog counts before and after, three executions
Edit saved unchanged left 1 server and added a secretMeasured, X01c
The ordered drop removes exactly one connectionMeasured, X01d
RESTRICT refused each over-reach with 2BP01Measured, X01d, verbatim SQLSTATE; the default is documented2, 3
A Dashboard create refuses an existing wrapper name with 42710Measured, X01e
A Dashboard-created connection is removed alone, with its secretMeasured, X01a
One wrapper can front several remote servers with independent foreign tablesDocumented, not tested4
ModuleExperimentTestArtifact
X01awrappers-delete-scopex01-delete-scope.tsnone published
X01bwrappers-delete-scopex01-delete-scope.tsnone published
X01cwrappers-delete-scopex01-delete-scope.tsnone published
X01dwrappers-delete-scopex01-delete-scope.tsnone published
X01ewrappers-delete-scopex01-delete-scope.tsnone published
  1. supabase/supabase, “packages/pg-meta/src/sql/studio/database/fdw.ts” (Studio’s own SQL builders: getFDWsSql, getDeleteFDWSql, getUpdateFDWSql, getCreateFDWSql). https://github.com/supabase/supabase/blob/ab7783f2ca/packages/pg-meta/src/sql/studio/database/fdw.ts ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. PostgreSQL Global Development Group, “DROP FOREIGN DATA WRAPPER,” PostgreSQL Documentation. https://www.postgresql.org/docs/current/sql-dropforeigndatawrapper.html ↩ ↩2

  3. PostgreSQL Global Development Group, “Appendix A. PostgreSQL Error Codes,” PostgreSQL Documentation. https://www.postgresql.org/docs/current/errcodes-appendix.html ↩ ↩2

  4. Supabase, “Foreign Data Wrappers,” Supabase Docs. https://supabase.com/docs/guides/database/extensions/wrappers/overview ↩