-- Etsy Listing Optimizer - Supabase (Postgres) schema -- Run this in the Supabase SQL editor for your project (see DEPLOY.md). -- Assumes Supabase Auth is enabled; auth.users is Supabase's built-in table. -- --------------------------------------------------------------------------- -- profiles: one row per auth user, holds subscription/tier state -- --------------------------------------------------------------------------- create table if not exists public.profiles ( user_id uuid primary key references auth.users (id) on delete cascade, email text not null, tier text not null default 'free' check (tier in ('free', 'starter', 'pro', 'business')), stripe_customer_id text, stripe_subscription_id text, stripe_subscription_status text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); alter table public.profiles enable row level security; create policy "profiles_select_own" on public.profiles for select using (auth.uid() = user_id); create policy "profiles_update_own" on public.profiles for update using (auth.uid() = user_id); -- Inserts happen via a service-role backend call on signup, not directly by -- the client, so no client-facing insert policy is defined here. -- --------------------------------------------------------------------------- -- generation_counters: one row per user per calendar month -- --------------------------------------------------------------------------- create table if not exists public.generation_counters ( user_id uuid not null references auth.users (id) on delete cascade, year_month text not null, -- e.g. '2026-07' count integer not null default 0, updated_at timestamptz not null default now(), primary key (user_id, year_month) ); alter table public.generation_counters enable row level security; create policy "counters_select_own" on public.generation_counters for select using (auth.uid() = user_id); -- Writes to counters go through the backend (service role) so quota can't -- be tampered with by the client; no client insert/update policy. -- --------------------------------------------------------------------------- -- brand_voice_profiles -- --------------------------------------------------------------------------- create table if not exists public.brand_voice_profiles ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, name text not null, tone text, target_audience text, favorite_words text, created_at timestamptz not null default now() ); alter table public.brand_voice_profiles enable row level security; create policy "brand_voice_select_own" on public.brand_voice_profiles for select using (auth.uid() = user_id); create policy "brand_voice_insert_own" on public.brand_voice_profiles for insert with check (auth.uid() = user_id); create policy "brand_voice_update_own" on public.brand_voice_profiles for update using (auth.uid() = user_id); create policy "brand_voice_delete_own" on public.brand_voice_profiles for delete using (auth.uid() = user_id); -- --------------------------------------------------------------------------- -- listings: generation history -- --------------------------------------------------------------------------- create table if not exists public.listings ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, brand_voice_profile_id uuid references public.brand_voice_profiles (id) on delete set null, product_description text not null, target_keywords text, titles jsonb not null, tags jsonb not null, description text not null, category_hints jsonb, provider_used text, created_at timestamptz not null default now() ); alter table public.listings enable row level security; create policy "listings_select_own" on public.listings for select using (auth.uid() = user_id); create policy "listings_insert_own" on public.listings for insert with check (auth.uid() = user_id); create policy "listings_delete_own" on public.listings for delete using (auth.uid() = user_id); create index if not exists listings_user_created_idx on public.listings (user_id, created_at desc); -- --------------------------------------------------------------------------- -- shops: for Business tier "multiple shops/clients" -- --------------------------------------------------------------------------- create table if not exists public.shops ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, name text not null, created_at timestamptz not null default now() ); alter table public.shops enable row level security; create policy "shops_select_own" on public.shops for select using (auth.uid() = user_id); create policy "shops_insert_own" on public.shops for insert with check (auth.uid() = user_id); create policy "shops_delete_own" on public.shops for delete using (auth.uid() = user_id); -- --------------------------------------------------------------------------- -- helper: touch updated_at on profiles -- --------------------------------------------------------------------------- create or replace function public.set_updated_at() returns trigger as $$ begin new.updated_at = now(); return new; end; $$ language plpgsql; drop trigger if exists profiles_set_updated_at on public.profiles; create trigger profiles_set_updated_at before update on public.profiles for each row execute function public.set_updated_at(); -- --------------------------------------------------------------------------- -- Reminder (see DEPLOY.md): -- - Enable "Leaked password protection" under Auth > Policies in the Supabase -- dashboard - this is a project setting, not a SQL statement. -- - Provision this project in an EU region (e.g. Frankfurt) at creation time -- for GDPR data residency - also a dashboard setting, not SQL. -- ---------------------------------------------------------------------------