diff --git a/README.md b/README.md index 5b89031..0593d0e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,14 @@ This repository now ships only the web application. It does not bundle Supabase, See `docs/04-supabase-kurulumu.md` for the expected Supabase-side resources. +For a fresh Supabase project, run the one-shot setup SQL: + +```bash +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql +``` + +You can also paste the full contents of `supabase/setup.sql` into Supabase SQL Editor and run it once. + ## Environment Variables Copy `.env.example` to `.env.local` for local development, or add the same values in Vercel, Coolify, Dokploy, or your hosting provider. diff --git a/docs/04-supabase-kurulumu.md b/docs/04-supabase-kurulumu.md index d94fd9d..a30aac1 100644 --- a/docs/04-supabase-kurulumu.md +++ b/docs/04-supabase-kurulumu.md @@ -22,6 +22,49 @@ Kurulumda genel yaklaşım şu: Repo içinde `supabase/` ve `docs/database/` klasörleri tutulmaya devam eder. Bunları deployment sırasında otomatik çalıştıran bir script yoktur. Bu dosyalar benim SQL kayıtlarım, migration notlarım ve schema geçmişim olarak durur. +## Tek Seferlik Kurulum SQL'i + +Yeni bir Supabase projesini Neta için hazırlamanın en kolay yolu `supabase/setup.sql` dosyasını çalıştırmak. Bu dosya `supabase/schema.sql` ve `supabase/migrations/0002..0011` arasındaki migration dosyalarının tek dosyada birleştirilmiş hâlidir. + +Demo seed verisi bu dosyaya dahil değildir. Production kurulumda örnek veri istemediğim için seed ayrı tutulur. + +### Supabase SQL Editor ile + +Supabase panelinde şu bölümü açın: + +```txt +SQL Editor -> New query +``` + +Sonra `supabase/setup.sql` dosyasının tamamını kopyalayıp tek seferde çalıştırın. + +Bu işlem şunları kurar: + +- Tablolar. +- Foreign key ve check constraint'leri. +- Index'ler. +- Trigger ve function'lar. +- RLS policy'leri. +- Storage bucket kayıtları ve storage policy'leri. + +### Terminalden Tek Komut ile + +Supabase connection string'iniz varsa aynı kurulumu tek `psql` komutuyla da çalıştırabilirsiniz: + +```bash +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql +``` + +Supabase hosted projelerde connection string'i şuradan alabilirsiniz: + +```txt +Project Settings -> Database -> Connection string +``` + +Şifre, host ve port bilgilerini kendi projenize göre değiştirmelisiniz. + +Bu komut başarılı bittikten sonra Supabase tarafı Neta uygulamasına hazır hâle gelir. + ## Gerekli Tablolar Neta şu public tabloları bekler: @@ -104,7 +147,21 @@ Bu yüzden `SUPABASE_SERVICE_ROLE_KEY` zorunludur. Bu key asla browser'a gönder ## SQL Dosyaları Nasıl Kullanılmalı? -Bu repodaki SQL dosyalarını otomatik migration sistemi gibi düşünmüyorum. Bunlar benim migration geçmişimi ve Supabase tarafında beklenen yapıyı belgeleyen kaynaklar. +Yeni kurulum için en pratik dosya: + +```txt +supabase/setup.sql +``` + +Bu dosya tek seferde çalıştırılacak kurulum dosyasıdır. + +Parçalı dosyalar ise migration geçmişini ve geliştirme sürecini belgelemek için korunur: + +```txt +supabase/schema.sql +supabase/migrations/ +docs/database/ +``` Yeni bir kurulum yaparken en güvenilir kaynak, çalışan Supabase projesinden aldığınız güncel SQL çıktısıdır. Supabase panelindeki "copy as SQL" çıktısı bu yüzden önemlidir. diff --git a/docs/06-vercel-deploy.md b/docs/06-vercel-deploy.md index cd6757f..d8abbdb 100644 --- a/docs/06-vercel-deploy.md +++ b/docs/06-vercel-deploy.md @@ -19,6 +19,12 @@ Deploy'dan önce hazır olması gerekenler: - Supabase tabloları, policy'leri, function'ları ve storage bucket'ları. - Environment variable değerleri. +Supabase tarafını yeni kuruyorsanız önce `supabase/setup.sql` dosyasını Supabase SQL Editor'da tek seferde çalıştırın. Terminal kullanıyorsanız aynı işlem şu komutla yapılabilir: + +```bash +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql +``` + Gerekli env değerleri: ```env diff --git a/docs/07-coolify-deploy.md b/docs/07-coolify-deploy.md index bfcc84e..cf7f3a2 100644 --- a/docs/07-coolify-deploy.md +++ b/docs/07-coolify-deploy.md @@ -22,6 +22,14 @@ Coolify deploy için hazır olması gerekenler: Supabase tarafında tablo, policy, function ve storage bucket'ları hazır olmalıdır. +Yeni bir Supabase projesi hazırlıyorsanız `supabase/setup.sql` dosyasını tek seferde çalıştırabilirsiniz: + +```bash +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql +``` + +Supabase panelinden ilerlemek isterseniz aynı dosyanın tamamını SQL Editor'a yapıştırıp çalıştırmanız yeterli. + ## Uygulama Oluşturma Coolify içinde yeni bir application oluştururken: diff --git a/docs/08-dokploy-deploy.md b/docs/08-dokploy-deploy.md index fd0761d..8fb74ff 100644 --- a/docs/08-dokploy-deploy.md +++ b/docs/08-dokploy-deploy.md @@ -20,6 +20,14 @@ Başlamadan önce şunlar hazır olmalı: - Supabase schema ve storage yapısı. - Ortam değişkenleri. +Supabase tarafı hazır değilse önce `supabase/setup.sql` dosyasını çalıştırın. Terminalden tek komutla kurmak için: + +```bash +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql +``` + +SQL Editor kullanıyorsanız `supabase/setup.sql` içeriğini komple yapıştırıp çalıştırabilirsiniz. + ## Dokploy'da Uygulama Oluşturma Dokploy içinde yeni bir uygulama oluştururken GitHub reposunu seçin. Uygulamayı Node.js/Next.js app olarak yapılandırın. diff --git a/docs/database/query-order.md b/docs/database/query-order.md index c4275b6..8d7ef3d 100644 --- a/docs/database/query-order.md +++ b/docs/database/query-order.md @@ -33,10 +33,10 @@ They must also be documented and registered in this file, but they should only b ## Applying The Ordered SQL -Use the migration helper from the repository root: +For a fresh Supabase project, use the one-shot setup file from the repository root: ```bash -DATABASE_URL='postgresql://postgres:password@host:5432/postgres' sh ./scripts/apply-migrations.sh +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql ``` -The helper applies missing queries from `0001` through `0011` in the order listed above and records completed migrations in `neta_internal.schema_migrations`. It uses local `psql` when available, otherwise it runs `psql` through Docker. After migrations, it sends `NOTIFY pgrst, 'reload schema'` so PostgREST can see new RPC functions without a manual restart. +The setup file applies `0001` through `0011` in the order listed above. It does not include optional seed data. If you are changing the schema later, keep adding ordered files under `supabase/migrations/` and update this table. diff --git a/supabase/README.md b/supabase/README.md index e161d32..7a801a0 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -2,6 +2,22 @@ This directory stores SQL files for the project's Supabase/PostgreSQL database. +## One-Shot Setup + +For a fresh Supabase project, use: + +```text +supabase/setup.sql +``` + +You can paste the full file into Supabase SQL Editor and run it once, or run it from the repository root with `psql`: + +```bash +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql +``` + +The setup file is generated by concatenating `schema.sql` and migrations `0002` through `0011` in the canonical order. Demo seed data is intentionally not included. + ## Current Baseline - `schema.sql` is the first registered database SQL file. @@ -42,10 +58,10 @@ Do not overwrite already executed SQL without also creating a new ordered migrat ## Apply Migrations -From the repository root: +For a new project, prefer the one-shot setup file: ```bash -DATABASE_URL='postgresql://postgres:password@host:5432/postgres' bash ./scripts/apply-migrations.sh +psql "postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres" -v ON_ERROR_STOP=1 -f supabase/setup.sql ``` -The script applies all required schema files in canonical order. It requires either local `psql` or Docker. If `psql` is not installed, it runs `psql` through the `postgres:16-alpine` Docker image. +The ordered migration files remain as the source history for future schema changes. diff --git a/supabase/migrations/0004_add_business_os_tables.sql b/supabase/migrations/0004_add_business_os_tables.sql index 76e2580..b688423 100644 --- a/supabase/migrations/0004_add_business_os_tables.sql +++ b/supabase/migrations/0004_add_business_os_tables.sql @@ -70,25 +70,41 @@ 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); diff --git a/supabase/migrations/0005_add_advanced_crm_tables.sql b/supabase/migrations/0005_add_advanced_crm_tables.sql index 4ad08ae..ab11f1c 100644 --- a/supabase/migrations/0005_add_advanced_crm_tables.sql +++ b/supabase/migrations/0005_add_advanced_crm_tables.sql @@ -24,7 +24,11 @@ create table if not exists public.client_activities ( 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); diff --git a/supabase/migrations/0006_add_pgvector_and_embeddings.sql b/supabase/migrations/0006_add_pgvector_and_embeddings.sql index 0f017c9..028e800 100644 --- a/supabase/migrations/0006_add_pgvector_and_embeddings.sql +++ b/supabase/migrations/0006_add_pgvector_and_embeddings.sql @@ -16,9 +16,13 @@ create table if not exists public.document_embeddings ( -- 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 diff --git a/supabase/migrations/0007_add_client_portal_tables.sql b/supabase/migrations/0007_add_client_portal_tables.sql index eca26ae..e49f390 100644 --- a/supabase/migrations/0007_add_client_portal_tables.sql +++ b/supabase/migrations/0007_add_client_portal_tables.sql @@ -23,6 +23,7 @@ CREATE TABLE IF NOT EXISTS public.project_revisions ( ); -- 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 @@ -35,18 +36,21 @@ ALTER TABLE public.project_revisions ENABLE ROW LEVEL SECURITY; -- 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 ( @@ -57,6 +61,7 @@ USING ( -- 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 ( @@ -70,6 +75,7 @@ USING ( -- 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 ( @@ -82,6 +88,7 @@ USING ( -- 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 ( @@ -91,6 +98,7 @@ USING ( ); -- 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 ( @@ -101,6 +109,7 @@ WITH CHECK ( ); -- 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 ( diff --git a/supabase/setup.sql b/supabase/setup.sql new file mode 100644 index 0000000..39c36d1 --- /dev/null +++ b/supabase/setup.sql @@ -0,0 +1,1563 @@ +-- 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';