Add spec pack
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user