-- 00001_initial_schema.sql -- MovieDice: initial database schema -- Tables: users, groups, group_members, movies, landing_reel_posters -- ============================================================================= -- users -- ============================================================================= CREATE TABLE public.users ( id uuid PRIMARY KEY, -- maps to auth.users.id (Supabase Anonymous Sign-In) display_name text NOT NULL CONSTRAINT users_display_name_length CHECK (char_length(display_name) BETWEEN 1 AND 30) CONSTRAINT users_display_name_no_html CHECK (display_name !~ '[<>]') CONSTRAINT users_display_name_no_control CHECK (display_name !~ '[\x00-\x1F\x7F]'), avatar_color text CONSTRAINT users_avatar_color_hex CHECK (avatar_color IS NULL OR avatar_color ~ '^#[0-9a-fA-F]{6}$'), recovery_code text, -- Argon2id hashed, nullable, single-use last_active_at timestamptz NOT NULL DEFAULT now(), created_at timestamptz NOT NULL DEFAULT now() ); -- ============================================================================= -- groups -- ============================================================================= CREATE TABLE public.groups ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL CONSTRAINT groups_name_length CHECK (char_length(name) BETWEEN 1 AND 50) CONSTRAINT groups_name_no_html CHECK (name !~ '[<>]') CONSTRAINT groups_name_no_control CHECK (name !~ '[\x00-\x1F\x7F]'), invite_code text NOT NULL UNIQUE, created_by uuid NOT NULL REFERENCES public.users(id), created_at timestamptz NOT NULL DEFAULT now() ); -- Note: invite_code already indexed by the UNIQUE constraint -- ============================================================================= -- group_members -- ============================================================================= CREATE TABLE public.group_members ( group_id uuid NOT NULL REFERENCES public.groups(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, role text NOT NULL DEFAULT 'member' CONSTRAINT group_members_role_valid CHECK (role IN ('admin', 'member')), joined_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (group_id, user_id) ); CREATE INDEX idx_group_members_user_id ON public.group_members (user_id); -- Note: group_id already indexed as leading column of the composite PK -- ============================================================================= -- movies -- ============================================================================= CREATE TABLE public.movies ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), group_id uuid NOT NULL REFERENCES public.groups(id) ON DELETE CASCADE, tmdb_id integer NOT NULL, title text NOT NULL, year integer, poster_path text, genres text[], trailer_url text CONSTRAINT movies_trailer_url_domain CHECK ( trailer_url IS NULL OR trailer_url ~ '^https://(www\.)?(youtube\.com|themoviedb\.org|imdb\.com)/' ), trailer_url_refreshed_at timestamptz, metadata_refreshed_at timestamptz, added_by uuid REFERENCES public.users(id) ON DELETE SET NULL, watched boolean NOT NULL DEFAULT false, watched_at timestamptz, added_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_movies_group_id ON public.movies (group_id); CREATE INDEX idx_movies_added_by ON public.movies (added_by); -- ============================================================================= -- landing_reel_posters -- ============================================================================= CREATE TABLE public.landing_reel_posters ( id serial PRIMARY KEY, tmdb_id integer NOT NULL, poster_path text NOT NULL, title text NOT NULL, refreshed_at timestamptz NOT NULL DEFAULT now() );