-- Menulio initial schema (PRD §22, §21) -- User -> Restaurant -> Location kept as separate tables from day one even -- though V1 UX is 1:1:1, so multi-location support (V2) needs no migration. create extension if not exists "pgcrypto"; -- --------------------------------------------------------------------------- -- Enums -- --------------------------------------------------------------------------- create type subscription_status as enum ('active', 'grace_period', 'suspended', 'deleted'); create type ai_import_status as enum ('pending', 'processing', 'completed', 'failed'); create type domain_status as enum ('pending', 'verified', 'failed'); -- --------------------------------------------------------------------------- -- Core identity -- --------------------------------------------------------------------------- create table users ( id uuid primary key references auth.users (id) on delete cascade, full_name text, created_at timestamptz not null default now() ); create table restaurants ( id uuid primary key default gen_random_uuid(), name text not null, slug text not null unique, logo_url text, cover_url text, phone text, address text, created_by uuid not null references users (id) on delete restrict, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table restaurant_members ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, user_id uuid not null references users (id) on delete cascade, role text not null default 'owner', created_at timestamptz not null default now(), unique (restaurant_id, user_id) ); create table locations ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, name text not null, address text, phone text, created_at timestamptz not null default now() ); -- --------------------------------------------------------------------------- -- Menu -- --------------------------------------------------------------------------- create table menus ( id uuid primary key default gen_random_uuid(), location_id uuid not null references locations (id) on delete cascade, name text not null default 'Menü', is_published boolean not null default false, published_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table menu_categories ( id uuid primary key default gen_random_uuid(), menu_id uuid not null references menus (id) on delete cascade, name text not null, description text, sort_order integer not null default 0, is_active boolean not null default true, created_at timestamptz not null default now() ); create table menu_items ( id uuid primary key default gen_random_uuid(), category_id uuid not null references menu_categories (id) on delete cascade, name text not null, description text, price numeric(10, 2) not null, image_url text, sort_order integer not null default 0, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table menu_item_options ( id uuid primary key default gen_random_uuid(), menu_item_id uuid not null references menu_items (id) on delete cascade, group_name text not null, label text not null, price_delta numeric(10, 2) not null default 0, sort_order integer not null default 0 ); -- --------------------------------------------------------------------------- -- Theming -- --------------------------------------------------------------------------- create table themes ( id uuid primary key default gen_random_uuid(), key text not null unique, name text not null, config jsonb not null default '{}'::jsonb ); create table restaurant_themes ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, theme_id uuid not null references themes (id) on delete restrict, overrides jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), unique (restaurant_id) ); -- --------------------------------------------------------------------------- -- Domains & QR -- --------------------------------------------------------------------------- create table domains ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, hostname text not null unique, is_custom boolean not null default false, status domain_status not null default 'pending', verified_at timestamptz, created_at timestamptz not null default now() ); create table qr_codes ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, target_url text not null, created_at timestamptz not null default now() ); -- --------------------------------------------------------------------------- -- AI import -- --------------------------------------------------------------------------- create table ai_imports ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, source_image text not null, status ai_import_status not null default 'pending', model text, raw_response jsonb, error text, processed_at timestamptz, created_at timestamptz not null default now() ); create table ai_import_items ( id uuid primary key default gen_random_uuid(), ai_import_id uuid not null references ai_imports (id) on delete cascade, category_name text, item_name text not null, description text, price numeric(10, 2), confidence numeric(4, 3), created_at timestamptz not null default now() ); -- --------------------------------------------------------------------------- -- Subscription -- --------------------------------------------------------------------------- create table subscriptions ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, revenuecat_entitlement text not null default 'pro', status subscription_status not null default 'active', product_id text, trial_ends_at timestamptz, current_period_ends_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (restaurant_id) ); -- --------------------------------------------------------------------------- -- Analytics & media -- --------------------------------------------------------------------------- create table analytics_events ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, event_type text not null, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); create table media ( id uuid primary key default gen_random_uuid(), restaurant_id uuid not null references restaurants (id) on delete cascade, storage_path text not null, kind text not null default 'image', created_at timestamptz not null default now() ); -- --------------------------------------------------------------------------- -- Indexes -- --------------------------------------------------------------------------- create index on restaurant_members (user_id); create index on locations (restaurant_id); create index on menus (location_id); create index on menu_categories (menu_id); create index on menu_items (category_id); create index on menu_item_options (menu_item_id); create index on domains (restaurant_id); create index on ai_imports (restaurant_id); create index on ai_import_items (ai_import_id); create index on analytics_events (restaurant_id, created_at); create index on media (restaurant_id); -- --------------------------------------------------------------------------- -- Row Level Security -- --------------------------------------------------------------------------- -- Business logic lives in the API layer (PRD §23), so most tables are locked -- to the service role. Owner-facing tables get membership-scoped policies for -- direct client reads; public menu tables get an anon read policy scoped to -- published menus only. alter table users enable row level security; alter table restaurants enable row level security; alter table restaurant_members enable row level security; alter table locations enable row level security; alter table menus enable row level security; alter table menu_categories enable row level security; alter table menu_items enable row level security; alter table menu_item_options enable row level security; alter table themes enable row level security; alter table restaurant_themes enable row level security; alter table domains enable row level security; alter table qr_codes enable row level security; alter table ai_imports enable row level security; alter table ai_import_items enable row level security; alter table subscriptions enable row level security; alter table analytics_events enable row level security; alter table media enable row level security; create policy "users read own row" on users for select using (id = auth.uid()); create policy "members read own restaurants" on restaurants for select using ( exists ( select 1 from restaurant_members rm where rm.restaurant_id = restaurants.id and rm.user_id = auth.uid() ) ); create policy "members read own membership rows" on restaurant_members for select using (user_id = auth.uid()); create policy "anyone can read published menus" on menus for select using (is_published = true); create policy "anyone can read categories of published menus" on menu_categories for select using ( exists ( select 1 from menus m where m.id = menu_categories.menu_id and m.is_published = true ) ); create policy "anyone can read items of published menus" on menu_items for select using ( exists ( select 1 from menu_categories mc join menus m on m.id = mc.menu_id where mc.id = menu_items.category_id and m.is_published = true ) ); create policy "themes are public" on themes for select using (true);