-- Signal Phase 2 Schema -- Run this in the Supabase SQL editor (Dashboard > SQL Editor > New query) -- Safe to run multiple times — uses IF NOT EXISTS throughout create extension if not exists "uuid-ossp"; -- Organizations (DME supplier accounts) create table if not exists organizations ( id uuid primary key default uuid_generate_v4(), name text not null, slug text unique not null, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- Users (staff at each org) create table if not exists users ( id uuid primary key default uuid_generate_v4(), org_id uuid not null references organizations(id) on delete cascade, email text not null unique, role text not null default 'staff', -- 'admin' | 'staff' created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- Upload batches (one per CSV upload event) create table if not exists upload_batches ( id uuid primary key default uuid_generate_v4(), org_id uuid not null references organizations(id) on delete cascade, uploaded_by uuid references users(id), filename text not null, row_count int not null default 0, skipped_count int not null default 0, status text not null default 'processing', -- 'processing' | 'complete' | 'failed' created_at timestamptz not null default now() ); -- Source files (raw CSV metadata, WORM) create table if not exists source_files ( id uuid primary key default uuid_generate_v4(), batch_id uuid not null references upload_batches(id) on delete cascade, filename text not null, content_hash text not null, -- SHA-256 of file bytes byte_size int not null, created_at timestamptz not null default now() ); -- Raw rows (original CSV row data before normalization, WORM) create table if not exists raw_rows ( id uuid primary key default uuid_generate_v4(), batch_id uuid not null references upload_batches(id) on delete cascade, row_number int not null, raw_data jsonb not null, -- original column key/value pairs created_at timestamptz not null default now() ); -- Normalized records (scored output) create table if not exists normalized_records ( id uuid primary key default uuid_generate_v4(), batch_id uuid not null references upload_batches(id) on delete cascade, patient_id_hash text not null, -- SHA-256 of patient_id — no raw PHI stored device_type text not null, shipment_date date not null, quantity int not null default 1, payer text not null, component text not null default 'sensor', coverage_status text not null, -- OUT_OF_COVERAGE | VISIT_DUE | REFILL_WINDOW | OK days_remaining int, reason text, recommended_action text, rule_version text not null, created_at timestamptz not null default now() ); -- Mapping decisions (header-to-field mapping log per batch) create table if not exists mapping_decisions ( id uuid primary key default uuid_generate_v4(), batch_id uuid not null references upload_batches(id) on delete cascade, raw_header text not null, canonical_field text, confidence text not null, -- 'high' | 'inferred' | 'unmapped' created_at timestamptz not null default now() ); -- Report runs (one per scored batch delivered to user) create table if not exists report_runs ( id uuid primary key default uuid_generate_v4(), batch_id uuid not null references upload_batches(id) on delete cascade, org_id uuid not null references organizations(id) on delete cascade, generated_by uuid references users(id), status text not null default 'complete', total_records int not null default 0, flagged_count int not null default 0, created_at timestamptz not null default now() ); -- Report items (one row per patient record in the worklist) create table if not exists report_items ( id uuid primary key default uuid_generate_v4(), report_run_id uuid not null references report_runs(id) on delete cascade, normalized_record_id uuid not null references normalized_records(id), patient_id_hash text not null, status text not null, days_remaining int, reason text, recommended_action text, created_at timestamptz not null default now() ); -- Export files (downloaded work queue CSVs) create table if not exists export_files ( id uuid primary key default uuid_generate_v4(), report_run_id uuid not null references report_runs(id) on delete cascade, exported_by uuid references users(id), filename text not null, row_count int not null default 0, created_at timestamptz not null default now() ); -- Audit events (WORM — append only, never update or delete) create table if not exists audit_events ( id uuid primary key default uuid_generate_v4(), org_id uuid references organizations(id), user_id uuid references users(id), action text not null, -- 'upload' | 'export' | 'login' | 'view_report' resource_type text, resource_id uuid, patient_id_hash text, metadata jsonb, ip_address text, created_at timestamptz not null default now() ); -- Enable Row Level Security on all tables alter table organizations enable row level security; alter table users enable row level security; alter table upload_batches enable row level security; alter table source_files enable row level security; alter table raw_rows enable row level security; alter table normalized_records enable row level security; alter table mapping_decisions enable row level security; alter table report_runs enable row level security; alter table report_items enable row level security; alter table export_files enable row level security; alter table audit_events enable row level security; -- WORM rules: audit_events, raw_rows, source_files are append-only do $$ begin if not exists (select 1 from pg_rules where rulename = 'audit_events_no_update') then create rule audit_events_no_update as on update to audit_events do instead nothing; end if; if not exists (select 1 from pg_rules where rulename = 'audit_events_no_delete') then create rule audit_events_no_delete as on delete to audit_events do instead nothing; end if; if not exists (select 1 from pg_rules where rulename = 'raw_rows_no_update') then create rule raw_rows_no_update as on update to raw_rows do instead nothing; end if; if not exists (select 1 from pg_rules where rulename = 'raw_rows_no_delete') then create rule raw_rows_no_delete as on delete to raw_rows do instead nothing; end if; if not exists (select 1 from pg_rules where rulename = 'source_files_no_update') then create rule source_files_no_update as on update to source_files do instead nothing; end if; if not exists (select 1 from pg_rules where rulename = 'source_files_no_delete') then create rule source_files_no_delete as on delete to source_files do instead nothing; end if; end $$; -- Indexes create index if not exists idx_upload_batches_org on upload_batches(org_id); create index if not exists idx_normalized_records_batch on normalized_records(batch_id); create index if not exists idx_normalized_records_status on normalized_records(coverage_status); create index if not exists idx_audit_events_org on audit_events(org_id); create index if not exists idx_audit_events_action on audit_events(action); create index if not exists idx_report_items_run on report_items(report_run_id);