Files
braidsbymira/supabase/migrations/20260808000003_notify_triggers.sql
belviskhoremk 8c1522a2b4 chore: deploy Phase 2A backend to live Supabase project (Task 9)
Substitutes the real admin UID everywhere the placeholder appeared,
points the Edge Functions base URL at the deployed project, and adds
production JWT verification: trigger functions now send the anon key
as a bearer token so verify_jwt can stay enabled instead of leaving
the functions open to unauthenticated calls from the public internet.

Also adds scripts/create-admin.ts (one-off helper used to create
Mira's admin auth user), documents the seed script's non-VITE env
vars in .env.example, and closes a gitignore gap where plain .env
wasn't excluded alongside .env.local.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 09:59:33 +02:00

128 lines
5.6 KiB
PL/PgSQL

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_<project> 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_<project> 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://<project-ref>.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();