-- Enable UUID extension create extension if not exists "uuid-ossp"; -- ===================================================== -- ENUMS -- ===================================================== do $$ begin create type member_role as enum ('admin', 'board', 'treasurer', 'member', 'associate'); exception when duplicate_object then null; end $$; do $$ begin create type member_tier as enum ('plus', 'basic', 'associate'); exception when duplicate_object then null; end $$; do $$ begin create type member_status as enum ('active', 'pending', 'inactive'); exception when duplicate_object then null; end $$; do $$ begin create type payment_status as enum ('current', 'late', '3mo_late', 'unverified'); exception when duplicate_object then null; end $$; do $$ begin create type task_type as enum ('chore', 'task'); exception when duplicate_object then null; end $$; do $$ begin create type task_status as enum ('open', 'claimed', 'in_progress', 'overdue', 'due_today', 'completed'); exception when duplicate_object then null; end $$; do $$ begin create type recurrence_type as enum ('daily', 'weekly', 'biweekly', 'monthly', 'none'); exception when duplicate_object then null; end $$; do $$ begin create type project_status as enum ('backlog', 'in_progress', 'review', 'done'); exception when duplicate_object then null; end $$; do $$ begin create type kb_visibility as enum ('all_members', 'board', 'admin_only'); exception when duplicate_object then null; end $$; do $$ begin create type area_lead_status as enum ('active', 'vacant', 'handoff'); exception when duplicate_object then null; end $$; do $$ begin create type contact_type as enum ('vendor', 'supplier', 'partner', 'landlord', 'city'); exception when duplicate_object then null; end $$; do $$ begin create type payment_platform as enum ('paypal', 'zeffy', 'venmo', 'cash'); exception when duplicate_object then null; end $$; do $$ begin create type payment_link_status as enum ('linked', 'unlinked'); exception when duplicate_object then null; end $$; do $$ begin create type channel_type as enum ('general', 'area', 'ops', 'project'); exception when duplicate_object then null; end $$; -- ===================================================== -- SPACES (Multi-tenant containers) -- ===================================================== create table if not exists public.spaces ( id uuid primary key default uuid_generate_v4(), name text not null, slug text unique not null, city text, require_approval boolean default true, public_member_directory boolean default false, invite_code text unique not null, webhook_secret text, created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.spaces enable row level security; -- ===================================================== -- SPACE MEMBERS (Users in spaces with roles) -- ===================================================== create table if not exists public.space_members ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, user_id uuid references auth.users(id) on delete cascade, role text default 'member', tier text default 'basic', display_name text not null, email text, handle text, phone text, status text default 'active', payment_status text, payment_note text, joined_at timestamp with time zone default now(), last_paid_at timestamp with time zone, has_card_access boolean default false, unique(space_id, user_id) ); alter table public.space_members enable row level security; -- Now add spaces RLS policies (after space_members exists) create policy "spaces_select_members" on public.spaces for select using ( exists ( select 1 from public.space_members where space_members.space_id = spaces.id and space_members.user_id = auth.uid() ) ); create policy "spaces_insert_authenticated" on public.spaces for insert with check (auth.uid() is not null); create policy "spaces_update_admins" on public.spaces for update using ( exists ( select 1 from public.space_members where space_members.space_id = spaces.id and space_members.user_id = auth.uid() and space_members.role = 'admin' ) ); -- space_members RLS policies create policy "space_members_select_own_space" on public.space_members for select using ( user_id = auth.uid() or space_id in ( select space_id from public.space_members where user_id = auth.uid() ) ); create policy "space_members_insert_authenticated" on public.space_members for insert with check (auth.uid() = user_id); create policy "space_members_update_admins" on public.space_members for update using ( user_id = auth.uid() or exists ( select 1 from public.space_members sm where sm.space_id = space_members.space_id and sm.user_id = auth.uid() and sm.role = 'admin' ) ); create policy "space_members_delete_admins" on public.space_members for delete using ( exists ( select 1 from public.space_members sm where sm.space_id = space_members.space_id and sm.user_id = auth.uid() and sm.role = 'admin' ) ); -- ===================================================== -- TASKS & CHORES -- ===================================================== create table if not exists public.tasks ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, title text not null, description text, type task_type default 'task', status task_status default 'open', area text, recurrence recurrence_type default 'none', due_date timestamp with time zone, assigned_to uuid references auth.users(id) on delete set null, assigned_to_name text, claimed_by uuid references auth.users(id) on delete set null, claimed_by_name text, requested_by uuid references auth.users(id) on delete set null, requested_by_name text, subtask_total integer default 0, subtask_completed integer default 0, last_done_at timestamp with time zone, completed_at timestamp with time zone, created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.tasks enable row level security; create policy "tasks_select_members" on public.tasks for select using ( exists ( select 1 from public.space_members where space_members.space_id = tasks.space_id and space_members.user_id = auth.uid() ) ); create policy "tasks_insert_members" on public.tasks for insert with check ( exists ( select 1 from public.space_members where space_members.space_id = tasks.space_id and space_members.user_id = auth.uid() ) ); create policy "tasks_update_members" on public.tasks for update using ( exists ( select 1 from public.space_members where space_members.space_id = tasks.space_id and space_members.user_id = auth.uid() ) ); create policy "tasks_delete_members" on public.tasks for delete using ( exists ( select 1 from public.space_members where space_members.space_id = tasks.space_id and space_members.user_id = auth.uid() ) ); -- ===================================================== -- PROJECTS -- ===================================================== create table if not exists public.projects ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, title text not null, description text, status project_status default 'backlog', area text, tags text[], assignee_names text[], task_count integer default 0, tasks_completed integer default 0, due_date timestamp with time zone, created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.projects enable row level security; create policy "projects_select_members" on public.projects for select using ( exists ( select 1 from public.space_members where space_members.space_id = projects.space_id and space_members.user_id = auth.uid() ) ); create policy "projects_insert_members" on public.projects for insert with check ( exists ( select 1 from public.space_members where space_members.space_id = projects.space_id and space_members.user_id = auth.uid() ) ); create policy "projects_update_members" on public.projects for update using ( exists ( select 1 from public.space_members where space_members.space_id = projects.space_id and space_members.user_id = auth.uid() ) ); create policy "projects_delete_members" on public.projects for delete using ( exists ( select 1 from public.space_members where space_members.space_id = projects.space_id and space_members.user_id = auth.uid() ) ); -- ===================================================== -- KNOWLEDGE BASE -- ===================================================== create table if not exists public.knowledge_base ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, title text not null, content text, icon text, visibility kb_visibility default 'all_members', area text, tags text[], is_pinned boolean default false, updated_by_id uuid references auth.users(id) on delete set null, updated_by_name text, created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.knowledge_base enable row level security; create policy "kb_select_members" on public.knowledge_base for select using ( exists ( select 1 from public.space_members where space_members.space_id = knowledge_base.space_id and space_members.user_id = auth.uid() and ( knowledge_base.visibility = 'all_members' or (knowledge_base.visibility = 'board' and space_members.role in ('admin', 'board', 'treasurer')) or (knowledge_base.visibility = 'admin_only' and space_members.role = 'admin') ) ) ); create policy "kb_insert_members" on public.knowledge_base for insert with check ( exists ( select 1 from public.space_members where space_members.space_id = knowledge_base.space_id and space_members.user_id = auth.uid() ) ); create policy "kb_update_members" on public.knowledge_base for update using ( exists ( select 1 from public.space_members where space_members.space_id = knowledge_base.space_id and space_members.user_id = auth.uid() ) ); create policy "kb_delete_board" on public.knowledge_base for delete using ( exists ( select 1 from public.space_members where space_members.space_id = knowledge_base.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'board') ) ); -- ===================================================== -- SECRETS VAULT -- ===================================================== create table if not exists public.secrets ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, title text not null, icon text, description text, value text not null, area text, created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.secrets enable row level security; create policy "secrets_select_admins" on public.secrets for select using ( exists ( select 1 from public.space_members where space_members.space_id = secrets.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'board', 'treasurer') ) ); create policy "secrets_modify_admins" on public.secrets for all using ( exists ( select 1 from public.space_members where space_members.space_id = secrets.space_id and space_members.user_id = auth.uid() and space_members.role = 'admin' ) ); -- ===================================================== -- AREA LEADS -- ===================================================== create table if not exists public.area_leads ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, area_name text not null, area_code text not null, lead_id uuid references auth.users(id) on delete set null, lead_handle text, status area_lead_status default 'active', created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.area_leads enable row level security; create policy "area_leads_select_members" on public.area_leads for select using ( exists ( select 1 from public.space_members where space_members.space_id = area_leads.space_id and space_members.user_id = auth.uid() ) ); create policy "area_leads_modify_board" on public.area_leads for all using ( exists ( select 1 from public.space_members where space_members.space_id = area_leads.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'board') ) ); -- ===================================================== -- CONTACTS -- ===================================================== create table if not exists public.contacts ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, name text not null, code text not null, contact_type contact_type not null, email text, phone text, details text, tags text[], created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.contacts enable row level security; create policy "contacts_select_members" on public.contacts for select using ( exists ( select 1 from public.space_members where space_members.space_id = contacts.space_id and space_members.user_id = auth.uid() ) ); create policy "contacts_modify_members" on public.contacts for all using ( exists ( select 1 from public.space_members where space_members.space_id = contacts.space_id and space_members.user_id = auth.uid() ) ); -- ===================================================== -- PAYMENTS -- ===================================================== create table if not exists public.payments ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, platform payment_platform not null, amount numeric(10,2) not null, from_identifier text, from_note text, member_id uuid references public.space_members(id) on delete set null, member_name text, link_status payment_link_status default 'unlinked', transaction_date timestamp with time zone not null, created_at timestamp with time zone default now() ); alter table public.payments enable row level security; create policy "payments_select_treasurer" on public.payments for select using ( exists ( select 1 from public.space_members where space_members.space_id = payments.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'board', 'treasurer') ) ); create policy "payments_modify_treasurer" on public.payments for all using ( exists ( select 1 from public.space_members where space_members.space_id = payments.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'treasurer') ) ); -- ===================================================== -- COMMS CHANNELS -- ===================================================== create table if not exists public.comms_channels ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, name text not null, icon text, channel_type channel_type default 'general', area_reference text, project_id uuid references public.projects(id) on delete cascade, member_count integer default 0, created_at timestamp with time zone default now() ); alter table public.comms_channels enable row level security; create policy "channels_select_members" on public.comms_channels for select using ( exists ( select 1 from public.space_members where space_members.space_id = comms_channels.space_id and space_members.user_id = auth.uid() ) ); create policy "channels_insert_board" on public.comms_channels for insert with check ( exists ( select 1 from public.space_members where space_members.space_id = comms_channels.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'board') ) ); create policy "channels_update_board" on public.comms_channels for update using ( exists ( select 1 from public.space_members where space_members.space_id = comms_channels.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'board') ) ); create policy "channels_delete_board" on public.comms_channels for delete using ( exists ( select 1 from public.space_members where space_members.space_id = comms_channels.space_id and space_members.user_id = auth.uid() and space_members.role in ('admin', 'board') ) ); -- ===================================================== -- COMMS MESSAGES (stored for history; real-time via CLASP) -- ===================================================== create table if not exists public.comms_messages ( id uuid primary key default uuid_generate_v4(), channel_id uuid references public.comms_channels(id) on delete cascade not null, space_id uuid references public.spaces(id) on delete cascade not null, user_id uuid references auth.users(id) on delete set null, display_name text not null, handle text, content text not null, created_at timestamp with time zone default now() ); alter table public.comms_messages enable row level security; create policy "messages_select_members" on public.comms_messages for select using ( exists ( select 1 from public.space_members where space_members.space_id = comms_messages.space_id and space_members.user_id = auth.uid() ) ); create policy "messages_insert_members" on public.comms_messages for insert with check ( exists ( select 1 from public.space_members where space_members.space_id = comms_messages.space_id and space_members.user_id = auth.uid() ) and user_id = auth.uid() ); -- ===================================================== -- INTEGRATIONS -- ===================================================== create table if not exists public.integrations ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, name text not null, platform text not null unique, description text, is_connected boolean default false, config jsonb, created_at timestamp with time zone default now(), updated_at timestamp with time zone default now() ); alter table public.integrations enable row level security; create policy "integrations_select_admins" on public.integrations for select using ( exists ( select 1 from public.space_members where space_members.space_id = integrations.space_id and space_members.user_id = auth.uid() and space_members.role = 'admin' ) ); create policy "integrations_modify_admins" on public.integrations for all using ( exists ( select 1 from public.space_members where space_members.space_id = integrations.space_id and space_members.user_id = auth.uid() and space_members.role = 'admin' ) ); -- ===================================================== -- ACTIVITY LOG -- ===================================================== create table if not exists public.activity_log ( id uuid primary key default uuid_generate_v4(), space_id uuid references public.spaces(id) on delete cascade not null, user_id uuid references auth.users(id) on delete set null, display_name text, action text not null, entity_type text, entity_id uuid, details text, created_at timestamp with time zone default now() ); alter table public.activity_log enable row level security; create policy "activity_select_members" on public.activity_log for select using ( exists ( select 1 from public.space_members where space_members.space_id = activity_log.space_id and space_members.user_id = auth.uid() ) ); create policy "activity_insert_members" on public.activity_log for insert with check ( exists ( select 1 from public.space_members where space_members.space_id = activity_log.space_id and space_members.user_id = auth.uid() ) ); -- ===================================================== -- INDEXES for Performance -- ===================================================== create index if not exists idx_space_members_space_id on public.space_members(space_id); create index if not exists idx_space_members_user_id on public.space_members(user_id); create index if not exists idx_tasks_space_id on public.tasks(space_id); create index if not exists idx_projects_space_id on public.projects(space_id); create index if not exists idx_kb_space_id on public.knowledge_base(space_id); create index if not exists idx_messages_channel_id on public.comms_messages(channel_id); create index if not exists idx_messages_space_id on public.comms_messages(space_id); create index if not exists idx_payments_space_id on public.payments(space_id); create index if not exists idx_activity_space_id on public.activity_log(space_id); create index if not exists idx_contacts_space_id on public.contacts(space_id); create index if not exists idx_area_leads_space_id on public.area_leads(space_id);