636 lines
21 KiB
SQL
636 lines
21 KiB
SQL
-- 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);
|