create extension if not exists pg_net with schema extensions; -- The Edge Functions base URL is environment-specific: for local development -- it points at the local functions runtime; Task 9 updates it to the real -- deployed URL in production. -- -- NOTE ON IMPLEMENTATION: the brief originally called for storing this as a -- database-level GUC via `alter database postgres set -- app.settings.edge_function_base_url = '...'`, read back in each trigger -- function via current_setting(). That statement fails locally with -- `permission denied to set parameter "app.settings.edge_function_base_url"` -- (SQLSTATE 42501): in this Supabase CLI version's local Postgres stack, the -- `postgres` role that runs migrations is deliberately NOT a superuser -- (Create role/Create DB/Replication/Bypass RLS only) — only `supabase_admin` -- is superuser, mirroring the hosted permission model. Setting an arbitrary -- custom GUC at the database level requires either superuser or a `GRANT SET -- ON PARAMETER` from a superuser, and the migration has no way to grant -- itself that privilege. Empirically verified via `docker exec -- supabase_db_ psql -U postgres -d postgres -c "alter database -- postgres set app.settings.edge_function_base_url = '...'"` (denied) vs the -- same command as `-U supabase_admin` (succeeds). -- -- Rather than depend on a migration-time GRANT (fragile, and would still -- need to run as supabase_admin some other way), the base URL is inlined as -- a literal constant in each trigger function below. Task 9 (or any future -- environment change) updates it by editing the constant here and -- re-migrating — the same operational shape the GUC approach would have had. -- -- Default here is the local Supabase CLI functions gateway address, -- confirmed to resolve from inside the Postgres container in Step 5 below -- (curl from supabase_db_ to http://host.docker.internal:54321 -- reached the edge runtime and got a real HTTP response, not a connection -- failure). -- -- SECOND CORRECTION: the brief's SQL called `extensions.net.http_post(...)` -- (treating `net` as a schema nested under `extensions`). Empirically, even -- though `pg_extension.extnamespace` for pg_net reports `extensions` (from -- `with schema extensions` above), pg_net's install script hardcodes its own -- `net` schema for the actual functions (confirmed via `select n.nspname, -- p.proname from pg_proc p join pg_namespace n on p.pronamespace = n.oid -- where p.proname = 'http_post'` -> `net.http_post`). Calling -- `extensions.net.http_post` therefore fails with "cross-database -- references are not implemented: extensions.net.http_post" (Postgres reads -- `extensions.net` as database.schema). Fixed to call `net.http_post` -- directly below. -- Single source of truth for the Edge Functions base URL, used by both -- notify_booking_created() and notify_inquiry_created() below. Task 9 -- (production deploy) MUST update this function's return value to the real -- deployed Edge Functions URL (e.g. https://.supabase.co/functions/v1) -- when promoting to production. Because both trigger functions call through -- this single function rather than each holding their own copy of the -- literal, updating this one definition (and re-running the migration or an -- equivalent `create or replace function` in production) is sufficient to -- repoint both notification paths — there is no second copy to remember to -- edit. create or replace function public.edge_functions_base_url() returns text language sql immutable as $$ select 'https://ylxppnclfpnnolnjkbwv.supabase.co/functions/v1' $$; -- The Edge Functions gateway enforces JWT verification (verify_jwt = true, -- see supabase/config.toml) in production, since these functions are -- reachable over the public internet and would otherwise let anyone trigger -- an outbound email send by POSTing directly to the URL. pg_net's request -- carries no Authorization header by default, so the trigger must supply -- one. The anon key is not a secret (it already ships in the frontend -- bundle), so embedding it here as a literal, single-source-of-truth -- constant is safe — the same pattern as edge_functions_base_url() above. create or replace function public.edge_functions_anon_key() returns text language sql immutable as $$ select 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlseHBwbmNsZnBubm9sbmprYnd2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODY0NTkyNzYsImV4cCI6MjEwMjAzNTI3Nn0.j0UaxlBvDctsDOXxPuWj43XvbObtX68-mjNf3AAefX8' $$; create or replace function public.notify_booking_created() returns trigger language plpgsql security definer set search_path = '' as $$ begin perform net.http_post( url := public.edge_functions_base_url() || '/on-booking-created', headers := jsonb_build_object( 'Content-Type', 'application/json', 'Authorization', 'Bearer ' || public.edge_functions_anon_key() ), body := jsonb_build_object('record', to_jsonb(new)) ); return new; end; $$; create trigger booking_created_notify after insert on public.bookings for each row execute function public.notify_booking_created(); create or replace function public.notify_inquiry_created() returns trigger language plpgsql security definer set search_path = '' as $$ begin perform net.http_post( url := public.edge_functions_base_url() || '/on-inquiry-created', headers := jsonb_build_object( 'Content-Type', 'application/json', 'Authorization', 'Bearer ' || public.edge_functions_anon_key() ), body := jsonb_build_object('record', to_jsonb(new)) ); return new; end; $$; create trigger inquiry_created_notify after insert on public.inquiries for each row execute function public.notify_inquiry_created();