Repository navigation
Remove pg_net from prod #321
Description
Key: SPR-14 · Type: HITL · Part of: #327 · Blocked by: #314 · Checked against: 3bb26de
Problem
Prod has the pg_net extension registered in schema public, and nothing in the repo uses it. On 2026-09-28 the Supabase security linter flagged it as WARN 0014_extension_in_public.
pg_net gives the database a server-side HTTP client (net.http_post, net.http_get). An unused HTTP client inside the database is attack surface with no benefit. The fix is to drop it. It cannot be moved, because it is not relocatable.
The repo also carries two false claims that pg_net "is relocatable". It still installs pg_net on every local reset. The Postgres upgrade (#322) waits for this drop.
Context
Labels: [prod] = read on prod on 2026-09-28. [local] = measured in the local container supabase_db_docsplus_supabase, not prod. It runs image supabase/postgres:15.8.1.073, the same version as prod. [repo] = read at HEAD 3bb26de06.
| Fact | Source |
|---|---|
The linter reports WARN extension_in_public: pg_net. |
[prod] |
Prod runs Postgres 15.8.1.073. The prod pg_net version was not read. |
[prod] |
Prod has the event trigger issue_pg_net_access. It is a Supabase platform trigger that grants access when pg_net is created. |
[prod] |
pg_net 0.14.0 lives in schema extensions. extrelocatable = f. Its control file names no schema. All its tables and functions live in schema net. |
[local] |
ALTER EXTENSION pg_net SET SCHEMA therefore fails. Moving it means drop and re-create, which costs the same as a drop. |
[local] |
anon holds USAGE on schema net and EXECUTE on net.http_post. Locally the browser cannot reach it, because packages/supabase/config.toml ~13 ([api] schemas) exposes only public and graphql_public. Prod grants and prod exposed schemas were not read. |
[local], [repo] |
The owner of pg_net is supabase_admin. Even so, the postgres role could run drop extension pg_net without CASCADE, inside a transaction that was rolled back. The drop also removed schema net. Prod was not tested. |
[local] |
No cron job calls net.*. No non-platform function body calls net.*. supabase_functions.hooks (Database Webhooks) has 0 rows. |
[local] |
Two Supabase platform functions do name net. (local check 2 returns exactly these two rows): extensions.grant_pg_net_access (the issue_pg_net_access trigger function) and supabase_functions.http_request (Database Webhooks). The precheck below skips these two. |
[local] |
No repo SQL calls net.*. The word pg_net appears only in comments about the move to pgmq, and in the three places this issue fixes. |
[repo] |
scripts/07-4-push-notifications-pgmq.sql ~26 (header DEPLOYMENT CHECKLIST) planned "Remove pg_net dependency after 1 week of stable operation". Nobody did it. |
[repo] |
The pg_net background worker runs two cleanup deletes, on net._http_response and net.http_request_queue. Each ran 176 times in a 352 s window (0.5 per second). The performance review puts the cost at about 21 ms of DB time per hour. |
[prod] |
Why the prechecks matter: a plpgsql body that names net.* is not a tracked dependency. It survives the drop and fails only when it runs. A cron job command is plain text, so it fails the same way.
What a drop loses: rows in net.http_request_queue and net._http_response. Both hold transient request data only.
The false claims in the repo:
packages/supabase/scripts/00-bootstrap.sql~9-13: a comment that says "pg_netis relocatable", thencreate extension if not exists pg_net with schema extensions;.packages/supabase/CLAUDE.md~33 (the rule "The seed batch runs with an emptysearch_path..."): "pg_netis relocatable and does not".packages/supabase/README.md~24 listspg_netamong the extensions that00-bootstrap.sqlcreates.
The real cause of the old 3F000 reset failure is still true. pg_net's control file names no schema. So a bare create extension pg_net under the seed's empty search_path raises 3F000: no schema has been selected to create in. Relocatability has nothing to do with it.
00-bootstrap.sql is not part of seed.sql. packages/supabase/generate-seed.ts ~11-18 (excludePatterns) excludes it. config.toml ~70 ([db.seed].sql_paths) runs it before seed.sql. So this change does not alter seed.sql.
What to do
-
[maintainer] Run three read-only prechecks on prod. Use the Supabase SQL editor. Post the results of all five queries on this issue. Every query only reads.
-- Check 1: no cron job calls net.* (expect 0 rows) select jobid, jobname from cron.job where command ~ '\mnet\.'; -- Cross-check: expect 12. Fewer means this role cannot see every job. select count(*) from cron.job; -- Check 2: no function body outside schema net calls net.* (expect 0 rows) select n.nspname, p.proname from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname <> 'net' and p.prosrc ~ '\mnet\.' and (n.nspname, p.proname) not in ( ('extensions', 'grant_pg_net_access'), ('supabase_functions', 'http_request') ); -- Check 3: no Database Webhooks (expect 0; "relation does not exist" also passes) select count(*) from supabase_functions.hooks; -- Record for rollback: where pg_net lives and its version select extname, extnamespace::regnamespace as schema, extversion from pg_extension where extname = 'pg_net';
If any check fails, stop. Name the caller on this issue. Do not drop
pg_net. The fallback is drop and re-createwith schema extensions, after the caller is ruled on. -
[agent] Remove the install line from bootstrap. In
packages/supabase/scripts/00-bootstrap.sql, delete lines ~9-13. That is the four-line comment starting "pg_netis relocatable" and thecreate extension if not exists pg_net with schema extensions;line. Keeppg_cron,pgmqand schemainternal. -
[agent] Fix the rule in
packages/supabase/CLAUDE.md~33. Replace only thepg_netclause. Keep the rest of the bullet. Suggested text, with this issue's number in place of#NNN:An extension whose control file names no schema fails:
pg_cronandpgmqname theirs.pg_netnamed none. So a barecreate extension pg_netraised3F000: no schema has been selected to create inand killed the whole reset on00-bootstrap.sql. The repo no longer installspg_net(#NNN).Keep the next sentence, "Give any such extension
with schema extensions...". -
[agent] Fix
packages/supabase/README.md~24. Change "(pg_cron, pgmq, pg_net, schemainternal)" to "(pg_cron, pgmq, schemainternal)". -
[agent] Add one paired migration. Name it
packages/supabase/migrations/<YYYYMMDDHHMMSS>_drop_pg_net.sql. Its timestamp must sort after every file inpackages/supabase/migrations/, including the one Port the 2026-09-28 prod grant and RLS fix into scripts and a paired migration #314 adds. At HEAD the newest file is20260928130000_reject_password_sign_in_hook.sql. Body:-- Drops pg_net. The repo moved push and email to pgmq and no longer calls -- net.*. pg_net is not relocatable, so it cannot move out of public. -- Paired with scripts/00-bootstrap.sql. The guard refuses the drop if a caller exists. do $$ begin if exists (select 1 from cron.job where command ~ '\mnet\.') then raise exception 'pg_net drop refused: a cron job calls net.*'; end if; if exists ( select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname <> 'net' and p.prosrc ~ '\mnet\.' and (n.nspname, p.proname) not in ( ('extensions', 'grant_pg_net_access'), ('supabase_functions', 'http_request') ) ) then raise exception 'pg_net drop refused: a function body calls net.*'; end if; if to_regclass('supabase_functions.hooks') is not null then if exists (select 1 from supabase_functions.hooks) then raise exception 'pg_net drop refused: a Database Webhook exists'; end if; end if; end; $$; drop extension if exists pg_net;
Do not add
cascade. A tracked dependency must stop the drop loudly. The guard and the drop were run on local, inside a rolled-back transaction: the guard passed and the drop succeeded. -
[agent] Verify locally. From the repo root, first apply the migration to the running local database:
docker exec -i supabase_db_docsplus_supabase psql -U postgres -d postgres < packages/supabase/migrations/<file>.sql
Then prove a fresh reset works withoutpg_net:bun run --filter @docs.plus/supabase_back reset. A reset wipes local data for every worktree, so tell the maintainer first. Then run the queries in Verification. -
[agent] Check the generated files. Run
bun run --filter @docs.plus/supabase_back seedandbun run --filter @docs.plus/supabase_back types. Expect no diff inpackages/supabase/seed.sqlorapps/webapp/src/types/supabase.ts. Schemanetis not in the generated types. If either file changes, include the regenerated file and say why. -
[maintainer] Ship to prod. Use the house push procedure that Ship the three migrations that prod's history lacks #313 uses. Run the CLI from
packages/supabase. Runbunx supabase migration listfirst. Set[db.migrations] enabled = trueonly while pushing, then set it back tofalse. Push only after Port the 2026-09-28 prod grant and RLS fix into scripts and a paired migration #314's migration is on prod.- If the push fails with a permission error on
drop extension, turnpg_netoff in the dashboard (Database > Extensions). Then push again.if existsmakes the migration a no-op. - If the guard raises, the push stops. Post the message here and go back to step 1.
- If the push fails with a permission error on
-
[maintainer] Post-checks on prod. Run the prod queries in Verification. Re-run the Security Advisor (Advisors > Security Advisor).
-
[agent] Commit only when the maintainer asks, through
/commit-review.
Rollback, if a hidden caller shows up later: turn pg_net on again in Database > Extensions. The SQL form is create extension if not exists pg_net with schema extensions;. Old queue and response rows do not come back.
Out of scope
- The grant and RLS fix port and its migration: Port the 2026-09-28 prod grant and RLS fix into scripts and a paired migration #314.
- The push procedure and the three missing migrations: Ship the three migrations that prod's history lacks #313.
- Default privileges for new objects: Stop new tables and functions from opening by default #316. The post-push grant guard: Add a read-only grant guard to run after each
db push#317. - The Postgres upgrade, which waits for this drop: Upgrade prod Postgres from 15.8.1.073 #322.
- The
pgjwtcheck before the upgrade: Upgrade prod Postgres from 15.8.1.073 #322. - Historical
pg_netcomments inscripts/07-4-push-notifications-pgmq.sql(~9, ~17, ~26, ~182, ~268, ~293, ~672) andscripts/07-5-email-notifications-pgmq.sql~8. They describe the move to pgmq. Leave them. - The same comments in applied migrations such as
20260519200000_scripts_functions_triggers_parity.sql. Never edit an applied migration.
Acceptance criteria
- The maintainer posted the results of prod checks 1-3 on this issue, and all three passed.
-
packages/supabase/scripts/00-bootstrap.sqlhas nopg_netline and no "relocatable" comment. -
packages/supabase/CLAUDE.mdandpackages/supabase/README.mdno longer saypg_netis relocatable or installed. - One new migration
*_drop_pg_net.sqlexists. It sorts last inpackages/supabase/migrations/. It has the guard block anddrop extension if exists pg_net;withoutcascade. - A fresh local reset succeeds, and local
pg_extensionhas nopg_netrow. -
seed.sqlandsupabase.tsare unchanged, or the regenerated files are in the change with a reason. - After the push, prod
pg_extensionhas nopg_netrow. - After the push, the Security Advisor shows no
extension_in_publicrow. - After the push, every cron job run in the next hour has status
succeeded.
Verification
Repo, from the repo root, after steps 2-4. Both commands print nothing.
grep -n -i -E "relocatable|pg_net" packages/supabase/scripts/00-bootstrap.sql packages/supabase/README.md
grep -rn -E "net\.(http_|_http)" packages/supabase/scripts packages/supabase/migrationsgrep -n "pg_net" packages/supabase/CLAUDE.md prints only the rewritten sentence, with "named none" and no "relocatable".
Local, after step 6:
select count(*) from pg_extension where extname = 'pg_net'; -- expect 0
select count(*) from pg_namespace where nspname = 'net'; -- expect 0Prod, after step 8 (read-only):
select count(*) from pg_extension where extname = 'pg_net'; -- expect 0
select j.jobname, d.status, count(*)
from cron.job_run_details d
join cron.job j on j.jobid = d.jobid
where d.start_time > now() - interval '1 hour'
group by 1, 2
order by 1, 2; -- expect status succeeded onlyHouse rules
- Scripts and migrations are paired. Edit
scripts/00-bootstrap.sqland add the migration in the same change. Never edit an applied migration (packages/supabase/CLAUDE.md). - Never hand-edit
packages/supabase/seed.sqlorapps/webapp/src/types/supabase.ts. Regenerate them with the workspace scripts and include any diff. [db.migrations] enabledstaysfalsein git. Set it totrueonly while pushing. Runbunx supabase migration listbefore any push or repair.- Bun only. Never run
npm,yarn,pnpm, ornpx. - No commits unless asked. No
git add,git commit, orgit push. The maintainer commits through/commit-review.
Activity
fix(supabase): harden grants and notification gates
HMarzban commentedon Oct 19, 2026
Prod check after the 2026-10-19
db push(read-only)Migration
20261014121500_drop_pg_netis applied on prod.pg_extensionhas nopg_netrow, and thenetschema is gone (checked 16:55 UTC).extension_in_publicrow.aggregate_document_view_statscleanup-push-subscriptionscompile_digest_emailsmessage_counter_batch_jobprocess_document_views_queueprocess_email_queueupdate-user-status