232 lines
8.4 KiB
Markdown
232 lines
8.4 KiB
Markdown
# Database Schema
|
|
## SOP Governance Tool — Stage 1 (Supabase Postgres)
|
|
|
|
Run order: `schema.sql` → `policies.sql` → `seed.sql`. All tables have `org_id` (AD-1). Timestamps are `timestamptz default now()`.
|
|
|
|
## 1. Entity Overview
|
|
|
|
```
|
|
orgs ─┬─ departments ─┬─ profiles (← auth.users)
|
|
│ └─ sop_assignments
|
|
├─ sops ─┬─ sop_versions ─┬─ acknowledgements
|
|
│ │ └─ sop_translations
|
|
│ └─ approvals
|
|
├─ incidents (→ sops, sop_versions optional)
|
|
├─ audit_log
|
|
└─ ai_log
|
|
```
|
|
|
|
## 2. Tables (DDL)
|
|
|
|
```sql
|
|
create type user_role as enum ('admin','approver','editor','staff');
|
|
create type sop_status as enum ('draft','submitted','approved','published','archived');
|
|
create type lang_code as enum ('en','ms','zh');
|
|
|
|
create table orgs (
|
|
id uuid primary key default gen_random_uuid(),
|
|
name text not null,
|
|
created_at timestamptz default now()
|
|
);
|
|
|
|
create table departments (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
name text not null,
|
|
unique (org_id, name)
|
|
);
|
|
|
|
create table profiles (
|
|
id uuid primary key references auth.users(id) on delete cascade,
|
|
org_id uuid not null references orgs(id),
|
|
full_name text not null,
|
|
role user_role not null default 'staff',
|
|
department_id uuid references departments(id),
|
|
preferred_language lang_code not null default 'en',
|
|
active boolean not null default true,
|
|
created_at timestamptz default now()
|
|
);
|
|
|
|
create table sops (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
code text not null, -- e.g. WH-PICK-001
|
|
title text not null,
|
|
department_id uuid references departments(id),
|
|
category text,
|
|
owner_id uuid references profiles(id),
|
|
status sop_status not null default 'draft',
|
|
review_months int not null default 12 check (review_months in (3,6,12,24)),
|
|
draft_content jsonb not null default '{}'::jsonb, -- working copy (sections, steps)
|
|
current_version_id uuid, -- FK added after sop_versions exists
|
|
published_at timestamptz,
|
|
created_by uuid references profiles(id),
|
|
created_at timestamptz default now(),
|
|
updated_at timestamptz default now(),
|
|
unique (org_id, code)
|
|
);
|
|
|
|
create table sop_versions (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
sop_id uuid not null references sops(id) on delete cascade,
|
|
version_label text not null, -- '1.0', '1.1', '2.0'
|
|
content jsonb not null, -- FROZEN snapshot of sections at publish
|
|
change_note text,
|
|
is_major boolean not null default false,
|
|
published_by uuid references profiles(id),
|
|
published_at timestamptz default now(),
|
|
unique (sop_id, version_label)
|
|
);
|
|
|
|
alter table sops
|
|
add constraint fk_current_version
|
|
foreign key (current_version_id) references sop_versions(id);
|
|
|
|
create table approvals (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
sop_id uuid not null references sops(id) on delete cascade,
|
|
decision text not null check (decision in ('approved','rejected')),
|
|
comment text,
|
|
decided_by uuid not null references profiles(id),
|
|
decided_at timestamptz default now()
|
|
);
|
|
|
|
create table sop_assignments (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
sop_id uuid not null references sops(id) on delete cascade,
|
|
department_id uuid not null references departments(id),
|
|
assigned_by uuid references profiles(id),
|
|
assigned_at timestamptz default now(),
|
|
unique (sop_id, department_id)
|
|
);
|
|
|
|
create table acknowledgements (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
sop_version_id uuid not null references sop_versions(id),
|
|
user_id uuid not null references profiles(id),
|
|
language_viewed lang_code not null,
|
|
typed_name text not null, -- the "signature"
|
|
acknowledged_at timestamptz default now(),
|
|
unique (sop_version_id, user_id) -- one ack per user per version
|
|
);
|
|
|
|
create table sop_translations (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
sop_version_id uuid not null references sop_versions(id) on delete cascade,
|
|
language lang_code not null,
|
|
content jsonb not null,
|
|
machine boolean not null default true,
|
|
reviewed_by uuid references profiles(id),
|
|
reviewed_at timestamptz,
|
|
created_at timestamptz default now(),
|
|
unique (sop_version_id, language)
|
|
);
|
|
|
|
create table audit_log (
|
|
id bigint generated always as identity primary key,
|
|
org_id uuid not null references orgs(id),
|
|
actor_id uuid references profiles(id),
|
|
action text not null, -- 'sop.published', 'ack.recorded', ...
|
|
entity_type text not null, -- 'sop','user','acknowledgement'
|
|
entity_id uuid,
|
|
detail jsonb,
|
|
created_at timestamptz default now()
|
|
);
|
|
-- Append-only: grant INSERT/SELECT; never UPDATE/DELETE (enforced by grants + no policy).
|
|
|
|
create table ai_log (
|
|
id bigint generated always as identity primary key,
|
|
org_id uuid not null references orgs(id),
|
|
user_id uuid references profiles(id),
|
|
kind text not null check (kind in ('draft','translate')),
|
|
input_chars int, output_chars int,
|
|
input_tokens int, output_tokens int,
|
|
model text, success boolean, error text,
|
|
created_at timestamptz default now()
|
|
);
|
|
|
|
create type incident_severity as enum ('low','medium','high');
|
|
create type incident_status as enum ('open','reviewed','closed');
|
|
|
|
create table incidents (
|
|
id uuid primary key default gen_random_uuid(),
|
|
org_id uuid not null references orgs(id),
|
|
reporter_id uuid not null references profiles(id),
|
|
department_id uuid references departments(id), -- copied from reporter at insert
|
|
sop_id uuid references sops(id), -- optional link
|
|
sop_version_id uuid references sop_versions(id), -- optional, set when reported from viewer
|
|
description text not null,
|
|
severity incident_severity not null default 'medium',
|
|
photo_path text, -- Storage path, optional
|
|
status incident_status not null default 'open',
|
|
resolution_note text,
|
|
reviewed_by uuid references profiles(id),
|
|
closed_at timestamptz,
|
|
created_at timestamptz default now()
|
|
);
|
|
|
|
create index idx_incidents_status on incidents(org_id, status, severity);
|
|
create index idx_incidents_sop on incidents(sop_id);
|
|
|
|
create index idx_sops_org_status on sops(org_id, status);
|
|
create index idx_ack_version on acknowledgements(sop_version_id);
|
|
create index idx_audit_entity on audit_log(entity_type, entity_id);
|
|
```
|
|
|
|
### draft_content / content JSON shape
|
|
```json
|
|
{
|
|
"purpose": "…",
|
|
"scope": "…",
|
|
"roles": "…",
|
|
"steps": [ { "order": 1, "text": "…", "photo_path": "sop-photos/…" } ],
|
|
"exceptions": "…",
|
|
"safety": "…",
|
|
"records": "…"
|
|
}
|
|
```
|
|
|
|
## 3. RLS Policy Sketch (`policies.sql`)
|
|
|
|
Enable RLS on every table. Helper: JWT carries `org_id` and `role` claims (custom access token hook reading `profiles`).
|
|
|
|
| Table | staff | editor | approver | admin |
|
|
|---|---|---|---|---|
|
|
| sops (read) | published + assigned to their dept only | all in org | all in org | all |
|
|
| sops (write) | — | insert/update drafts | — | all |
|
|
| sop_versions | read if assigned | read | read | all |
|
|
| acknowledgements | insert own; read own | read | read | read |
|
|
| profiles | read own + names in org | read org | read org | all |
|
|
| audit_log | — | — | read | read |
|
|
| approvals | — | read | insert/read | all |
|
|
| translations | read | read/write | read | all |
|
|
| incidents | insert own; read own | read org | read/update org | all |
|
|
|
|
Representative policy:
|
|
```sql
|
|
alter table acknowledgements enable row level security;
|
|
|
|
create policy ack_insert_own on acknowledgements
|
|
for insert with check (
|
|
user_id = auth.uid()
|
|
and org_id = (auth.jwt() ->> 'org_id')::uuid
|
|
);
|
|
|
|
create policy ack_read on acknowledgements
|
|
for select using (
|
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
|
and ( user_id = auth.uid()
|
|
or (auth.jwt() ->> 'role') in ('admin','approver','editor') )
|
|
);
|
|
```
|
|
|
|
Workflow mutations (publish, approve, assign) run via the **service-role client inside /api routes** so drafts can be snapshotted and audit rows written in one transaction; RLS remains the safety net for direct reads.
|
|
|
|
## 4. Seed (`seed.sql`)
|
|
One org ("<Your Company>"), 3 departments (Inbound, Outbound, Admin), the founder as admin, 2 test staff, 2 sample SOPs (one draft, one published v1.0 with BM translation), a few acknowledgements — enough for the dashboard to show real numbers on day one.
|