2026-05-08 20:45:16 +08:00
|
|
|
-- Mike Supabase schema
|
2026-06-15 17:34:58 +08:00
|
|
|
-- Use this for a fresh Supabase database. Existing deployments should instead
|
|
|
|
|
-- apply the dated incremental migration files in backend/migrations that are
|
|
|
|
|
-- newer than the version of Mike they currently have deployed.
|
2026-05-08 20:45:16 +08:00
|
|
|
|
|
|
|
|
create extension if not exists "pgcrypto";
|
|
|
|
|
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
-- User profiles
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
create table if not exists public.user_profiles (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id uuid not null unique references auth.users(id) on delete cascade,
|
|
|
|
|
display_name text,
|
|
|
|
|
organisation text,
|
|
|
|
|
tier text not null default 'Free',
|
|
|
|
|
message_credits_used integer not null default 0,
|
|
|
|
|
credits_reset_date timestamptz not null default (now() + interval '30 days'),
|
2026-06-06 15:48:47 +08:00
|
|
|
title_model text,
|
2026-05-08 20:45:16 +08:00
|
|
|
tabular_model text not null default 'gemini-3-flash-preview',
|
2026-06-06 15:48:47 +08:00
|
|
|
quote_model text,
|
2026-06-10 03:48:08 +08:00
|
|
|
mfa_on_login boolean not null default false,
|
2026-06-11 21:50:58 +08:00
|
|
|
legal_research_us boolean not null default true,
|
2026-05-08 20:45:16 +08:00
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_user_profiles_user
|
|
|
|
|
on public.user_profiles(user_id);
|
|
|
|
|
|
|
|
|
|
create or replace function public.handle_new_user()
|
|
|
|
|
returns trigger
|
|
|
|
|
language plpgsql
|
|
|
|
|
security definer
|
|
|
|
|
set search_path = public
|
|
|
|
|
as $$
|
|
|
|
|
begin
|
|
|
|
|
insert into public.user_profiles (user_id)
|
|
|
|
|
values (new.id)
|
|
|
|
|
on conflict (user_id) do nothing;
|
|
|
|
|
return new;
|
|
|
|
|
exception when others then
|
|
|
|
|
-- Never block signup if the profile insert fails.
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
create table if not exists public.user_api_keys (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
2026-06-06 15:48:47 +08:00
|
|
|
provider text not null check (provider in ('claude', 'gemini', 'openai', 'openrouter', 'courtlistener')),
|
2026-05-08 20:45:16 +08:00
|
|
|
encrypted_key text not null,
|
|
|
|
|
iv text not null,
|
|
|
|
|
auth_tag text not null,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now(),
|
|
|
|
|
unique(user_id, provider)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_user_api_keys_user
|
|
|
|
|
on public.user_api_keys(user_id);
|
|
|
|
|
|
2026-06-09 01:46:58 +08:00
|
|
|
alter table public.user_api_keys enable row level security;
|
|
|
|
|
|
2026-06-15 17:34:58 +08:00
|
|
|
create table if not exists public.user_mcp_connectors (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
|
|
|
name text not null,
|
|
|
|
|
transport text not null default 'streamable_http'
|
|
|
|
|
check (transport in ('streamable_http')),
|
|
|
|
|
server_url text not null,
|
|
|
|
|
auth_type text not null default 'none'
|
|
|
|
|
check (auth_type in ('none', 'bearer', 'oauth')),
|
|
|
|
|
enabled boolean not null default true,
|
|
|
|
|
tool_policy jsonb not null default '{}'::jsonb,
|
|
|
|
|
encrypted_auth_config text,
|
|
|
|
|
auth_config_iv text,
|
|
|
|
|
auth_config_tag text,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_user_mcp_connectors_user
|
|
|
|
|
on public.user_mcp_connectors(user_id);
|
|
|
|
|
|
|
|
|
|
alter table public.user_mcp_connectors enable row level security;
|
|
|
|
|
|
|
|
|
|
create table if not exists public.user_mcp_oauth_tokens (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
connector_id uuid not null references public.user_mcp_connectors(id) on delete cascade,
|
|
|
|
|
encrypted_access_token text,
|
|
|
|
|
access_token_iv text,
|
|
|
|
|
access_token_tag text,
|
|
|
|
|
encrypted_refresh_token text,
|
|
|
|
|
refresh_token_iv text,
|
|
|
|
|
refresh_token_tag text,
|
|
|
|
|
token_type text,
|
|
|
|
|
scope text,
|
|
|
|
|
expires_at timestamptz,
|
|
|
|
|
authorization_server text,
|
|
|
|
|
token_endpoint text,
|
|
|
|
|
client_id text,
|
|
|
|
|
encrypted_client_secret text,
|
|
|
|
|
client_secret_iv text,
|
|
|
|
|
client_secret_tag text,
|
|
|
|
|
resource text,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now(),
|
|
|
|
|
unique(connector_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
alter table public.user_mcp_oauth_tokens enable row level security;
|
|
|
|
|
|
|
|
|
|
create table if not exists public.user_mcp_oauth_states (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
|
|
|
connector_id uuid not null references public.user_mcp_connectors(id) on delete cascade,
|
|
|
|
|
state_hash text not null unique,
|
|
|
|
|
encrypted_state_config text not null,
|
|
|
|
|
state_config_iv text not null,
|
|
|
|
|
state_config_tag text not null,
|
|
|
|
|
expires_at timestamptz not null,
|
|
|
|
|
created_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_user_mcp_oauth_states_expires
|
|
|
|
|
on public.user_mcp_oauth_states(expires_at);
|
|
|
|
|
|
|
|
|
|
alter table public.user_mcp_oauth_states enable row level security;
|
|
|
|
|
|
|
|
|
|
create table if not exists public.user_mcp_connector_tools (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
connector_id uuid not null references public.user_mcp_connectors(id) on delete cascade,
|
|
|
|
|
tool_name text not null,
|
|
|
|
|
openai_tool_name text not null,
|
|
|
|
|
title text,
|
|
|
|
|
description text,
|
|
|
|
|
input_schema jsonb not null default '{"type":"object","properties":{}}'::jsonb,
|
|
|
|
|
output_schema jsonb,
|
|
|
|
|
annotations jsonb not null default '{}'::jsonb,
|
|
|
|
|
enabled boolean not null default true,
|
|
|
|
|
requires_confirmation boolean not null default false,
|
|
|
|
|
last_seen_at timestamptz not null default now(),
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now(),
|
|
|
|
|
unique(connector_id, tool_name),
|
|
|
|
|
unique(openai_tool_name)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_user_mcp_connector_tools_connector
|
|
|
|
|
on public.user_mcp_connector_tools(connector_id);
|
|
|
|
|
|
|
|
|
|
alter table public.user_mcp_connector_tools enable row level security;
|
|
|
|
|
|
|
|
|
|
create table if not exists public.user_mcp_tool_audit_logs (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
|
|
|
connector_id uuid not null references public.user_mcp_connectors(id) on delete cascade,
|
|
|
|
|
tool_id uuid references public.user_mcp_connector_tools(id) on delete set null,
|
|
|
|
|
tool_name text not null,
|
|
|
|
|
openai_tool_name text not null,
|
|
|
|
|
status text not null check (status in ('ok', 'error')),
|
|
|
|
|
error_message text,
|
|
|
|
|
duration_ms integer not null default 0,
|
|
|
|
|
result_size_chars integer not null default 0,
|
|
|
|
|
created_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_user_mcp_tool_audit_logs_user_created
|
|
|
|
|
on public.user_mcp_tool_audit_logs(user_id, created_at desc);
|
|
|
|
|
|
|
|
|
|
alter table public.user_mcp_tool_audit_logs enable row level security;
|
|
|
|
|
|
2026-05-08 20:45:16 +08:00
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
-- Projects and documents
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
create table if not exists public.projects (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id text not null,
|
|
|
|
|
name text not null,
|
|
|
|
|
cm_number text,
|
|
|
|
|
visibility text not null default 'private',
|
|
|
|
|
shared_with jsonb not null default '[]'::jsonb,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_projects_user
|
|
|
|
|
on public.projects(user_id);
|
|
|
|
|
|
|
|
|
|
create index if not exists projects_shared_with_idx
|
|
|
|
|
on public.projects using gin (shared_with);
|
|
|
|
|
|
|
|
|
|
create table if not exists public.project_subfolders (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
project_id uuid not null references public.projects(id) on delete cascade,
|
|
|
|
|
user_id text not null,
|
|
|
|
|
name text not null,
|
|
|
|
|
parent_folder_id uuid references public.project_subfolders(id) on delete cascade,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_project_subfolders_project
|
|
|
|
|
on public.project_subfolders(project_id);
|
|
|
|
|
|
|
|
|
|
create table if not exists public.documents (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
project_id uuid references public.projects(id) on delete cascade,
|
|
|
|
|
user_id text not null,
|
|
|
|
|
status text not null default 'pending',
|
|
|
|
|
folder_id uuid references public.project_subfolders(id) on delete set null,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_documents_user_project
|
|
|
|
|
on public.documents(user_id, project_id);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_documents_project_folder
|
|
|
|
|
on public.documents(project_id, folder_id);
|
|
|
|
|
|
|
|
|
|
create table if not exists public.document_versions (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
document_id uuid not null references public.documents(id) on delete cascade,
|
2026-06-11 21:50:58 +08:00
|
|
|
storage_path text,
|
2026-05-08 20:45:16 +08:00
|
|
|
pdf_storage_path text,
|
|
|
|
|
source text not null default 'upload',
|
|
|
|
|
version_number integer,
|
2026-06-06 15:48:47 +08:00
|
|
|
filename text,
|
|
|
|
|
file_type text,
|
|
|
|
|
size_bytes integer,
|
|
|
|
|
page_count integer,
|
2026-06-11 21:50:58 +08:00
|
|
|
deleted_at timestamptz,
|
|
|
|
|
deleted_by uuid,
|
2026-05-08 20:45:16 +08:00
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
constraint document_versions_source_check
|
|
|
|
|
check (source = any (array[
|
|
|
|
|
'upload'::text,
|
|
|
|
|
'user_upload'::text,
|
|
|
|
|
'assistant_edit'::text,
|
|
|
|
|
'user_accept'::text,
|
|
|
|
|
'user_reject'::text,
|
|
|
|
|
'generated'::text
|
|
|
|
|
]))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists document_versions_document_id_idx
|
|
|
|
|
on public.document_versions(document_id, created_at desc);
|
|
|
|
|
|
2026-06-11 21:50:58 +08:00
|
|
|
create index if not exists document_versions_active_document_id_idx
|
|
|
|
|
on public.document_versions(document_id, created_at desc)
|
|
|
|
|
where deleted_at is null;
|
|
|
|
|
|
2026-05-08 20:45:16 +08:00
|
|
|
create index if not exists document_versions_doc_vnum_idx
|
|
|
|
|
on public.document_versions(document_id, version_number);
|
|
|
|
|
|
2026-06-09 01:46:58 +08:00
|
|
|
do $$
|
|
|
|
|
begin
|
|
|
|
|
if not exists (
|
|
|
|
|
select 1
|
|
|
|
|
from pg_constraint
|
|
|
|
|
where conname = 'document_versions_doc_version_unique'
|
|
|
|
|
and conrelid = 'public.document_versions'::regclass
|
|
|
|
|
) then
|
|
|
|
|
alter table public.document_versions
|
|
|
|
|
add constraint document_versions_doc_version_unique
|
|
|
|
|
unique (document_id, version_number);
|
|
|
|
|
end if;
|
|
|
|
|
end;
|
|
|
|
|
$$;
|
|
|
|
|
|
2026-05-08 20:45:16 +08:00
|
|
|
alter table public.documents
|
|
|
|
|
add column if not exists current_version_id uuid
|
|
|
|
|
references public.document_versions(id) on delete set null;
|
|
|
|
|
|
|
|
|
|
create table if not exists public.document_edits (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
document_id uuid not null references public.documents(id) on delete cascade,
|
|
|
|
|
chat_message_id uuid,
|
|
|
|
|
version_id uuid not null references public.document_versions(id) on delete cascade,
|
|
|
|
|
change_id text not null,
|
|
|
|
|
del_w_id text,
|
|
|
|
|
ins_w_id text,
|
|
|
|
|
deleted_text text not null default '',
|
|
|
|
|
inserted_text text not null default '',
|
|
|
|
|
context_before text,
|
|
|
|
|
context_after text,
|
|
|
|
|
status text not null default 'pending'
|
|
|
|
|
check (status = any (array[
|
|
|
|
|
'pending'::text,
|
|
|
|
|
'accepted'::text,
|
|
|
|
|
'rejected'::text
|
|
|
|
|
])),
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
resolved_at timestamptz
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists document_edits_document_id_idx
|
|
|
|
|
on public.document_edits(document_id, created_at desc);
|
|
|
|
|
|
|
|
|
|
create index if not exists document_edits_message_id_idx
|
|
|
|
|
on public.document_edits(chat_message_id);
|
|
|
|
|
|
|
|
|
|
create index if not exists document_edits_version_id_idx
|
|
|
|
|
on public.document_edits(version_id);
|
|
|
|
|
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
-- Workflows
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
create table if not exists public.workflows (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id text,
|
|
|
|
|
title text not null,
|
|
|
|
|
type text not null,
|
|
|
|
|
prompt_md text,
|
|
|
|
|
columns_config jsonb,
|
|
|
|
|
practice text,
|
|
|
|
|
is_system boolean not null default false,
|
|
|
|
|
created_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_workflows_user
|
|
|
|
|
on public.workflows(user_id);
|
|
|
|
|
|
|
|
|
|
create table if not exists public.hidden_workflows (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
user_id text not null,
|
|
|
|
|
workflow_id text not null,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
unique(user_id, workflow_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_hidden_workflows_user
|
|
|
|
|
on public.hidden_workflows(user_id);
|
|
|
|
|
|
|
|
|
|
create table if not exists public.workflow_shares (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
workflow_id uuid not null references public.workflows(id) on delete cascade,
|
|
|
|
|
shared_by_user_id text not null,
|
|
|
|
|
shared_with_email text not null,
|
|
|
|
|
allow_edit boolean not null default false,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
constraint workflow_shares_workflow_email_unique
|
|
|
|
|
unique(workflow_id, shared_with_email)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists workflow_shares_workflow_id_idx
|
|
|
|
|
on public.workflow_shares(workflow_id);
|
|
|
|
|
|
|
|
|
|
create index if not exists workflow_shares_email_idx
|
|
|
|
|
on public.workflow_shares(shared_with_email);
|
|
|
|
|
|
2026-06-15 17:34:58 +08:00
|
|
|
create or replace function public.get_workflows_overview(
|
|
|
|
|
p_user_id text,
|
|
|
|
|
p_user_email text default null,
|
|
|
|
|
p_type text default null
|
|
|
|
|
)
|
|
|
|
|
returns table (
|
|
|
|
|
id uuid,
|
|
|
|
|
user_id text,
|
|
|
|
|
title text,
|
|
|
|
|
type text,
|
|
|
|
|
prompt_md text,
|
|
|
|
|
columns_config jsonb,
|
|
|
|
|
practice text,
|
|
|
|
|
is_system boolean,
|
|
|
|
|
created_at timestamptz,
|
|
|
|
|
allow_edit boolean,
|
|
|
|
|
is_owner boolean,
|
|
|
|
|
shared_by_name text
|
|
|
|
|
)
|
|
|
|
|
language sql
|
|
|
|
|
stable
|
|
|
|
|
as $$
|
|
|
|
|
with owned as (
|
|
|
|
|
select
|
|
|
|
|
w.*,
|
|
|
|
|
true as allow_edit,
|
|
|
|
|
true as is_owner,
|
|
|
|
|
null::text as shared_by_name,
|
|
|
|
|
0 as sort_bucket
|
|
|
|
|
from public.workflows w
|
|
|
|
|
where w.user_id = p_user_id
|
|
|
|
|
and w.is_system = false
|
|
|
|
|
and (p_type is null or w.type = p_type)
|
|
|
|
|
),
|
|
|
|
|
shared as (
|
|
|
|
|
select
|
|
|
|
|
w.*,
|
|
|
|
|
ws.allow_edit,
|
|
|
|
|
false as is_owner,
|
|
|
|
|
nullif(trim(up.display_name), '') as shared_by_name,
|
|
|
|
|
1 as sort_bucket
|
|
|
|
|
from public.workflow_shares ws
|
|
|
|
|
join public.workflows w
|
|
|
|
|
on w.id = ws.workflow_id
|
|
|
|
|
left join public.user_profiles up
|
|
|
|
|
on up.user_id::text = ws.shared_by_user_id
|
|
|
|
|
where lower(ws.shared_with_email) = lower(coalesce(p_user_email, ''))
|
|
|
|
|
and (p_type is null or w.type = p_type)
|
|
|
|
|
),
|
|
|
|
|
visible_workflows as (
|
|
|
|
|
select * from owned
|
|
|
|
|
union all
|
|
|
|
|
select * from shared
|
|
|
|
|
)
|
|
|
|
|
select
|
|
|
|
|
vw.id,
|
|
|
|
|
vw.user_id,
|
|
|
|
|
vw.title,
|
|
|
|
|
vw.type,
|
|
|
|
|
vw.prompt_md,
|
|
|
|
|
vw.columns_config,
|
|
|
|
|
vw.practice,
|
|
|
|
|
vw.is_system,
|
|
|
|
|
vw.created_at,
|
|
|
|
|
vw.allow_edit,
|
|
|
|
|
vw.is_owner,
|
|
|
|
|
vw.shared_by_name
|
|
|
|
|
from visible_workflows vw
|
|
|
|
|
order by vw.sort_bucket asc, vw.created_at desc;
|
|
|
|
|
$$;
|
|
|
|
|
|
2026-05-08 20:45:16 +08:00
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
-- Assistant chats
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
create table if not exists public.chats (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
project_id uuid references public.projects(id) on delete cascade,
|
|
|
|
|
user_id text not null,
|
|
|
|
|
title text,
|
|
|
|
|
created_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_chats_user
|
|
|
|
|
on public.chats(user_id);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_chats_project
|
|
|
|
|
on public.chats(project_id);
|
|
|
|
|
|
2026-06-15 17:34:58 +08:00
|
|
|
create or replace function public.get_chats_overview(
|
|
|
|
|
p_user_id text,
|
|
|
|
|
p_limit integer default null
|
|
|
|
|
)
|
|
|
|
|
returns table (
|
|
|
|
|
id uuid,
|
|
|
|
|
project_id uuid,
|
|
|
|
|
user_id text,
|
|
|
|
|
title text,
|
|
|
|
|
created_at timestamptz
|
|
|
|
|
)
|
|
|
|
|
language sql
|
|
|
|
|
stable
|
|
|
|
|
as $$
|
|
|
|
|
select
|
|
|
|
|
c.id,
|
|
|
|
|
c.project_id,
|
|
|
|
|
c.user_id,
|
|
|
|
|
c.title,
|
|
|
|
|
c.created_at
|
|
|
|
|
from public.chats c
|
|
|
|
|
where c.user_id = p_user_id
|
|
|
|
|
or exists (
|
|
|
|
|
select 1
|
|
|
|
|
from public.projects p
|
|
|
|
|
where p.id = c.project_id
|
|
|
|
|
and p.user_id = p_user_id
|
|
|
|
|
)
|
|
|
|
|
order by c.created_at desc
|
|
|
|
|
limit case
|
|
|
|
|
when p_limit is null then null
|
|
|
|
|
else greatest(1, least(p_limit, 100))
|
|
|
|
|
end;
|
|
|
|
|
$$;
|
|
|
|
|
|
2026-05-08 20:45:16 +08:00
|
|
|
create table if not exists public.chat_messages (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
chat_id uuid not null references public.chats(id) on delete cascade,
|
|
|
|
|
role text not null,
|
|
|
|
|
content jsonb,
|
|
|
|
|
files jsonb,
|
|
|
|
|
annotations jsonb,
|
|
|
|
|
created_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_chat_messages_chat
|
|
|
|
|
on public.chat_messages(chat_id);
|
|
|
|
|
|
|
|
|
|
do $$
|
|
|
|
|
begin
|
|
|
|
|
if not exists (
|
|
|
|
|
select 1
|
|
|
|
|
from pg_constraint
|
|
|
|
|
where conname = 'document_edits_chat_message_id_fkey'
|
|
|
|
|
and conrelid = 'public.document_edits'::regclass
|
|
|
|
|
) then
|
|
|
|
|
alter table public.document_edits
|
|
|
|
|
add constraint document_edits_chat_message_id_fkey
|
|
|
|
|
foreign key (chat_message_id)
|
|
|
|
|
references public.chat_messages(id)
|
|
|
|
|
on delete set null;
|
|
|
|
|
end if;
|
|
|
|
|
end;
|
|
|
|
|
$$;
|
|
|
|
|
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
-- Tabular reviews
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
create table if not exists public.tabular_reviews (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
project_id uuid references public.projects(id) on delete cascade,
|
|
|
|
|
user_id text not null,
|
|
|
|
|
title text,
|
|
|
|
|
columns_config jsonb,
|
2026-05-18 00:21:40 +08:00
|
|
|
document_ids jsonb,
|
2026-05-08 20:45:16 +08:00
|
|
|
workflow_id uuid references public.workflows(id) on delete set null,
|
|
|
|
|
practice text,
|
|
|
|
|
shared_with jsonb not null default '[]'::jsonb,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_tabular_reviews_user
|
|
|
|
|
on public.tabular_reviews(user_id);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_tabular_reviews_project
|
|
|
|
|
on public.tabular_reviews(project_id);
|
|
|
|
|
|
|
|
|
|
create index if not exists tabular_reviews_shared_with_idx
|
|
|
|
|
on public.tabular_reviews using gin (shared_with);
|
|
|
|
|
|
2026-06-15 17:34:58 +08:00
|
|
|
create or replace function public.get_projects_overview(
|
|
|
|
|
p_user_id text,
|
|
|
|
|
p_user_email text default null
|
|
|
|
|
)
|
|
|
|
|
returns table (
|
|
|
|
|
id uuid,
|
|
|
|
|
user_id text,
|
|
|
|
|
name text,
|
|
|
|
|
cm_number text,
|
|
|
|
|
shared_with jsonb,
|
|
|
|
|
created_at timestamptz,
|
|
|
|
|
updated_at timestamptz,
|
|
|
|
|
is_owner boolean,
|
|
|
|
|
owner_display_name text,
|
|
|
|
|
owner_email text,
|
|
|
|
|
document_count integer,
|
|
|
|
|
chat_count integer,
|
|
|
|
|
review_count integer
|
|
|
|
|
)
|
|
|
|
|
language sql
|
|
|
|
|
stable
|
|
|
|
|
as $$
|
|
|
|
|
with visible_projects as (
|
|
|
|
|
select p.*
|
|
|
|
|
from public.projects p
|
|
|
|
|
where p.user_id = p_user_id
|
|
|
|
|
or (
|
|
|
|
|
coalesce(p_user_email, '') <> ''
|
|
|
|
|
and p.user_id <> p_user_id
|
|
|
|
|
and p.shared_with @> jsonb_build_array(p_user_email)
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
document_counts as (
|
|
|
|
|
select d.project_id, count(*)::integer as document_count
|
|
|
|
|
from public.documents d
|
|
|
|
|
where d.project_id in (select vp.id from visible_projects vp)
|
|
|
|
|
group by d.project_id
|
|
|
|
|
),
|
|
|
|
|
chat_counts as (
|
|
|
|
|
select c.project_id, count(*)::integer as chat_count
|
|
|
|
|
from public.chats c
|
|
|
|
|
where c.project_id in (select vp.id from visible_projects vp)
|
|
|
|
|
group by c.project_id
|
|
|
|
|
),
|
|
|
|
|
review_counts as (
|
|
|
|
|
select tr.project_id, count(*)::integer as review_count
|
|
|
|
|
from public.tabular_reviews tr
|
|
|
|
|
where tr.project_id in (select vp.id from visible_projects vp)
|
|
|
|
|
group by tr.project_id
|
|
|
|
|
)
|
|
|
|
|
select
|
|
|
|
|
vp.id,
|
|
|
|
|
vp.user_id,
|
|
|
|
|
vp.name,
|
|
|
|
|
vp.cm_number,
|
|
|
|
|
vp.shared_with,
|
|
|
|
|
vp.created_at,
|
|
|
|
|
vp.updated_at,
|
|
|
|
|
vp.user_id = p_user_id as is_owner,
|
|
|
|
|
nullif(trim(up.display_name), '') as owner_display_name,
|
|
|
|
|
null::text as owner_email,
|
|
|
|
|
coalesce(dc.document_count, 0) as document_count,
|
|
|
|
|
coalesce(cc.chat_count, 0) as chat_count,
|
|
|
|
|
coalesce(rc.review_count, 0) as review_count
|
|
|
|
|
from visible_projects vp
|
|
|
|
|
left join public.user_profiles up
|
|
|
|
|
on up.user_id::text = vp.user_id
|
|
|
|
|
left join document_counts dc
|
|
|
|
|
on dc.project_id = vp.id
|
|
|
|
|
left join chat_counts cc
|
|
|
|
|
on cc.project_id = vp.id
|
|
|
|
|
left join review_counts rc
|
|
|
|
|
on rc.project_id = vp.id
|
|
|
|
|
order by vp.created_at desc;
|
|
|
|
|
$$;
|
|
|
|
|
|
2026-05-08 20:45:16 +08:00
|
|
|
create table if not exists public.tabular_cells (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
review_id uuid not null references public.tabular_reviews(id) on delete cascade,
|
|
|
|
|
document_id uuid not null references public.documents(id) on delete cascade,
|
|
|
|
|
column_index integer not null,
|
|
|
|
|
content text,
|
|
|
|
|
citations jsonb,
|
|
|
|
|
status text not null default 'pending',
|
|
|
|
|
created_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists idx_tabular_cells_review
|
|
|
|
|
on public.tabular_cells(review_id, document_id, column_index);
|
|
|
|
|
|
2026-06-15 17:34:58 +08:00
|
|
|
create or replace function public.get_tabular_reviews_overview(
|
|
|
|
|
p_user_id text,
|
|
|
|
|
p_user_email text default null,
|
|
|
|
|
p_project_id text default null
|
|
|
|
|
)
|
|
|
|
|
returns table (
|
|
|
|
|
id uuid,
|
|
|
|
|
project_id uuid,
|
|
|
|
|
user_id text,
|
|
|
|
|
title text,
|
|
|
|
|
columns_config jsonb,
|
|
|
|
|
document_ids jsonb,
|
|
|
|
|
workflow_id uuid,
|
|
|
|
|
shared_with jsonb,
|
|
|
|
|
created_at timestamptz,
|
|
|
|
|
updated_at timestamptz,
|
|
|
|
|
is_owner boolean,
|
|
|
|
|
document_count integer
|
|
|
|
|
)
|
|
|
|
|
language sql
|
|
|
|
|
stable
|
|
|
|
|
as $$
|
|
|
|
|
with accessible_projects as (
|
|
|
|
|
select p.id
|
|
|
|
|
from public.projects p
|
|
|
|
|
where p.user_id = p_user_id
|
|
|
|
|
or (
|
|
|
|
|
coalesce(p_user_email, '') <> ''
|
|
|
|
|
and p.user_id <> p_user_id
|
|
|
|
|
and p.shared_with @> jsonb_build_array(p_user_email)
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
visible_reviews as (
|
|
|
|
|
select tr.*
|
|
|
|
|
from public.tabular_reviews tr
|
|
|
|
|
where (p_project_id is null or tr.project_id::text = p_project_id)
|
|
|
|
|
and (
|
|
|
|
|
p_project_id is null
|
|
|
|
|
or exists (
|
|
|
|
|
select 1
|
|
|
|
|
from accessible_projects ap
|
|
|
|
|
where ap.id::text = p_project_id
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
and (
|
|
|
|
|
tr.user_id = p_user_id
|
|
|
|
|
or (
|
|
|
|
|
tr.project_id in (select ap.id from accessible_projects ap)
|
|
|
|
|
and tr.user_id <> p_user_id
|
|
|
|
|
)
|
|
|
|
|
or (
|
|
|
|
|
p_project_id is null
|
|
|
|
|
and coalesce(p_user_email, '') <> ''
|
|
|
|
|
and tr.user_id <> p_user_id
|
|
|
|
|
and tr.shared_with @> jsonb_build_array(p_user_email)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
cell_document_counts as (
|
|
|
|
|
select
|
|
|
|
|
tc.review_id,
|
|
|
|
|
count(distinct tc.document_id)::integer as document_count
|
|
|
|
|
from public.tabular_cells tc
|
|
|
|
|
where tc.review_id in (select vr.id from visible_reviews vr)
|
|
|
|
|
group by tc.review_id
|
|
|
|
|
)
|
|
|
|
|
select
|
|
|
|
|
vr.id,
|
|
|
|
|
vr.project_id,
|
|
|
|
|
vr.user_id,
|
|
|
|
|
vr.title,
|
|
|
|
|
vr.columns_config,
|
|
|
|
|
vr.document_ids,
|
|
|
|
|
vr.workflow_id,
|
|
|
|
|
vr.shared_with,
|
|
|
|
|
vr.created_at,
|
|
|
|
|
vr.updated_at,
|
|
|
|
|
vr.user_id = p_user_id as is_owner,
|
|
|
|
|
case
|
|
|
|
|
when jsonb_typeof(vr.document_ids) = 'array'
|
|
|
|
|
then (
|
|
|
|
|
select count(distinct doc_id.value)::integer
|
|
|
|
|
from jsonb_array_elements_text(vr.document_ids) as doc_id(value)
|
|
|
|
|
)
|
|
|
|
|
else coalesce(cdc.document_count, 0)
|
|
|
|
|
end as document_count
|
|
|
|
|
from visible_reviews vr
|
|
|
|
|
left join cell_document_counts cdc
|
|
|
|
|
on cdc.review_id = vr.id
|
|
|
|
|
order by vr.created_at desc;
|
|
|
|
|
$$;
|
|
|
|
|
|
2026-05-08 20:45:16 +08:00
|
|
|
create table if not exists public.tabular_review_chats (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
review_id uuid not null references public.tabular_reviews(id) on delete cascade,
|
|
|
|
|
user_id text not null,
|
|
|
|
|
title text,
|
|
|
|
|
created_at timestamptz not null default now(),
|
|
|
|
|
updated_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists tabular_review_chats_review_idx
|
|
|
|
|
on public.tabular_review_chats(review_id, updated_at desc);
|
|
|
|
|
|
|
|
|
|
create index if not exists tabular_review_chats_user_idx
|
|
|
|
|
on public.tabular_review_chats(user_id);
|
|
|
|
|
|
|
|
|
|
create table if not exists public.tabular_review_chat_messages (
|
|
|
|
|
id uuid primary key default gen_random_uuid(),
|
|
|
|
|
chat_id uuid not null references public.tabular_review_chats(id) on delete cascade,
|
|
|
|
|
role text not null,
|
|
|
|
|
content jsonb,
|
|
|
|
|
annotations jsonb,
|
|
|
|
|
created_at timestamptz not null default now()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists tabular_review_chat_messages_chat_idx
|
|
|
|
|
on public.tabular_review_chat_messages(chat_id, created_at);
|
|
|
|
|
|
2026-06-06 15:48:47 +08:00
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
-- CourtListener bulk-data indexes
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
create table if not exists public.courtlistener_citation_index (
|
|
|
|
|
id bigint primary key,
|
|
|
|
|
volume text not null,
|
|
|
|
|
reporter text not null,
|
|
|
|
|
page text not null,
|
|
|
|
|
type integer,
|
|
|
|
|
cluster_id bigint not null,
|
|
|
|
|
date_created timestamptz,
|
|
|
|
|
date_modified timestamptz
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
create index if not exists courtlistener_citation_lookup_idx
|
|
|
|
|
on public.courtlistener_citation_index(volume, reporter, page);
|
|
|
|
|
|
|
|
|
|
create index if not exists courtlistener_citation_cluster_idx
|
|
|
|
|
on public.courtlistener_citation_index(cluster_id);
|
|
|
|
|
|
2026-06-09 01:46:58 +08:00
|
|
|
alter table public.courtlistener_citation_index enable row level security;
|
|
|
|
|
|
2026-06-06 15:48:47 +08:00
|
|
|
create table if not exists public.courtlistener_opinion_cluster_index (
|
|
|
|
|
id bigint primary key,
|
|
|
|
|
case_name text,
|
|
|
|
|
case_name_short text,
|
|
|
|
|
case_name_full text,
|
|
|
|
|
slug text,
|
|
|
|
|
date_filed date,
|
|
|
|
|
citation_count integer,
|
|
|
|
|
precedential_status text,
|
|
|
|
|
filepath_pdf_harvard text,
|
|
|
|
|
filepath_json_harvard text,
|
|
|
|
|
docket_id bigint
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-09 01:46:58 +08:00
|
|
|
alter table public.courtlistener_opinion_cluster_index enable row level security;
|
|
|
|
|
|
2026-05-09 14:55:51 +08:00
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
-- Direct client grant hardening
|
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
|
--
|
|
|
|
|
-- The frontend uses Supabase directly only for authentication. Application
|
|
|
|
|
-- data access goes through the backend API with the service role after the
|
2026-05-13 02:32:26 +08:00
|
|
|
-- backend verifies the user's JWT. Do not grant the browser anon/authenticated
|
2026-05-09 14:55:51 +08:00
|
|
|
-- roles direct table privileges for backend-owned data.
|
|
|
|
|
|
|
|
|
|
revoke all on public.user_profiles from anon, authenticated;
|
|
|
|
|
revoke all on public.projects from anon, authenticated;
|
|
|
|
|
revoke all on public.project_subfolders from anon, authenticated;
|
|
|
|
|
revoke all on public.documents from anon, authenticated;
|
|
|
|
|
revoke all on public.document_versions from anon, authenticated;
|
|
|
|
|
revoke all on public.document_edits from anon, authenticated;
|
|
|
|
|
revoke all on public.workflows from anon, authenticated;
|
|
|
|
|
revoke all on public.hidden_workflows from anon, authenticated;
|
|
|
|
|
revoke all on public.workflow_shares from anon, authenticated;
|
|
|
|
|
revoke all on public.chats from anon, authenticated;
|
|
|
|
|
revoke all on public.chat_messages from anon, authenticated;
|
|
|
|
|
revoke all on public.tabular_reviews from anon, authenticated;
|
|
|
|
|
revoke all on public.tabular_cells from anon, authenticated;
|
|
|
|
|
revoke all on public.tabular_review_chats from anon, authenticated;
|
|
|
|
|
revoke all on public.tabular_review_chat_messages from anon, authenticated;
|
|
|
|
|
revoke all on public.user_api_keys from anon, authenticated;
|
2026-06-15 17:34:58 +08:00
|
|
|
revoke all on public.user_mcp_connectors from anon, authenticated;
|
|
|
|
|
revoke all on public.user_mcp_oauth_tokens from anon, authenticated;
|
|
|
|
|
revoke all on public.user_mcp_oauth_states from anon, authenticated;
|
|
|
|
|
revoke all on public.user_mcp_connector_tools from anon, authenticated;
|
|
|
|
|
revoke all on public.user_mcp_tool_audit_logs from anon, authenticated;
|
2026-06-06 15:48:47 +08:00
|
|
|
revoke all on public.courtlistener_citation_index from anon, authenticated;
|
|
|
|
|
revoke all on public.courtlistener_opinion_cluster_index from anon, authenticated;
|