-- Migration 001: users table -- Mirrors GoTrue auth.users — populated by trigger on first OIDC login. -- ── Enums ───────────────────────────────────────────────────────────────────── CREATE TYPE public.language_code AS ENUM ('en', 'es'); CREATE TYPE public.avatar_type AS ENUM ('initials', 'emoji', 'upload'); -- ── Table ───────────────────────────────────────────────────────────────────── CREATE TABLE public.users ( id uuid PRIMARY KEY, -- matches auth.users.id (GoTrue UUID) email text NOT NULL, display_name text NOT NULL DEFAULT '', language public.language_code NOT NULL DEFAULT 'en', avatar_type public.avatar_type NOT NULL DEFAULT 'initials', avatar_emoji text NULL, avatar_url text NULL, -- signed URL from Supabase Storage created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; -- ── updated_at trigger ──────────────────────────────────────────────────────── CREATE OR REPLACE FUNCTION public.set_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$; CREATE TRIGGER users_set_updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); -- ── Sync trigger: GoTrue auth.users → public.users on first OIDC login ──────── -- GoTrue inserts into auth.users after a successful Keycloak OIDC exchange. -- We mirror display_name and detect language from raw_user_meta_data claims. CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_display_name text; v_language public.language_code; v_raw jsonb; BEGIN v_raw := NEW.raw_user_meta_data; -- Build display name: full_name > name > email prefix v_display_name := COALESCE( NULLIF(trim(v_raw->>'full_name'), ''), NULLIF(trim(v_raw->>'name'), ''), split_part(NEW.email, '@', 1) ); -- Detect language from OIDC locale claim; default en v_language := CASE WHEN v_raw->>'locale' ILIKE 'es%' THEN 'es'::public.language_code ELSE 'en'::public.language_code END; INSERT INTO public.users (id, email, display_name, language) VALUES (NEW.id, NEW.email, v_display_name, v_language) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, -- Preserve user-set display name; only backfill if still empty display_name = CASE WHEN public.users.display_name = '' THEN EXCLUDED.display_name ELSE public.users.display_name END; RETURN NEW; END; $$; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();