Adds pgTAP tests and migration to create the salon-images storage bucket
with RLS policies allowing public read access and admin-only write access.
Tests verify anon users can read but not upload, and admin can upload.
Fixes issues raised by the whole-branch review before merge:
- Critical: the previous migration hardcoded the live production
functions URL and anon key as the committed default, so every local
`supabase db reset` silently pointed dev/test triggers at the real
project. Adding a new migration instead of editing the already-applied
one (checksum/drift safety), with local dev values as the committed
default; production values are applied out-of-band, never committed.
- Important: verify_jwt=true plus sending the anon key as a bearer
token didn't actually stop anyone, since the anon key is public
(ships in the frontend bundle) — confirmed empirically, a request
with a valid anon key but no shared secret now gets a real 401.
Added a random shared secret known only to the database and the
Edge Functions' secret store.
- Important: notification emails interpolated raw contact fields into
HTML with no escaping. Added escapeHtml() and tests proving injected
markup renders as literal text, not live markup.
- Important: the URL/key/secret functions moved from public to a
private schema with EXECUTE revoked from anon/authenticated, closing
an unintended PostgREST RPC endpoint (verified 404 after the fix).
Also adds a test:functions npm script now that Deno is confirmed
available and the shared email builders' tests pass (12 total incl.
the new escaping cases).
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
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
notify_booking_created() and notify_inquiry_created() each held their own
literal copy of the local Edge Functions base URL. Introduce
public.edge_functions_base_url() as the single source of truth so Task 9's
production deploy only needs to update one function definition instead of
two independent literals.
Wires pg_net-based database triggers on bookings/inquiries to invoke the two
Edge Functions on insert. Two empirically-discovered fixes over the brief's
literal SQL were required to make this work locally:
- ALTER DATABASE ... SET for a custom GUC needs superuser, but the local
migration role (postgres) isn't superuser in this Supabase CLI stack (only
supabase_admin is) - inlined the base URL as a literal constant in each
trigger function instead.
- pg_net's http_post lives in schema `net`, not `extensions.net` - fixed the
function calls accordingly.
Also disabled verify_jwt for both functions in config.toml since they're
invoked by pg_net (no Authorization header), not by end-user clients.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Adds supabase/functions/ (deno.json, _shared/email.ts) with pure
buildBookingNotificationEmail/buildInquiryNotificationEmail functions
for Task 6's Edge Functions, tested via Deno's test runner. Excludes
supabase/functions/** from the root Vitest config, which otherwise
tried to resolve the jsr: import and failed.
Replace the blanket select/insert/update/delete grant to anon/authenticated/
service_role with privilege-scoped grants: anon/authenticated get select-only
on the public-read tables plus insert-only on bookings/inquiries; full CRUD
is reserved for authenticated (narrowed to the admin UID by RLS) and
service_role. Table grants are now a real second layer of defense instead of
mirroring RLS. Updates two pgTAP assertions whose blocking mechanism moved
from RLS-filtered-empty to grant-level permission-denied, and adds a new
assertion proving anon cannot UPDATE bookings.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
Includes explicit GRANTs for anon/authenticated/service_role on all
public tables: the Supabase CLI's current default no longer
auto-exposes new tables, so RLS policies from the initial schema were
unreachable without them (every request failed at the privilege check
before RLS was evaluated).
Initializes the local Supabase project (supabase/config.toml) and adds
a thin src/lib/supabase.ts wrapper around @supabase/supabase-js that
reads connection details from Vite env vars, tested with a mocked
createClient.