-- Neta one-shot Supabase setup SQL -- Run this once in a fresh Supabase project after Auth and Storage schemas are available. -- This file is generated from supabase/schema.sql and supabase/migrations/0002..0011. -- Demo seed data is intentionally not included. -- ----------------------------------------------------------------------------- -- Source: supabase/schema.sql -- ----------------------------------------------------------------------------- -- 0001: Initial Cognis baseline schema -- This file is intentionally idempotent enough to be retried after a failed SQL Editor run. -- Enable necessary extensions create extension if not exists "uuid-ossp"; -- 1. Create journals table create table if not exists public.journals ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, date timestamp with time zone default timezone('utc'::text, now()) not null, mood text not null, energy smallint not null, content text not null, ai_tags text[] default array[]::text[], ai_sentiment_score numeric, ai_summary text, ai_reflection text, analysis_status text default 'pending'::text, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- 2. Create tasks table create table if not exists public.tasks ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, date timestamp with time zone default timezone('utc'::text, now()) not null, title text not null, description text, status text default 'todo'::text, ai_generated boolean default false, source_journal_id uuid references public.journals(id) on delete set null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- 3. Create chat_sessions table create table if not exists public.chat_sessions ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, title text not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- 4. Create chat_messages table create table if not exists public.chat_messages ( id uuid default uuid_generate_v4() primary key, session_id uuid references public.chat_sessions(id) on delete cascade not null, role text not null, content text not null, context_journal_ids uuid[] default array[]::uuid[], created_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- 5. Create profiles table create table if not exists public.profiles ( id uuid references auth.users(id) on delete cascade primary key, first_name text, last_name text, avatar_url text, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- RLS (Row Level Security) alter table public.journals enable row level security; alter table public.tasks enable row level security; alter table public.chat_sessions enable row level security; alter table public.chat_messages enable row level security; alter table public.profiles enable row level security; -- Journals policies drop policy if exists "Users can view their own journals." on public.journals; create policy "Users can view their own journals." on public.journals for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own journals." on public.journals; create policy "Users can insert their own journals." on public.journals for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own journals." on public.journals; create policy "Users can update their own journals." on public.journals for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own journals." on public.journals; create policy "Users can delete their own journals." on public.journals for delete using (auth.uid() = user_id); -- Tasks policies drop policy if exists "Users can view their own tasks." on public.tasks; create policy "Users can view their own tasks." on public.tasks for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own tasks." on public.tasks; create policy "Users can insert their own tasks." on public.tasks for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own tasks." on public.tasks; create policy "Users can update their own tasks." on public.tasks for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own tasks." on public.tasks; create policy "Users can delete their own tasks." on public.tasks for delete using (auth.uid() = user_id); -- Chat sessions policies drop policy if exists "Users can view their own chat sessions." on public.chat_sessions; create policy "Users can view their own chat sessions." on public.chat_sessions for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own chat sessions." on public.chat_sessions; create policy "Users can insert their own chat sessions." on public.chat_sessions for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own chat sessions." on public.chat_sessions; create policy "Users can update their own chat sessions." on public.chat_sessions for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own chat sessions." on public.chat_sessions; create policy "Users can delete their own chat sessions." on public.chat_sessions for delete using (auth.uid() = user_id); -- Chat messages policies drop policy if exists "Users can view their own chat messages." on public.chat_messages; create policy "Users can view their own chat messages." on public.chat_messages for select using ( exists ( select 1 from public.chat_sessions where chat_sessions.id = chat_messages.session_id and chat_sessions.user_id = auth.uid() ) ); drop policy if exists "Users can insert their own chat messages." on public.chat_messages; create policy "Users can insert their own chat messages." on public.chat_messages for insert with check ( exists ( select 1 from public.chat_sessions where chat_sessions.id = session_id and chat_sessions.user_id = auth.uid() ) ); drop policy if exists "Users can update their own chat messages." on public.chat_messages; create policy "Users can update their own chat messages." on public.chat_messages for update using ( exists ( select 1 from public.chat_sessions where chat_sessions.id = session_id and chat_sessions.user_id = auth.uid() ) ); drop policy if exists "Users can delete their own chat messages." on public.chat_messages; create policy "Users can delete their own chat messages." on public.chat_messages for delete using ( exists ( select 1 from public.chat_sessions where chat_sessions.id = session_id and chat_sessions.user_id = auth.uid() ) ); -- Profiles policies drop policy if exists "Users can view their own profile." on public.profiles; create policy "Users can view their own profile." on public.profiles for select using (auth.uid() = id); drop policy if exists "Users can insert their own profile." on public.profiles; create policy "Users can insert their own profile." on public.profiles for insert with check (auth.uid() = id); drop policy if exists "Users can update their own profile." on public.profiles; create policy "Users can update their own profile." on public.profiles for update using (auth.uid() = id); -- First admin setup guard for self-hosted installations create or replace function public.is_first_admin_setup_available() returns boolean language sql security definer set search_path = public as $$ select not exists ( select 1 from public.profiles limit 1 ); $$; revoke all on function public.is_first_admin_setup_available() from public; grant execute on function public.is_first_admin_setup_available() to anon; grant execute on function public.is_first_admin_setup_available() to authenticated; create or replace function public.neta_current_jwt_role() returns text language sql stable as $$ select coalesce( nullif(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role', ''), nullif(current_setting('request.jwt.claim.role', true), ''), '' ); $$; revoke all on function public.neta_current_jwt_role() from public; grant execute on function public.neta_current_jwt_role() to anon; grant execute on function public.neta_current_jwt_role() to authenticated; grant execute on function public.neta_current_jwt_role() to service_role; create schema if not exists neta_internal; create table if not exists neta_internal.internal_auth_creations ( id uuid default uuid_generate_v4() primary key, email text not null, reason text default 'internal'::text not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, expires_at timestamp with time zone default (timezone('utc'::text, now()) + interval '2 minutes') not null ); create index if not exists internal_auth_creations_email_idx on neta_internal.internal_auth_creations (lower(email)); revoke all on schema neta_internal from public; revoke all on all tables in schema neta_internal from public; create or replace function public.request_internal_auth_creation( target_email text, target_reason text default 'internal' ) returns void language plpgsql security definer set search_path = public, neta_internal as $$ begin if public.neta_current_jwt_role() <> 'service_role' then raise exception 'Only service role can request internal auth creation.'; end if; if target_email is null or btrim(target_email) = '' then raise exception 'target_email is required.'; end if; delete from neta_internal.internal_auth_creations where expires_at <= timezone('utc'::text, now()) or lower(email) = lower(btrim(target_email)); insert into neta_internal.internal_auth_creations (email, reason) values (btrim(target_email), coalesce(nullif(btrim(target_reason), ''), 'internal')); end; $$; revoke all on function public.request_internal_auth_creation(text, text) from public; grant execute on function public.request_internal_auth_creation(text, text) to service_role; -- Function to handle new user signup create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = public, neta_internal as $$ declare allowed_internal_creation boolean := false; begin delete from neta_internal.internal_auth_creations where lower(email) = lower(new.email) and expires_at > timezone('utc'::text, now()) returning true into allowed_internal_creation; allowed_internal_creation := coalesce(allowed_internal_creation, false); if exists (select 1 from public.profiles limit 1) and coalesce(new.raw_app_meta_data->>'internal_created', 'false') <> 'true' and not allowed_internal_creation then raise exception 'Registration is closed. The first admin account already exists.'; end if; insert into public.profiles (id, first_name, last_name, avatar_url) values (new.id, '', '', '') on conflict (id) do nothing; return new; end; $$; -- Trigger to automatically create profile on signup drop trigger if exists on_auth_user_created on auth.users; create trigger on_auth_user_created after insert on auth.users for each row execute procedure public.handle_new_user(); -- Setup storage bucket for avatars insert into storage.buckets (id, name, public) values ('avatars', 'avatars', true) on conflict (id) do nothing; drop policy if exists "Avatar images are publicly accessible." on storage.objects; create policy "Avatar images are publicly accessible." on storage.objects for select using ( bucket_id = 'avatars' or public.neta_current_jwt_role() = 'service_role' ); drop policy if exists "Users can upload an avatar." on storage.objects; create policy "Users can upload an avatar." on storage.objects for insert with check ( bucket_id = 'avatars' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can update their own avatar." on storage.objects; create policy "Users can update their own avatar." on storage.objects for update using ( bucket_id = 'avatars' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can delete their own avatar." on storage.objects; create policy "Users can delete their own avatar." on storage.objects for delete using ( bucket_id = 'avatars' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0002_add_freelancer_os_core_tables.sql -- ----------------------------------------------------------------------------- -- 0002: Add Freelancer OS core tables -- Run after: supabase/schema.sql create extension if not exists "uuid-ossp"; create or replace function public.set_updated_at() returns trigger as $$ begin new.updated_at = timezone('utc'::text, now()); return new; end; $$ language plpgsql; create table if not exists public.clients ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, name text not null, company_name text, email text, phone text, website text, status text default 'active'::text not null, notes text, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); create table if not exists public.projects ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, client_id uuid references public.clients(id) on delete set null, name text not null, type text default 'client_project'::text not null, description text, status text default 'planning'::text not null, start_date date, due_date date, budget_amount numeric(12, 2), currency text default 'USD'::text not null, progress integer default 0 not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); create table if not exists public.calendar_events ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, client_id uuid references public.clients(id) on delete set null, project_id uuid references public.projects(id) on delete set null, task_id uuid references public.tasks(id) on delete set null, title text not null, description text, type text default 'focus'::text not null, starts_at timestamp with time zone not null, ends_at timestamp with time zone, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); create table if not exists public.finance_transactions ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, client_id uuid references public.clients(id) on delete set null, project_id uuid references public.projects(id) on delete set null, type text not null, amount numeric(12, 2) not null, currency text default 'USD'::text not null, transaction_date date default current_date not null, category text, payment_status text default 'planned'::text not null, description text, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); create table if not exists public.daily_logs ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, log_date date default current_date not null, mood_score smallint not null, energy_score smallint not null, work_satisfaction_score smallint, note text, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null, unique (user_id, log_date) ); create table if not exists public.app_settings ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null unique, timezone text default 'UTC'::text not null, currency text default 'USD'::text not null, ai_provider text default 'ollama'::text not null, ai_model text, api_key text, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); alter table public.tasks add column if not exists client_id uuid references public.clients(id) on delete set null, add column if not exists project_id uuid references public.projects(id) on delete set null, add column if not exists priority text default 'medium'::text not null, add column if not exists due_at timestamp with time zone, add column if not exists estimated_minutes integer, add column if not exists actual_minutes integer; do $$ begin if not exists (select 1 from pg_constraint where conname = 'clients_status_check') then alter table public.clients add constraint clients_status_check check (status in ('active', 'paused', 'archived')); end if; if not exists (select 1 from pg_constraint where conname = 'projects_type_check') then alter table public.projects add constraint projects_type_check check (type in ('client_project', 'side_project')); end if; if not exists (select 1 from pg_constraint where conname = 'projects_status_check') then alter table public.projects add constraint projects_status_check check (status in ('planning', 'active', 'paused', 'completed', 'cancelled')); end if; if not exists (select 1 from pg_constraint where conname = 'projects_progress_check') then alter table public.projects add constraint projects_progress_check check (progress >= 0 and progress <= 100); end if; if not exists (select 1 from pg_constraint where conname = 'tasks_priority_check') then alter table public.tasks add constraint tasks_priority_check check (priority in ('low', 'medium', 'high', 'urgent')); end if; if not exists (select 1 from pg_constraint where conname = 'calendar_events_type_check') then alter table public.calendar_events add constraint calendar_events_type_check check (type in ('meeting', 'focus', 'deadline', 'personal', 'finance')); end if; if not exists (select 1 from pg_constraint where conname = 'calendar_events_time_check') then alter table public.calendar_events add constraint calendar_events_time_check check (ends_at is null or ends_at >= starts_at); end if; if not exists (select 1 from pg_constraint where conname = 'finance_transactions_type_check') then alter table public.finance_transactions add constraint finance_transactions_type_check check (type in ('income', 'expense')); end if; if not exists (select 1 from pg_constraint where conname = 'finance_transactions_amount_check') then alter table public.finance_transactions add constraint finance_transactions_amount_check check (amount >= 0); end if; if not exists (select 1 from pg_constraint where conname = 'finance_transactions_payment_status_check') then alter table public.finance_transactions add constraint finance_transactions_payment_status_check check (payment_status in ('planned', 'pending', 'paid', 'cancelled')); end if; if not exists (select 1 from pg_constraint where conname = 'daily_logs_mood_score_check') then alter table public.daily_logs add constraint daily_logs_mood_score_check check (mood_score between 1 and 5); end if; if not exists (select 1 from pg_constraint where conname = 'daily_logs_energy_score_check') then alter table public.daily_logs add constraint daily_logs_energy_score_check check (energy_score between 1 and 5); end if; if not exists (select 1 from pg_constraint where conname = 'daily_logs_work_satisfaction_score_check') then alter table public.daily_logs add constraint daily_logs_work_satisfaction_score_check check (work_satisfaction_score is null or work_satisfaction_score between 1 and 5); end if; if not exists (select 1 from pg_constraint where conname = 'app_settings_ai_provider_check') then alter table public.app_settings add constraint app_settings_ai_provider_check check (ai_provider in ('ollama', 'openai', 'gemini', 'groq')); end if; end $$; create index if not exists clients_user_id_idx on public.clients(user_id); create index if not exists clients_status_idx on public.clients(status); create index if not exists projects_user_id_idx on public.projects(user_id); create index if not exists projects_client_id_idx on public.projects(client_id); create index if not exists projects_status_idx on public.projects(status); create index if not exists projects_due_date_idx on public.projects(due_date); create index if not exists tasks_client_id_idx on public.tasks(client_id); create index if not exists tasks_project_id_idx on public.tasks(project_id); create index if not exists tasks_due_at_idx on public.tasks(due_at); create index if not exists tasks_priority_idx on public.tasks(priority); create index if not exists calendar_events_user_id_idx on public.calendar_events(user_id); create index if not exists calendar_events_starts_at_idx on public.calendar_events(starts_at); create index if not exists calendar_events_project_id_idx on public.calendar_events(project_id); create index if not exists calendar_events_task_id_idx on public.calendar_events(task_id); create index if not exists finance_transactions_user_id_idx on public.finance_transactions(user_id); create index if not exists finance_transactions_client_id_idx on public.finance_transactions(client_id); create index if not exists finance_transactions_project_id_idx on public.finance_transactions(project_id); create index if not exists finance_transactions_transaction_date_idx on public.finance_transactions(transaction_date); create index if not exists finance_transactions_payment_status_idx on public.finance_transactions(payment_status); create index if not exists daily_logs_user_id_idx on public.daily_logs(user_id); create index if not exists daily_logs_log_date_idx on public.daily_logs(log_date); alter table public.clients enable row level security; alter table public.projects enable row level security; alter table public.calendar_events enable row level security; alter table public.finance_transactions enable row level security; alter table public.daily_logs enable row level security; alter table public.app_settings enable row level security; drop policy if exists "Users can view their own clients." on public.clients; create policy "Users can view their own clients." on public.clients for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own clients." on public.clients; create policy "Users can insert their own clients." on public.clients for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own clients." on public.clients; create policy "Users can update their own clients." on public.clients for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own clients." on public.clients; create policy "Users can delete their own clients." on public.clients for delete using (auth.uid() = user_id); drop policy if exists "Users can view their own projects." on public.projects; create policy "Users can view their own projects." on public.projects for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own projects." on public.projects; create policy "Users can insert their own projects." on public.projects for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own projects." on public.projects; create policy "Users can update their own projects." on public.projects for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own projects." on public.projects; create policy "Users can delete their own projects." on public.projects for delete using (auth.uid() = user_id); drop policy if exists "Users can view their own calendar events." on public.calendar_events; create policy "Users can view their own calendar events." on public.calendar_events for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own calendar events." on public.calendar_events; create policy "Users can insert their own calendar events." on public.calendar_events for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own calendar events." on public.calendar_events; create policy "Users can update their own calendar events." on public.calendar_events for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own calendar events." on public.calendar_events; create policy "Users can delete their own calendar events." on public.calendar_events for delete using (auth.uid() = user_id); drop policy if exists "Users can view their own finance transactions." on public.finance_transactions; create policy "Users can view their own finance transactions." on public.finance_transactions for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own finance transactions." on public.finance_transactions; create policy "Users can insert their own finance transactions." on public.finance_transactions for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own finance transactions." on public.finance_transactions; create policy "Users can update their own finance transactions." on public.finance_transactions for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own finance transactions." on public.finance_transactions; create policy "Users can delete their own finance transactions." on public.finance_transactions for delete using (auth.uid() = user_id); drop policy if exists "Users can view their own daily logs." on public.daily_logs; create policy "Users can view their own daily logs." on public.daily_logs for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own daily logs." on public.daily_logs; create policy "Users can insert their own daily logs." on public.daily_logs for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own daily logs." on public.daily_logs; create policy "Users can update their own daily logs." on public.daily_logs for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own daily logs." on public.daily_logs; create policy "Users can delete their own daily logs." on public.daily_logs for delete using (auth.uid() = user_id); drop policy if exists "Users can view their own app settings." on public.app_settings; create policy "Users can view their own app settings." on public.app_settings for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own app settings." on public.app_settings; create policy "Users can insert their own app settings." on public.app_settings for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own app settings." on public.app_settings; create policy "Users can update their own app settings." on public.app_settings for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own app settings." on public.app_settings; create policy "Users can delete their own app settings." on public.app_settings for delete using (auth.uid() = user_id); drop trigger if exists set_clients_updated_at on public.clients; create trigger set_clients_updated_at before update on public.clients for each row execute procedure public.set_updated_at(); drop trigger if exists set_projects_updated_at on public.projects; create trigger set_projects_updated_at before update on public.projects for each row execute procedure public.set_updated_at(); drop trigger if exists set_calendar_events_updated_at on public.calendar_events; create trigger set_calendar_events_updated_at before update on public.calendar_events for each row execute procedure public.set_updated_at(); drop trigger if exists set_finance_transactions_updated_at on public.finance_transactions; create trigger set_finance_transactions_updated_at before update on public.finance_transactions for each row execute procedure public.set_updated_at(); drop trigger if exists set_daily_logs_updated_at on public.daily_logs; create trigger set_daily_logs_updated_at before update on public.daily_logs for each row execute procedure public.set_updated_at(); drop trigger if exists set_app_settings_updated_at on public.app_settings; create trigger set_app_settings_updated_at before update on public.app_settings for each row execute procedure public.set_updated_at(); -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0003_add_project_planning_assets.sql -- ----------------------------------------------------------------------------- -- 0003: Add project planning sections and project visual storage -- Run after: supabase/migrations/0002_add_freelancer_os_core_tables.sql alter table public.projects add column if not exists cover_image_path text, add column if not exists cover_image_alt text; create table if not exists public.project_planning_sections ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, project_id uuid references public.projects(id) on delete cascade not null, category text not null, title text not null, content text, metadata jsonb default '{}'::jsonb not null, sort_order integer default 0 not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); do $$ begin if not exists (select 1 from pg_constraint where conname = 'project_planning_sections_category_check') then alter table public.project_planning_sections add constraint project_planning_sections_category_check check ( category in ( 'overview', 'problem', 'goal', 'audience', 'scope', 'design_system', 'color_palette', 'typography', 'assets', 'notes' ) ); end if; end $$; create index if not exists project_planning_sections_user_id_idx on public.project_planning_sections(user_id); create index if not exists project_planning_sections_project_id_idx on public.project_planning_sections(project_id); create index if not exists project_planning_sections_category_idx on public.project_planning_sections(category); alter table public.project_planning_sections enable row level security; drop policy if exists "Users can view their own project planning sections." on public.project_planning_sections; create policy "Users can view their own project planning sections." on public.project_planning_sections for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own project planning sections." on public.project_planning_sections; create policy "Users can insert their own project planning sections." on public.project_planning_sections for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own project planning sections." on public.project_planning_sections; create policy "Users can update their own project planning sections." on public.project_planning_sections for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own project planning sections." on public.project_planning_sections; create policy "Users can delete their own project planning sections." on public.project_planning_sections for delete using (auth.uid() = user_id); drop trigger if exists set_project_planning_sections_updated_at on public.project_planning_sections; create trigger set_project_planning_sections_updated_at before update on public.project_planning_sections for each row execute procedure public.set_updated_at(); insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types) values ( 'project-assets', 'project-assets', false, 5242880, array['image/jpeg', 'image/png', 'image/webp', 'image/gif'] ) on conflict (id) do update set public = excluded.public, file_size_limit = excluded.file_size_limit, allowed_mime_types = excluded.allowed_mime_types; drop policy if exists "Users can view their own project assets." on storage.objects; create policy "Users can view their own project assets." on storage.objects for select using ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can upload their own project assets." on storage.objects; create policy "Users can upload their own project assets." on storage.objects for insert with check ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can update their own project assets." on storage.objects; create policy "Users can update their own project assets." on storage.objects for update using ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can delete their own project assets." on storage.objects; create policy "Users can delete their own project assets." on storage.objects for delete using ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0004_add_business_os_tables.sql -- ----------------------------------------------------------------------------- -- 0004: Faz 6 - Freelancer Business OS Tables -- 1. Proposals create table if not exists public.proposals ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, client_id uuid references public.clients(id) on delete set null, project_id uuid references public.projects(id) on delete set null, title text not null, description text, amount numeric(12,2) not null default 0, currency text default 'TRY'::text, status text default 'draft'::text check (status in ('draft', 'sent', 'accepted', 'rejected')), valid_until timestamp with time zone, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- 2. Contracts create table if not exists public.contracts ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, proposal_id uuid references public.proposals(id) on delete set null, client_id uuid references public.clients(id) on delete set null, title text not null, content text, status text default 'draft'::text check (status in ('draft', 'active', 'completed', 'cancelled')), signed_at timestamp with time zone, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- 3. Invoices create table if not exists public.invoices ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, client_id uuid references public.clients(id) on delete set null, project_id uuid references public.projects(id) on delete set null, invoice_number text not null, amount numeric(12,2) not null default 0, tax_rate numeric(5,2) default 0, -- percentage currency text default 'TRY'::text, status text default 'draft'::text check (status in ('draft', 'sent', 'paid', 'overdue', 'cancelled')), issue_date timestamp with time zone default timezone('utc'::text, now()), due_date timestamp with time zone, paid_at timestamp with time zone, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- 4. Subscriptions create table if not exists public.subscriptions ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, name text not null, amount numeric(12,2) not null default 0, currency text default 'TRY'::text, billing_cycle text default 'monthly'::text check (billing_cycle in ('monthly', 'yearly', 'weekly')), next_billing_date timestamp with time zone, status text default 'active'::text check (status in ('active', 'cancelled')), category text, -- e.g., 'software', 'hosting', 'marketing' created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- Enable RLS alter table public.proposals enable row level security; alter table public.contracts enable row level security; alter table public.invoices enable row level security; alter table public.subscriptions enable row level security; -- Proposals RLS drop policy if exists "Users can view their own proposals" on public.proposals; create policy "Users can view their own proposals" on public.proposals for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own proposals" on public.proposals; create policy "Users can insert their own proposals" on public.proposals for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own proposals" on public.proposals; create policy "Users can update their own proposals" on public.proposals for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own proposals" on public.proposals; create policy "Users can delete their own proposals" on public.proposals for delete using (auth.uid() = user_id); -- Contracts RLS drop policy if exists "Users can view their own contracts" on public.contracts; create policy "Users can view their own contracts" on public.contracts for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own contracts" on public.contracts; create policy "Users can insert their own contracts" on public.contracts for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own contracts" on public.contracts; create policy "Users can update their own contracts" on public.contracts for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own contracts" on public.contracts; create policy "Users can delete their own contracts" on public.contracts for delete using (auth.uid() = user_id); -- Invoices RLS drop policy if exists "Users can view their own invoices" on public.invoices; create policy "Users can view their own invoices" on public.invoices for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own invoices" on public.invoices; create policy "Users can insert their own invoices" on public.invoices for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own invoices" on public.invoices; create policy "Users can update their own invoices" on public.invoices for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own invoices" on public.invoices; create policy "Users can delete their own invoices" on public.invoices for delete using (auth.uid() = user_id); -- Subscriptions RLS drop policy if exists "Users can view their own subscriptions" on public.subscriptions; create policy "Users can view their own subscriptions" on public.subscriptions for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own subscriptions" on public.subscriptions; create policy "Users can insert their own subscriptions" on public.subscriptions for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own subscriptions" on public.subscriptions; create policy "Users can update their own subscriptions" on public.subscriptions for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own subscriptions" on public.subscriptions; create policy "Users can delete their own subscriptions" on public.subscriptions for delete using (auth.uid() = user_id); -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0005_add_advanced_crm_tables.sql -- ----------------------------------------------------------------------------- -- 0005: Faz 7 - Advanced CRM Tables -- 1. Alter clients table to add CRM specific columns alter table public.clients add column if not exists pipeline_stage text default 'lead'::text check (pipeline_stage in ('lead', 'contacted', 'proposal_sent', 'won', 'lost')), add column if not exists next_follow_up_date timestamp with time zone, add column if not exists last_contact_date timestamp with time zone, add column if not exists client_value_score numeric(5,2) default 0; -- 2. Create client_activities table create table if not exists public.client_activities ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, client_id uuid references public.clients(id) on delete cascade not null, type text not null check (type in ('note', 'call', 'meeting', 'email')), title text not null, content text, activity_date timestamp with time zone default timezone('utc'::text, now()) not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- Enable RLS alter table public.client_activities enable row level security; -- Client Activities RLS drop policy if exists "Users can view their own client activities" on public.client_activities; create policy "Users can view their own client activities" on public.client_activities for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own client activities" on public.client_activities; create policy "Users can insert their own client activities" on public.client_activities for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own client activities" on public.client_activities; create policy "Users can update their own client activities" on public.client_activities for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own client activities" on public.client_activities; create policy "Users can delete their own client activities" on public.client_activities for delete using (auth.uid() = user_id); -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0006_add_pgvector_and_embeddings.sql -- ----------------------------------------------------------------------------- -- 0006: Faz 8 - pgvector & RAG Embeddings -- Enable the pgvector extension to work with embedding vectors create extension if not exists vector; -- Create a table to store document embeddings for RAG create table if not exists public.document_embeddings ( id uuid default uuid_generate_v4() primary key, user_id uuid references auth.users(id) on delete cascade not null, content text not null, metadata jsonb, -- e.g. { "source_type": "note", "source_id": "123" } embedding vector(1536), -- 1536 works for OpenAI text-embedding-3-small and text-embedding-ada-002 created_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- Enable RLS alter table public.document_embeddings enable row level security; drop policy if exists "Users can view their own embeddings" on public.document_embeddings; create policy "Users can view their own embeddings" on public.document_embeddings for select using (auth.uid() = user_id); drop policy if exists "Users can insert their own embeddings" on public.document_embeddings; create policy "Users can insert their own embeddings" on public.document_embeddings for insert with check (auth.uid() = user_id); drop policy if exists "Users can update their own embeddings" on public.document_embeddings; create policy "Users can update their own embeddings" on public.document_embeddings for update using (auth.uid() = user_id); drop policy if exists "Users can delete their own embeddings" on public.document_embeddings; create policy "Users can delete their own embeddings" on public.document_embeddings for delete using (auth.uid() = user_id); -- Create a function to similarity search for embeddings create or replace function match_documents ( query_embedding vector(1536), match_count int default null, filter_user_id uuid default null ) returns table ( id uuid, content text, metadata jsonb, similarity float ) language plpgsql as $$ #variable_conflict use_column begin return query select document_embeddings.id, document_embeddings.content, document_embeddings.metadata, 1 - (document_embeddings.embedding <=> query_embedding) as similarity from document_embeddings where document_embeddings.user_id = filter_user_id order by document_embeddings.embedding <=> query_embedding limit match_count; end; $$; -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0007_add_client_portal_tables.sql -- ----------------------------------------------------------------------------- -- 0007: Add Client Portal Tables and Roles -- Run after: 0006_add_pgvector_and_embeddings.sql -- 1. Update Profiles with Role ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS role text DEFAULT 'freelancer'::text CHECK (role IN ('freelancer', 'client')); -- 2. Update Clients to link to Client Auth ID ALTER TABLE public.clients ADD COLUMN IF NOT EXISTS client_auth_id uuid REFERENCES auth.users(id) ON DELETE SET NULL; -- 3. Update Tasks to add Client Visibility ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS is_public_to_client boolean DEFAULT false; -- 4. Create Project Revisions table CREATE TABLE IF NOT EXISTS public.project_revisions ( id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, project_id uuid REFERENCES public.projects(id) ON DELETE CASCADE NOT NULL, client_id uuid REFERENCES public.clients(id) ON DELETE CASCADE NOT NULL, requested_by uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, description text NOT NULL, status text DEFAULT 'pending'::text CHECK (status IN ('pending', 'in_progress', 'completed', 'rejected')), created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL, updated_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL ); -- Trigger to set updated_at DROP TRIGGER IF EXISTS set_project_revisions_updated_at ON public.project_revisions; CREATE TRIGGER set_project_revisions_updated_at BEFORE UPDATE ON public.project_revisions FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); -- 5. Enable RLS ALTER TABLE public.project_revisions ENABLE ROW LEVEL SECURITY; -- 6. Add RLS Policies for Client Access -- PROFILES -- Clients can read their own profile DROP POLICY IF EXISTS "Clients can view own profile" ON public.profiles; CREATE POLICY "Clients can view own profile" ON public.profiles FOR SELECT USING (auth.uid() = id); -- CLIENTS -- Clients can read their own client record DROP POLICY IF EXISTS "Clients can view own client record" ON public.clients; CREATE POLICY "Clients can view own client record" ON public.clients FOR SELECT USING (client_auth_id = auth.uid()); -- PROJECTS -- Clients can view projects where client_id matches their client record DROP POLICY IF EXISTS "Clients can view own projects" ON public.projects; CREATE POLICY "Clients can view own projects" ON public.projects FOR SELECT USING ( client_id IN ( SELECT id FROM public.clients WHERE client_auth_id = auth.uid() ) ); -- TASKS -- Clients can view tasks linked to their projects IF is_public_to_client is true DROP POLICY IF EXISTS "Clients can view public tasks of their projects" ON public.tasks; CREATE POLICY "Clients can view public tasks of their projects" ON public.tasks FOR SELECT USING ( is_public_to_client = true AND project_id IN ( SELECT id FROM public.projects WHERE client_id IN ( SELECT id FROM public.clients WHERE client_auth_id = auth.uid() ) ) ); -- PROJECT PLANNING SECTIONS (Milestones, Roadmap, etc.) -- Clients can view planning sections of their projects DROP POLICY IF EXISTS "Clients can view planning sections of their projects" ON public.project_planning_sections; CREATE POLICY "Clients can view planning sections of their projects" ON public.project_planning_sections FOR SELECT USING ( project_id IN ( SELECT id FROM public.projects WHERE client_id IN ( SELECT id FROM public.clients WHERE client_auth_id = auth.uid() ) ) ); -- PROJECT REVISIONS -- Freelancers can manage all revisions for their projects DROP POLICY IF EXISTS "Freelancers can manage revisions" ON public.project_revisions; CREATE POLICY "Freelancers can manage revisions" ON public.project_revisions FOR ALL USING ( project_id IN ( SELECT id FROM public.projects WHERE user_id = auth.uid() ) ); -- Clients can insert revisions for their projects DROP POLICY IF EXISTS "Clients can insert revisions" ON public.project_revisions; CREATE POLICY "Clients can insert revisions" ON public.project_revisions FOR INSERT WITH CHECK ( client_id IN ( SELECT id FROM public.clients WHERE client_auth_id = auth.uid() ) AND requested_by = auth.uid() ); -- Clients can view their own revisions DROP POLICY IF EXISTS "Clients can view own revisions" ON public.project_revisions; CREATE POLICY "Clients can view own revisions" ON public.project_revisions FOR SELECT USING ( client_id IN ( SELECT id FROM public.clients WHERE client_auth_id = auth.uid() ) ); -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0008_add_project_progress_and_quota.sql -- ----------------------------------------------------------------------------- -- 0008: Add Project Progress Type and Revision Quota -- Add columns to projects table ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS progress_type text DEFAULT 'manual'::text CHECK (progress_type IN ('manual', 'auto')), ADD COLUMN IF NOT EXISTS revision_quota integer DEFAULT 0; -- Function to update project progress automatically if progress_type is 'auto' CREATE OR REPLACE FUNCTION public.update_project_progress_on_task_change() RETURNS TRIGGER AS $$ DECLARE v_project_id uuid; v_progress_type text; v_total_tasks integer; v_done_tasks integer; v_new_progress integer; BEGIN IF TG_OP = 'DELETE' THEN v_project_id := OLD.project_id; ELSE v_project_id := NEW.project_id; END IF; IF v_project_id IS NULL THEN RETURN NULL; END IF; SELECT progress_type INTO v_progress_type FROM public.projects WHERE id = v_project_id; IF v_progress_type = 'auto' THEN SELECT count(*) INTO v_total_tasks FROM public.tasks WHERE project_id = v_project_id; SELECT count(*) INTO v_done_tasks FROM public.tasks WHERE project_id = v_project_id AND status = 'done'; IF v_total_tasks > 0 THEN v_new_progress := round((v_done_tasks::numeric / v_total_tasks::numeric) * 100); ELSE v_new_progress := 0; END IF; UPDATE public.projects SET progress = v_new_progress WHERE id = v_project_id; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Trigger to recalculate progress when tasks are modified DROP TRIGGER IF EXISTS trigger_update_project_progress ON public.tasks; CREATE TRIGGER trigger_update_project_progress AFTER INSERT OR UPDATE OF status OR DELETE ON public.tasks FOR EACH ROW EXECUTE FUNCTION public.update_project_progress_on_task_change(); -- Trigger to recalculate progress when a project's progress_type is changed to 'auto' CREATE OR REPLACE FUNCTION public.update_project_progress_on_type_change() RETURNS TRIGGER AS $$ DECLARE v_total_tasks integer; v_done_tasks integer; v_new_progress integer; BEGIN IF NEW.progress_type = 'auto' AND (OLD.progress_type IS DISTINCT FROM NEW.progress_type) THEN SELECT count(*) INTO v_total_tasks FROM public.tasks WHERE project_id = NEW.id; SELECT count(*) INTO v_done_tasks FROM public.tasks WHERE project_id = NEW.id AND status = 'done'; IF v_total_tasks > 0 THEN v_new_progress := round((v_done_tasks::numeric / v_total_tasks::numeric) * 100); ELSE v_new_progress := 0; END IF; NEW.progress := v_new_progress; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; DROP TRIGGER IF EXISTS trigger_update_project_progress_type ON public.projects; CREATE TRIGGER trigger_update_project_progress_type BEFORE UPDATE OF progress_type ON public.projects FOR EACH ROW EXECUTE FUNCTION public.update_project_progress_on_type_change(); -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0009_lock_registration_after_first_admin.sql -- ----------------------------------------------------------------------------- -- 0009: Lock public registration after the first self-host admin account -- Run after: supabase/migrations/0008_add_project_progress_and_quota.sql create or replace function public.is_first_admin_setup_available() returns boolean language sql security definer set search_path = public as $$ select not exists ( select 1 from public.profiles limit 1 ); $$; revoke all on function public.is_first_admin_setup_available() from public; grant execute on function public.is_first_admin_setup_available() to anon; grant execute on function public.is_first_admin_setup_available() to authenticated; create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = public as $$ begin if exists (select 1 from public.profiles limit 1) and coalesce(new.raw_app_meta_data->>'internal_created', 'false') <> 'true' then raise exception 'Registration is closed. The first admin account already exists.'; end if; insert into public.profiles (id, first_name, last_name, avatar_url) values (new.id, '', '', '') on conflict (id) do nothing; return new; end; $$; drop trigger if exists on_auth_user_created on auth.users; create trigger on_auth_user_created after insert on auth.users for each row execute procedure public.handle_new_user(); notify pgrst, 'reload schema'; -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0010_allow_internal_auth_user_creation.sql -- ----------------------------------------------------------------------------- -- 0010: Allow trusted server-side internal Auth user creation -- Run after: supabase/migrations/0009_lock_registration_after_first_admin.sql create schema if not exists neta_internal; create table if not exists neta_internal.internal_auth_creations ( id uuid default uuid_generate_v4() primary key, email text not null, reason text default 'internal'::text not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, expires_at timestamp with time zone default (timezone('utc'::text, now()) + interval '2 minutes') not null ); create index if not exists internal_auth_creations_email_idx on neta_internal.internal_auth_creations (lower(email)); revoke all on schema neta_internal from public; revoke all on all tables in schema neta_internal from public; create or replace function public.neta_current_jwt_role() returns text language sql stable as $$ select coalesce( nullif(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role', ''), nullif(current_setting('request.jwt.claim.role', true), ''), '' ); $$; revoke all on function public.neta_current_jwt_role() from public; grant execute on function public.neta_current_jwt_role() to anon; grant execute on function public.neta_current_jwt_role() to authenticated; grant execute on function public.neta_current_jwt_role() to service_role; create or replace function public.request_internal_auth_creation( target_email text, target_reason text default 'internal' ) returns void language plpgsql security definer set search_path = public, neta_internal as $$ begin if public.neta_current_jwt_role() <> 'service_role' then raise exception 'Only service role can request internal auth creation.'; end if; if target_email is null or btrim(target_email) = '' then raise exception 'target_email is required.'; end if; delete from neta_internal.internal_auth_creations where expires_at <= timezone('utc'::text, now()) or lower(email) = lower(btrim(target_email)); insert into neta_internal.internal_auth_creations (email, reason) values (btrim(target_email), coalesce(nullif(btrim(target_reason), ''), 'internal')); end; $$; revoke all on function public.request_internal_auth_creation(text, text) from public; grant execute on function public.request_internal_auth_creation(text, text) to service_role; create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = public, neta_internal as $$ declare allowed_internal_creation boolean := false; begin delete from neta_internal.internal_auth_creations where lower(email) = lower(new.email) and expires_at > timezone('utc'::text, now()) returning true into allowed_internal_creation; allowed_internal_creation := coalesce(allowed_internal_creation, false); if exists (select 1 from public.profiles limit 1) and coalesce(new.raw_app_meta_data->>'internal_created', 'false') <> 'true' and not allowed_internal_creation then raise exception 'Registration is closed. The first admin account already exists.'; end if; insert into public.profiles (id, first_name, last_name, avatar_url) values (new.id, '', '', '') on conflict (id) do nothing; return new; end; $$; drop trigger if exists on_auth_user_created on auth.users; create trigger on_auth_user_created after insert on auth.users for each row execute procedure public.handle_new_user(); notify pgrst, 'reload schema'; -- ----------------------------------------------------------------------------- -- Source: supabase/migrations/0011_fix_service_role_claims_and_storage_policies.sql -- ----------------------------------------------------------------------------- -- 0011: Fix service-role JWT claim handling and storage policies -- Run after: supabase/migrations/0010_allow_internal_auth_user_creation.sql create schema if not exists neta_internal; create table if not exists neta_internal.internal_auth_creations ( id uuid default uuid_generate_v4() primary key, email text not null, reason text default 'internal'::text not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null, expires_at timestamp with time zone default (timezone('utc'::text, now()) + interval '2 minutes') not null ); create index if not exists internal_auth_creations_email_idx on neta_internal.internal_auth_creations (lower(email)); revoke all on schema neta_internal from public; revoke all on all tables in schema neta_internal from public; create or replace function public.neta_current_jwt_role() returns text language sql stable as $$ select coalesce( nullif(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role', ''), nullif(current_setting('request.jwt.claim.role', true), ''), '' ); $$; revoke all on function public.neta_current_jwt_role() from public; grant execute on function public.neta_current_jwt_role() to anon; grant execute on function public.neta_current_jwt_role() to authenticated; grant execute on function public.neta_current_jwt_role() to service_role; create or replace function public.request_internal_auth_creation( target_email text, target_reason text default 'internal' ) returns void language plpgsql security definer set search_path = public, neta_internal as $$ begin if public.neta_current_jwt_role() <> 'service_role' then raise exception 'Only service role can request internal auth creation.'; end if; if target_email is null or btrim(target_email) = '' then raise exception 'target_email is required.'; end if; delete from neta_internal.internal_auth_creations where expires_at <= timezone('utc'::text, now()) or lower(email) = lower(btrim(target_email)); insert into neta_internal.internal_auth_creations (email, reason) values (btrim(target_email), coalesce(nullif(btrim(target_reason), ''), 'internal')); end; $$; revoke all on function public.request_internal_auth_creation(text, text) from public; grant execute on function public.request_internal_auth_creation(text, text) to service_role; drop policy if exists "Avatar images are publicly accessible." on storage.objects; create policy "Avatar images are publicly accessible." on storage.objects for select using ( bucket_id = 'avatars' or public.neta_current_jwt_role() = 'service_role' ); drop policy if exists "Users can upload an avatar." on storage.objects; create policy "Users can upload an avatar." on storage.objects for insert with check ( bucket_id = 'avatars' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can update their own avatar." on storage.objects; create policy "Users can update their own avatar." on storage.objects for update using ( bucket_id = 'avatars' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can delete their own avatar." on storage.objects; create policy "Users can delete their own avatar." on storage.objects for delete using ( bucket_id = 'avatars' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can view their own project assets." on storage.objects; create policy "Users can view their own project assets." on storage.objects for select using ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can upload their own project assets." on storage.objects; create policy "Users can upload their own project assets." on storage.objects for insert with check ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can update their own project assets." on storage.objects; create policy "Users can update their own project assets." on storage.objects for update using ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); drop policy if exists "Users can delete their own project assets." on storage.objects; create policy "Users can delete their own project assets." on storage.objects for delete using ( bucket_id = 'project-assets' and ( public.neta_current_jwt_role() = 'service_role' or auth.uid()::text = (storage.foldername(name))[1] ) ); notify pgrst, 'reload schema';