Banco de dados
Supabase Self-Hosted · instancia nao configurada
starchat-schema.sql
DDL + RLS + GRANTS
-- ============================================================
-- StarChat :: Schema multi-tenant para Supabase Self-Hosted
-- Executar no SQL Editor da instancia (VPS).
-- ============================================================
create extension if not exists "pgcrypto";
-- ---------- Enums ----------
do $$ begin
create type app_role as enum ('admin', 'supervisor', 'agent');
exception when duplicate_object then null; end $$;
do $$ begin
create type wa_provider as enum ('meta_cloud', 'evolution');
exception when duplicate_object then null; end $$;
do $$ begin
create type instance_status as enum ('connected', 'connecting', 'disconnected');
exception when duplicate_object then null; end $$;
do $$ begin
create type conversation_status as enum ('open', 'pending', 'closed');
exception when duplicate_object then null; end $$;
do $$ begin
create type message_direction as enum ('inbound', 'outbound');
exception when duplicate_object then null; end $$;
do $$ begin
create type message_kind as enum ('text', 'image', 'audio', 'video', 'document', 'note', 'template');
exception when duplicate_object then null; end $$;
-- ---------- organizations (tenant raiz) ----------
create table if not exists public.organizations (
id uuid primary key default gen_random_uuid(),
name text not null,
slug text unique not null,
plan text not null default 'starter',
timezone text not null default 'America/Sao_Paulo',
business_hours jsonb not null default '{"mon_fri":["08:00","18:00"],"sat":["08:00","12:00"]}'::jsonb,
created_at timestamptz not null default now()
);
-- ---------- profiles (usuarios do tenant) ----------
create table if not exists public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
organization_id uuid not null references public.organizations(id) on delete cascade,
full_name text not null,
email text not null,
avatar_url text,
is_active boolean not null default true,
created_at timestamptz not null default now()
);
create index if not exists profiles_org_idx on public.profiles(organization_id);
-- ---------- user_roles (papeis SEMPRE em tabela separada) ----------
create table if not exists public.user_roles (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
organization_id uuid not null references public.organizations(id) on delete cascade,
role app_role not null,
unique (user_id, organization_id, role)
);
-- ---------- departments / filas ----------
create table if not exists public.departments (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
name text not null,
sla_minutes int not null default 5,
created_at timestamptz not null default now()
);
-- ---------- whatsapp_instances (Meta Cloud + Evolution) ----------
create table if not exists public.whatsapp_instances (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
department_id uuid references public.departments(id) on delete set null,
name text not null,
provider wa_provider not null,
status instance_status not null default 'disconnected',
phone_number text,
-- Meta Cloud API
waba_id text,
phone_number_id text,
access_token text,
-- Evolution API
server_url text,
global_api_key text,
instance_name text,
webhook_secret text default encode(gen_random_bytes(24), 'hex'),
created_at timestamptz not null default now()
);
create index if not exists wa_instances_org_idx on public.whatsapp_instances(organization_id);
-- ---------- contacts ----------
create table if not exists public.contacts (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
name text not null,
phone text not null,
email text,
document text, -- CPF / CNPJ
medical_record text, -- prontuario (StarLaudo)
balance_due numeric(12,2) not null default 0,
tags text[] not null default '{}',
custom_fields jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
unique (organization_id, phone)
);
create index if not exists contacts_org_idx on public.contacts(organization_id);
-- ---------- conversations (tickets / cards do CRM) ----------
create table if not exists public.conversations (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
contact_id uuid not null references public.contacts(id) on delete cascade,
instance_id uuid references public.whatsapp_instances(id) on delete set null,
department_id uuid references public.departments(id) on delete set null,
assignee_id uuid references public.profiles(id) on delete set null,
status conversation_status not null default 'open',
stage text not null default 'Novo Lead',
deal_value numeric(12,2) not null default 0,
tags text[] not null default '{}',
unread_count int not null default 0,
last_message_at timestamptz not null default now(),
created_at timestamptz not null default now()
);
create index if not exists conversations_org_status_idx on public.conversations(organization_id, status);
create index if not exists conversations_assignee_idx on public.conversations(assignee_id);
-- ---------- messages ----------
create table if not exists public.messages (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
conversation_id uuid not null references public.conversations(id) on delete cascade,
sender_id uuid references public.profiles(id) on delete set null,
direction message_direction not null,
kind message_kind not null default 'text',
body text,
media_url text,
is_private boolean not null default false, -- nota interna
provider_message_id text,
delivered_at timestamptz,
read_at timestamptz,
created_at timestamptz not null default now()
);
create index if not exists messages_conversation_idx on public.messages(conversation_id, created_at desc);
-- ---------- automations / chatbot ----------
create table if not exists public.automations (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
name text not null,
trigger_type text not null,
is_active boolean not null default true,
flow jsonb not null default '[]'::jsonb,
created_at timestamptz not null default now()
);
-- ---------- api_keys (StarGestor / StarLaudo) ----------
create table if not exists public.api_keys (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
name text not null,
key_hash text not null,
scopes text[] not null default '{}',
last_used_at timestamptz,
revoked_at timestamptz,
created_at timestamptz not null default now()
);
-- ---------- webhooks ----------
create table if not exists public.webhooks (
id uuid primary key default gen_random_uuid(),
organization_id uuid not null references public.organizations(id) on delete cascade,
target_url text not null,
events text[] not null default '{}',
secret text not null default encode(gen_random_bytes(24), 'hex'),
is_active boolean not null default true,
last_status int,
created_at timestamptz not null default now()
);
-- ============================================================
-- GRANTS (obrigatorio: PostgREST nao concede por padrao)
-- ============================================================
grant select on public.organizations to authenticated;
grant all on public.organizations to service_role;
grant select, insert, update on public.profiles to authenticated;
grant all on public.profiles to service_role;
grant select on public.user_roles to authenticated;
grant all on public.user_roles to service_role;
grant select, insert, update, delete on public.departments to authenticated;
grant all on public.departments to service_role;
grant select, insert, update, delete on public.whatsapp_instances to authenticated;
grant all on public.whatsapp_instances to service_role;
grant select, insert, update, delete on public.contacts to authenticated;
grant all on public.contacts to service_role;
grant select, insert, update, delete on public.conversations to authenticated;
grant all on public.conversations to service_role;
grant select, insert, update, delete on public.messages to authenticated;
grant all on public.messages to service_role;
grant select, insert, update, delete on public.automations to authenticated;
grant all on public.automations to service_role;
grant select, insert, update, delete on public.api_keys to authenticated;
grant all on public.api_keys to service_role;
grant select, insert, update, delete on public.webhooks to authenticated;
grant all on public.webhooks to service_role;
-- ============================================================
-- Funcoes de seguranca (security definer, evitam recursao em RLS)
-- ============================================================
create or replace function public.has_role(_user_id uuid, _role app_role)
returns boolean language sql stable security definer set search_path = public as $$
select exists (select 1 from public.user_roles where user_id = _user_id and role = _role);
$$;
create or replace function public.current_org()
returns uuid language sql stable security definer set search_path = public as $$
select organization_id from public.profiles where id = auth.uid();
$$;
-- ============================================================
-- RLS
-- ============================================================
alter table public.organizations enable row level security;
alter table public.profiles enable row level security;
alter table public.user_roles enable row level security;
alter table public.departments enable row level security;
alter table public.whatsapp_instances enable row level security;
alter table public.contacts enable row level security;
alter table public.conversations enable row level security;
alter table public.messages enable row level security;
alter table public.automations enable row level security;
alter table public.api_keys enable row level security;
alter table public.webhooks enable row level security;
create policy "org members read org" on public.organizations
for select to authenticated using (id = public.current_org());
create policy "read profiles of my org" on public.profiles
for select to authenticated using (organization_id = public.current_org());
create policy "update own profile" on public.profiles
for update to authenticated using (id = auth.uid());
create policy "read my roles" on public.user_roles
for select to authenticated using (user_id = auth.uid() or organization_id = public.current_org());
-- Tabelas operacionais: isolamento por tenant
do $$
declare t text;
begin
foreach t in array array['departments','whatsapp_instances','contacts','conversations','messages','automations','webhooks']
loop
execute format($f$
create policy "tenant read %1$s" on public.%1$s
for select to authenticated using (organization_id = public.current_org());
create policy "tenant write %1$s" on public.%1$s
for insert to authenticated with check (organization_id = public.current_org());
create policy "tenant update %1$s" on public.%1$s
for update to authenticated using (organization_id = public.current_org());
create policy "tenant delete %1$s" on public.%1$s
for delete to authenticated using (organization_id = public.current_org()
and (public.has_role(auth.uid(),'admin') or public.has_role(auth.uid(),'supervisor')));
$f$, t);
end loop;
end $$;
-- Credenciais sensiveis: somente admin do tenant
create policy "admin manage api keys" on public.api_keys
for all to authenticated
using (organization_id = public.current_org() and public.has_role(auth.uid(),'admin'))
with check (organization_id = public.current_org() and public.has_role(auth.uid(),'admin'));
-- ============================================================
-- Realtime (Live Chat)
-- ============================================================
alter publication supabase_realtime add table public.messages;
alter publication supabase_realtime add table public.conversations;