-- Follow-up to 20260808000003, prompted by final whole-branch review before -- merge. Two problems with the previous migration's approach: -- -- 1. It hardcoded the LIVE PRODUCTION base URL and anon key as the committed -- default. Migrations replay on every `supabase db reset`, so any local -- dev/test run was silently wired to the live project — once a Resend key -- is configured, local testing would send real emails to Mira's inbox -- with test data. Fixed by making the committed default the LOCAL dev -- values again; production values are applied out-of-band (see below), -- never committed. -- -- 2. `verify_jwt = true` + sending the (public) anon key as a bearer token -- does not actually stop an attacker: the anon key already ships in the -- deployed frontend bundle, so anyone can copy it and POST directly to -- the function URLs to trigger arbitrary outbound emails. Fixed by -- adding a real shared secret (`edge_functions_notify_secret()`), known -- only to this database and the Edge Functions' secret store, checked by -- both functions before doing any work. -- -- Both new base-url/anon-key/secret functions move to a `private` schema -- (not exposed via PostgREST — only `public` is listed under `[api]` in -- config.toml) with EXECUTE explicitly revoked from anon/authenticated, so -- they can never become an anon-callable RPC endpoint. The previous -- `public.edge_functions_base_url()` / `public.edge_functions_anon_key()` -- were reachable that way (POST /rest/v1/rpc/edge_functions_anon_key) — -- harmless only because the value itself was already public. Dropped here. -- -- PRODUCTION DEPLOY: after this migration is pushed, apply the real values -- out-of-band (never commit these — the anon key is public but the base URL -- is project-specific noise, and the notify secret is a genuine secret): -- -- supabase db query --linked "create or replace function private.edge_functions_base_url() returns text language sql immutable as \$\$ select 'https://.supabase.co/functions/v1' \$\$;" -- supabase db query --linked "create or replace function private.edge_functions_anon_key() returns text language sql immutable as \$\$ select '' \$\$;" -- supabase db query --linked "create or replace function private.edge_functions_notify_secret() returns text language sql immutable as \$\$ select '' \$\$;" -- supabase secrets set NOTIFY_WEBHOOK_SECRET= -- supabase functions deploy on-booking-created -- supabase functions deploy on-inquiry-created create schema if not exists private; drop function if exists public.edge_functions_base_url(); drop function if exists public.edge_functions_anon_key(); create or replace function private.edge_functions_base_url() returns text language sql immutable as $$ select 'http://host.docker.internal:54321/functions/v1' $$; create or replace function private.edge_functions_anon_key() returns text language sql immutable as $$ select 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' $$; -- Local dev placeholder only — production sets a real random value via the -- out-of-band command above, never committed to this file. create or replace function private.edge_functions_notify_secret() returns text language sql immutable as $$ select 'local-dev-notify-secret' $$; revoke execute on function private.edge_functions_base_url() from public, anon, authenticated; revoke execute on function private.edge_functions_anon_key() from public, anon, authenticated; revoke execute on function private.edge_functions_notify_secret() from public, anon, authenticated; create or replace function public.notify_booking_created() returns trigger language plpgsql security definer set search_path = '' as $$ begin perform net.http_post( url := private.edge_functions_base_url() || '/on-booking-created', headers := jsonb_build_object( 'Content-Type', 'application/json', 'Authorization', 'Bearer ' || private.edge_functions_anon_key(), 'X-Notify-Secret', private.edge_functions_notify_secret() ), body := jsonb_build_object('record', to_jsonb(new)) ); return new; end; $$; create or replace function public.notify_inquiry_created() returns trigger language plpgsql security definer set search_path = '' as $$ begin perform net.http_post( url := private.edge_functions_base_url() || '/on-inquiry-created', headers := jsonb_build_object( 'Content-Type', 'application/json', 'Authorization', 'Bearer ' || private.edge_functions_anon_key(), 'X-Notify-Secret', private.edge_functions_notify_secret() ), body := jsonb_build_object('record', to_jsonb(new)) ); return new; end; $$;