Implementation Package v2.0

Texas Executive
Clinical Systems

Revised schema, credential verification strategy, and security compliance refactor for production-grade clinical staffing governance.

Supabase / PostgreSQLHIPAA AlignedNursys e-NotifyRow-Level Security

Executive Summary

This package provides a revised TECS database design and implementation plan that incorporates the previously recommended fixes. The revised design replaces fragile facility access logic with explicit facility membership and role-based row-level security, adds credential verification evidence records, stores clinical incident narratives using an encryption-ready ciphertext pattern, introduces an append-only audit log, and creates an addendum-based incident correction workflow.

The recommended operational decision is to implement real-time credential verification as a deterministic source-based workflow, not as an AI-first compliance workflow. Nursys e-Notify should be treated as the primary automated monitoring source for license and discipline notifications, while the Texas Board of Nursing portal should remain the Texas-specific official fallback.

Revised Schema

Full PostgreSQL migration with tables, triggers, RLS, and hard-stop procedures

Credential Strategy

Nursys e-Notify integration with Texas BON fallback and deterministic rules

Security Refactor

RLS, encryption, audit logging, and incident immutability controls

Research Findings

Supporting source links and evidence from official regulatory bodies

§ 2.0 Database Architecture

Revised Schema Overview

The revised schema is organized around four major control domains: identity and tenancy, credential verification, clinical scheduling, and legal/audit records. This separation ensures that access decisions, credential evidence, shift impacts, and audit proof are structurally independent.

DomainKey TablesPrimary Improvement
Identity & Tenancyorganizations, facilities, user_profiles, facility_membershipsReplaces facility_id = auth.uid() with realistic membership and role model
Nurse Credentialingnurses, facility_nurses, credential_documents, credential_verificationsPreserves license source evidence, uploaded docs, review history, and deterministic compliance outcomes
SchedulingshiftsSupports future shift release through controlled hard-stop procedure
Incident & Legalincident_reports, incident_addendaEncrypted narrative content with addendum-only correction workflow
Auditaudit_eventsAppend-only business audit records for credential scans, hard stops, shift releases, and blocked mutations

Entity-Relationship Diagram

Interactive · Mermaid.js

Use zoom controls or scroll wheel to zoom. Drag to pan across the full entity-relationship diagram.

Loading diagram...

Full Migration Script Available

887 lines of production-ready SQL including all tables, triggers, RLS policies, and the hard-stop procedure.

§ 3.0 Access Control

Row-Level Security Refactor

The revised schema uses facility_memberships to map authenticated users to facilities and roles. This allows one person to access multiple facilities, permits different access levels, and enables proper tenant isolation.

Role Matrix

RoleCapabilities
facility_adminManage facility settings, users, nurse assignments, shifts, incidents, and audit review
schedulerCreate and manage shifts but not alter credential determinations or audit records
compliance_officerReview credentials, manage nurse status, apply or review hard stops, review incidents and audit
clinical_reviewerCreate incident reports and addenda, review permitted clinical records
read_only_auditorRead audit and incident records without operational write access

Helper Functions: tecs.is_facility_member(facility_id) · tecs.has_facility_role(facility_id, roles[]) · tecs.can_access_nurse(nurse_id) · tecs.is_platform_admin()

§ 4.0 Data Protection

Encryption and Incident Records

The revised schema does not store clinical incident narratives as plaintext. Instead, it stores ciphertext with SHA-256 hashes, encryption key references, and algorithm identifiers. Incident reports are protected by database triggers that block deletion, narrative mutation, and unauthorized legal-hold removal.

Sensitive ItemStorage Pattern
Incident narrativeApplication-encrypted ciphertext in incident_reports
Incident correctionApplication-encrypted ciphertext in incident_addenda
License verification payloadParsed fields plus encrypted raw payload or hash
BLS/TB credential documentPrivate object storage path plus file hash and verification metadata
API credentialsServer secrets or Supabase Vault, never client-exposed

Immutability Controls

Block deletion of incident reports
Block narrative field mutation
Block unauthorized legal-hold removal
Corrections via addenda only

§ 5.0 Verification

Real-Time Credential Verification

TECS implements real-time credential verification through a protected backend service. The primary automated source is Nursys e-Notify with the Texas Board of Nursing as the official fallback for manual review and audit evidence.

Authoritative Sources

SourcePositionRationale
Nursys e-Notify APIPrimary automated sourceAutomated license and discipline notification with institution API for JSON reports
Texas BON PortalOfficial Texas fallbackOfficial license verification page for manual review and audit evidence
Uploaded DocumentsSupplemental sourceBLS, TB, and onboarding evidence requiring expiration tracking
Facility AttestationEmergency fallback onlyTime-limited and audited continuity during source outages

Verification Process Flow

01

Nurse Selection

Query nurses due for verification and those assigned to future shifts

02

Source Call

Retrieve Nursys license report or change notification data

03

Rule Evaluation

Apply deterministic status rules — result, license_status, risk_score, reason

04

Hard Stop

Call tecs.apply_credential_hard_stop() when required — nurse moves to red

05

Human Review

Route ambiguous or unavailable-source cases to compliance queue

Hard Stop Triggers

  • Suspended or expired license detected
  • Encumbered or not-found license status
  • Required facility credentials (BLS/TB) expired

Manual Review Triggers

  • Source system unavailable
  • Identity match is ambiguous
  • Response lacks expected fields

§ 6.0 Mobile Mechanics

GPS Geofencing, Document Vault & Signup

The mobile extension adds server-verified GPS geofencing for clock-in/out, a credential document vault with review workflow, and a structured nurse self-service signup pipeline. All mobile events are validated server-side and logged immutably.

GPS Geofenced Clock-In

01

Device submits GPS coordinates

Latitude, longitude, accuracy, and device metadata sent via RPC

02

Server calculates Haversine distance

PostgreSQL function computes great-circle distance to nearest facility geofence

03

Geofence radius enforced

Configurable 50m–2000m per facility; failed attempts logged with distance

04

Clock event recorded immutably

Triggers block UPDATE/DELETE; audit event created on success

05

Shift locked

Shift status transitions to 'locked' preventing reassignment

Credential Document Vault

  • Camera/file upload with SHA-256 integrity hash
  • OCR extraction with confidence scoring
  • Review workflow: uploaded → pending → verified/rejected
  • Expiration monitoring with indexed queries
  • Supabase Storage integration with private buckets

Nurse Self-Service Signup

  • Multi-step: profile → license verify → docs → review
  • Automated Nursys license query on submission
  • Document checklist enforcement (license, BLS, TB, ID)
  • Compliance officer approval gate
  • Duplicate prevention via composite unique constraint

Mobile Extension Schema

Geofences, clock events, document vault, and signup request tables with RLS and immutability triggers.

Download Mobile SQL

§ 7.0 UI Architecture

Multi-Portal Application Design

TECS operates through two distinct frontend environments: a mobile clinician portal for field nurses and a responsive web dashboard for administrators and facility partners. Each portal enforces role-based routing tied to the user_role enum.

Field Clinician Portal (Mobile App)

iOS & Android native app targeting users with associated_role == 'field_nurse'

Digital Sign-Up & License Onboarding

  • • Legal Full Name
  • • Texas Nursing License Number
  • • BLS Expiration Date
  • • TB Expiration Date

Triggers executeNurseProfileSignup validation

Specialty Matching Shift Feed

  • • Facility Name
  • • Unit Type (ICU/ER/Med-Surg)
  • • Hourly Pay Rate
  • • Shift Window

Filtered by nurse's verified skill set

Geofenced Clock-In Console

  • • Active Shift Timer
  • • Distance to Facility Meter
  • • 200m proximity interlock

Fires executeGeofencedClockIn on button press

Credential Vault & Pay Stubs

  • • Camera-based cert capture
  • • Pay Stub PDFs (Gusto API)
  • • Document expiration alerts

Native phone camera integration

Executive Enterprise Control Center (Web)

Responsive web application with conditional routing paths based on associated_role access flags.

Admin Command Center

Role: tecs_admin

  • Live Global Compliance Heat Map (Green/Yellow/Red)
  • One-Click PDF Audit Package Generator
  • Gross Revenue Monitor (Billing vs. W-2 Outlays)

Client/Partner Dashboard

Role: facility_partner

  • Shift Broadcasting Matrix (Post ICU/ER gaps)
  • Live Roster Verification Window
  • Timesheet Ledger Approval Board (Gusto sync)

Live Dashboard Prototype

Interactive demo of the TECS portal routing model. Switch between Admin and Partner roles to see how each environment renders based on user_profiles.associated_role.

Active Role:

Texas Executive Clinical Systems

Real-time systemic operational oversight map

Audit-Ready

Fully Compliant

2

Yellow Zone

0

Hard-Stopped

1

DFW Active Clinician Roster

ClinicianLicenseStatusHealthActions
Sarah Jenkins, RNTX-88392Active● Audit-Ready
Michael Chang, NPTX-94401Active● Audit-Ready
Amanda Ross, RNTX-72210Under_Review● Hard-Stopped

§ 8.0 Revenue Model

Monetization Calculator

Interactive profit ledger demonstrating how bill_rate, pay_rate, and vms_admin_percentage flow through the TECS financial engine.

Rate Configuration

$85.00
$55.00
3.0%

Volume Parameters

Profit Ledger Output

Per-Hour Breakdown

Gross Profit (bill − pay)$30.00
VMS Admin Fee (3%)−$2.55
Net Profit / Hour$27.45
Margin %35.3%

Monthly Projections

Shift Spread Revenue$43,200
SaaS Platform Fees (8 facilities)$20,000
VMS Admin Deductions−$3,672
Total Monthly Net$59,528

Annual Projection

1440 shifts × 12h + SaaS

$714,336

calculated_gross_profit = bill_rate − pay_rate
vms_fee = bill_rate × (vms_admin_percentage / 100)
monthly_net = (gross_profit × hours × shifts) + (saas_fee × facilities) − (vms_fee × hours × shifts)

§ 7.0 Original vs. Revised

Side-by-Side Comparison

Key architectural differences between the original TECS prototype (4 tables, FlutterFlow Dart, Claude-based compliance) and the revised production system (15+ tables, server-side verification, deterministic rules).

Database Architecture

AspectOriginalRevised
Tables4 (nurses, facilities, shifts, incident_reports)15+ tables across 4 control domains
Schemapublic (default)tecs (dedicated namespace)
EnumsNone (free-text columns)12 typed enums for all status fields
Nurse namename TEXTlegal_name + preferred_name
License trackinglicense_number TEXT UNIQUElicense_number + jurisdiction + type + ncsbn_id
Shift statusis_locked BOOLEAN6-state enum with constraints
Incident narrativeTEXT (plaintext)BYTEA ciphertext + SHA-256 + key ref

Row-Level Security

Original (Broken)

facility_id = auth.uid()
// Assumes user IS the facility

Revised (Production)

tecs.has_facility_role(facility_id, roles[])
// Membership + role-based

Mobile Mechanics

FeatureOriginal (FlutterFlow)Revised (Server-Side)
Geofence configHardcoded 200m, GPS in facilities tableConfigurable 50–2000m via facility_geofences
Distance calcClient-side Dart (spoofable)Server-side PostgreSQL Haversine
Failed clock-inError string, no loggingLogged to clock_events with distance
Clock event immutabilityNot enforcedTrigger blocks UPDATE/DELETE
Nurse signupDirect insert, no validationMulti-step with license verification
Document vaultNot implementedFull upload + OCR + review workflow
Device metadataNot captureddevice_id, OS, app_version recorded

Backend Automation

AspectOriginalRevised
Credential sourceClaude API with simulated dataNursys e-Notify API (real license data)
Decision logicAI text parsing ('CLEAR'/'FLAGGED')Deterministic rules: enum + risk score + date math
Hard-stopInline Edge Function logicDedicated PostgreSQL function with per-shift audit
Audit trailconsole.warn() onlyStructured audit_events table
SchedulingManual HTTP callpg_cron or external scheduler
Expiration window24h only72h warning + 24h hard-stop

Complete Original Codebase

Download the finalized TECS prototype source files — consolidated SQL with RBAC, Edge Function with Claude AI audit, Flutter mobile actions, and the UI routing model spec.

Download Complete Codebase (.zip)All 4 files bundled

Security Architecture

LayerOriginalRevised
Encryption at restNone (plaintext)XChaCha20-Poly1305 AEAD
Key managementNoneSupabase Vault / external KMS
Audit loggingNoneAppend-only audit_events + PGAudit
PII handlingStored rawDOB hashed; legal/preferred name split
File integrityNoneSHA-256 on all documents
GPS verificationClient-side onlyServer-side with logged results

Interactive SQL Diff Viewer

RBAC Extension

Compare the original 4-table prototype schema against the revised 15+ table production schema. Drag the center handle to resize panes. Toggle between split and unified views.

+887 added-146 removed0 unchanged
Consolidated Original (5 tables + RBAC)
1--- ============================================================================
2--- 1. BASE SYSTEM STRUCTURAL CORE & RELATIONAL EXTENSIONS
3--- ============================================================================
4-CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
5-
6--- Avoid schema drift by cleanly dropping conflicts on rebuilds
7-DROP TABLE IF EXISTS public.user_profiles CASCADE;
8-DROP TABLE IF EXISTS public.incident_reports CASCADE;
9-DROP TABLE IF EXISTS public.shifts CASCADE;
10-DROP TABLE IF EXISTS public.facilities CASCADE;
11-DROP TABLE IF EXISTS public.nurses CASCADE;
12-DROP TYPE IF EXISTS public.user_role CASCADE;
13-
14--- Establish Explicit Multi-Portal Access Enums
15-CREATE TYPE public.user_role AS ENUM (text-amber-300">'tecs_admin', text-amber-300">'facility_partner', text-amber-300">'field_nurse');
16-
17--- Clinician Compliance Infrastructure Table
18-CREATE TABLE public.nurses (
19- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
20- name TEXT NOT NULL,
21- license_number TEXT UNIQUE NOT NULL,
22- license_status TEXT NOT NULL DEFAULT text-amber-300">'Active', -- Active, Suspended, Under_Review
23- compliance_status TEXT NOT NULL DEFAULT text-amber-300">'Green', -- Green, Yellow, Red
24- bls_expiration DATE NOT NULL,
25- tb_expiration DATE NOT NULL,
26- created_at TIMESTAMPTZ DEFAULT now()
27-);
28-
29--- Multi-Tenant Facility Profile Hubs (Hospitals, Prisons, LTACs)
30-CREATE TABLE public.facilities (
31- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
32- name TEXT NOT NULL,
33- region TEXT NOT NULL DEFAULT text-amber-300">'DFW',
34- gps_latitude NUMERIC(9,6) NOT NULL,
35- gps_longitude NUMERIC(9,6) NOT NULL,
36- monthly_saas_fee NUMERIC(8,2) DEFAULT 0.00,
37- vms_admin_percentage NUMERIC(4,2) DEFAULT 3.00, -- Default 3% Managed Admin Markup
38- created_at TIMESTAMPTZ DEFAULT now()
39-);
40-
41--- Central Identity Security Workspace Profiles Table
42-CREATE TABLE public.user_profiles (
43- id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
44- email TEXT NOT NULL UNIQUE,
45- associated_role public.user_role NOT NULL DEFAULT text-amber-300">'field_nurse',
46- facility_id UUID REFERENCES public.facilities(id) ON DELETE SET NULL,
47- nurse_id UUID REFERENCES public.nurses(id) ON DELETE SET NULL,
48- created_at TIMESTAMPTZ DEFAULT now()
49-);
50-
51--- Scheduling Engine with Automated Float Profit Accounting
52-CREATE TABLE public.shifts (
53- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
54- facility_id UUID NOT NULL REFERENCES public.facilities(id) ON DELETE CASCADE,
55- nurse_id UUID REFERENCES public.nurses(id) ON DELETE SET NULL,
56- unit_type TEXT NOT NULL, -- ICU, ER, Med-Surg, L&D
57- shift_start TIMESTAMPTZ NOT NULL,
58- shift_end TIMESTAMPTZ NOT NULL,
59- is_locked BOOLEAN NOT NULL DEFAULT FALSE,
60- bill_rate NUMERIC(6,2) NOT NULL DEFAULT 0.00,
61- pay_rate NUMERIC(6,2) NOT NULL DEFAULT 0.00,
62- calculated_gross_profit NUMERIC(6,2) GENERATED ALWAYS AS (bill_rate - pay_rate) STORED,
63- created_at TIMESTAMPTZ DEFAULT now()
64-);
65-
66--- Immutable Incident Logs (The Guardian GRC Defense Layer)
67-CREATE TABLE public.incident_reports (
68- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
69- nurse_id UUID NOT NULL REFERENCES public.nurses(id) ON DELETE RESTRICT,
70- facility_id UUID NOT NULL REFERENCES public.facilities(id) ON DELETE RESTRICT,
71- severity_tier TEXT NOT NULL, -- Tier_1_Critical, Tier_2_Admin
72- incident_type TEXT NOT NULL, -- Medication_Error, Patient_Fall, Abuse_Neglect
73- narrative TEXT NOT NULL, -- Secure AES-256 Encrypted Clinical Notes
74- is_legal_hold BOOLEAN NOT NULL DEFAULT FALSE,
75- gps_latitude NUMERIC(9,6),
76- gps_longitude NUMERIC(9,6),
77- created_at TIMESTAMPTZ DEFAULT now()
78-);
79-
80--- ============================================================================
81--- 2. "AUDITOR-GRADE" DELETION INTERLOCKS & VERSION CONTROLS
82--- ============================================================================
83-
84--- Disables standard DELETE paths completely on critical clinical logs
85-CREATE RULE protect_tecs_incidents AS
86-ON DELETE TO public.incident_reports
87-DO INSTEAD NOTHING;
88-
89--- Enforces historical version lock parameters on filed reports during active audits
90-CREATE OR REPLACE FUNCTION public.lock_immutable_incidents()
91-RETURNS TRIGGER AS $$
92-BEGIN
93- IF OLD.is_legal_hold = TRUE AND NEW.is_legal_hold = FALSE THEN
94- RAISE EXCEPTION text-amber-300">'Audit Violation: Active legal holds cannot be stripped without senior compliance override keys.';
95- END IF;
96- IF OLD.narrative IS DISTINCT FROM NEW.narrative THEN
97- RAISE EXCEPTION text-amber-300">'Audit Violation: Modifications to existing clinical incident narratives are strictly forbidden.';
98- END IF;
99- RETURN NEW;
100-END;
101-$$ LANGUAGE plpgsql;
102-
103-CREATE TRIGGER trg_lock_incidents
104-BEFORE UPDATE ON public.incident_reports
105-FOR EACH ROW EXECUTE FUNCTION public.lock_immutable_incidents();
106-
107--- ============================================================================
108--- 3. GOVERNANCE AND SECURITY SECURITY (ROW-LEVEL SECURITY MESH)
109--- ============================================================================
110-ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
111-ALTER TABLE public.nurses ENABLE ROW LEVEL SECURITY;
112-ALTER TABLE public.facilities ENABLE ROW LEVEL SECURITY;
113-ALTER TABLE public.shifts ENABLE ROW LEVEL SECURITY;
114-ALTER TABLE public.incident_reports ENABLE ROW LEVEL SECURITY;
115-
116--- Rule A: System Admins hold unrestricted read/write authority across the database
117-CREATE POLICY admin_omni_access ON public.user_profiles
118- FOR ALL USING (
119- EXISTS (
120- SELECT 1 FROM public.user_profiles
121- WHERE user_profiles.id = auth.uid() AND associated_role = text-amber-300">'tecs_admin'
122- )
123- );
124-
125--- Rule B: Multi-Tenant Tenant Isolation: Facility partners see only their specific shifts/records
126-CREATE POLICY client_secure_shift_access ON public.shifts
127- FOR ALL USING (
128- facility_id = (
129- SELECT facility_id FROM public.user_profiles WHERE user_profiles.id = auth.uid()
130- )
131- );
132-
133-CREATE POLICY client_secure_incident_access ON public.incident_reports
134- FOR ALL USING (
135- facility_id = (
136- SELECT facility_id FROM public.user_profiles WHERE user_profiles.id = auth.uid()
137- )
138- );
139-
140--- Rule C: Field Clinicians isolated completely to viewing their personal nurse data vectors
141-CREATE POLICY nurse_self_access ON public.nurses
142- FOR SELECT USING (
143- id = (
144- SELECT nurse_id FROM public.user_profiles WHERE user_profiles.id = auth.uid()
145- )
146- );
Revised Schema (15+ tables)
1+-- ============================================================================
2+-- TECS REVISED DATABASE SCHEMA
3+-- Texas Executive Clinical Systems
4+-- Purpose: Production-oriented Supabase/PostgreSQL schema incorporating
5+-- corrected RLS, encrypted-content storage patterns, credential
6+-- verification records, append-only incident addenda, and audit logging.
7+-- Author: Manus AI
8+-- Date: 2026-05-20
9+-- ============================================================================
10+
11+-- ---------------------------------------------------------------------------
12+-- 0. EXTENSIONS AND SCHEMA
13+-- ---------------------------------------------------------------------------
14+
15+create extension if not exists pgcrypto;
16+create extension if not exists "uuid-ossp";
17+-- Enable pgaudit from the Supabase dashboard when available, then configure
18+-- role/object logging outside this migration. Some hosted environments restrict
19+-- direct CREATE EXTENSION for pgaudit.
20+-- create extension if not exists pgaudit;
21+
22+create schema if not exists tecs;
23+
24+-- ---------------------------------------------------------------------------
25+-- 1. CONTROLLED VOCABULARIES
26+-- ---------------------------------------------------------------------------
27+
28+do $$ begin
29+ create type tecs.member_role as enum (
30+ text-amber-300">'facility_admin',
31+ text-amber-300">'scheduler',
32+ text-amber-300">'compliance_officer',
33+ text-amber-300">'clinical_reviewer',
34+ text-amber-300">'read_only_auditor'
35+ );
36+exception when duplicate_object then null;
37+end $$;
38+
39+do $$ begin
40+ create type tecs.license_status as enum (
41+ text-amber-300">'active',
42+ text-amber-300">'expired',
43+ text-amber-300">'suspended',
44+ text-amber-300">'encumbered',
45+ text-amber-300">'under_review',
46+ text-amber-300">'unknown'
47+ );
48+exception when duplicate_object then null;
49+end $$;
50+
51+do $$ begin
52+ create type tecs.compliance_status as enum (
53+ text-amber-300">'green',
54+ text-amber-300">'yellow',
55+ text-amber-300">'red',
56+ text-amber-300">'manual_review'
57+ );
58+exception when duplicate_object then null;
59+end $$;
60+
61+do $$ begin
62+ create type tecs.credential_source as enum (
63+ text-amber-300">'nursys_enotify_api',
64+ text-amber-300">'texas_bon_portal_manual',
65+ text-amber-300">'document_upload',
66+ text-amber-300">'facility_attestation',
67+ text-amber-300">'system_backfill'
68+ );
69+exception when duplicate_object then null;
70+end $$;
71+
72+do $$ begin
73+ create type tecs.verification_result as enum (
74+ text-amber-300">'clear',
75+ text-amber-300">'flagged',
76+ text-amber-300">'not_found',
77+ text-amber-300">'source_unavailable',
78+ text-amber-300">'manual_review_required'
79+ );
80+exception when duplicate_object then null;
81+end $$;
82+
83+do $$ begin
84+ create type tecs.shift_status as enum (
85+ text-amber-300">'open',
86+ text-amber-300">'assigned',
87+ text-amber-300">'locked',
88+ text-amber-300">'released',
89+ text-amber-300">'cancelled',
90+ text-amber-300">'completed'
91+ );
92+exception when duplicate_object then null;
93+end $$;
94+
95+do $$ begin
96+ create type tecs.incident_severity as enum (
97+ text-amber-300">'tier_1_critical',
98+ text-amber-300">'tier_2_clinical',
99+ text-amber-300">'tier_3_admin'
100+ );
101+exception when duplicate_object then null;
102+end $$;
103+
104+do $$ begin
105+ create type tecs.audit_action as enum (
106+ text-amber-300">'insert',
107+ text-amber-300">'update',
108+ text-amber-300">'delete_attempt_blocked',
109+ text-amber-300">'credential_scan_started',
110+ text-amber-300">'credential_scan_completed',
111+ text-amber-300">'credential_flag_detected',
112+ text-amber-300">'hard_stop_applied',
113+ text-amber-300">'shift_released',
114+ text-amber-300">'incident_created',
115+ text-amber-300">'incident_addendum_created',
116+ text-amber-300">'legal_hold_applied',
117+ text-amber-300">'security_event'
118+ );
119+exception when duplicate_object then null;
120+end $$;
121+
122+-- ---------------------------------------------------------------------------
123+-- 2. IDENTITY, TENANCY, AND ROLE MEMBERSHIP
124+-- ---------------------------------------------------------------------------
125+
126+create table if not exists tecs.organizations (
127+ id uuid primary key default gen_random_uuid(),
128+ name text not null,
129+ legal_name text,
130+ tax_identifier_last4 text,
131+ business_associate_agreement_on_file boolean not null default false,
132+ is_active boolean not null default true,
133+ created_at timestamptz not null default now(),
134+ updated_at timestamptz not null default now()
135+);
136+
137+create table if not exists tecs.facilities (
138+ id uuid primary key default gen_random_uuid(),
139+ organization_id uuid not null references tecs.organizations(id) on delete restrict,
140+ name text not null,
141+ region text not null default text-amber-300">'DFW',
142+ npi text,
143+ state_license_number text,
144+ address_line1 text,
145+ address_line2 text,
146+ city text,
147+ state text not null default text-amber-300">'TX',
148+ postal_code text,
149+ is_active boolean not null default true,
150+ created_at timestamptz not null default now(),
151+ updated_at timestamptz not null default now(),
152+ unique (organization_id, name)
153+);
154+
155+create table if not exists tecs.user_profiles (
156+ user_id uuid primary key references auth.users(id) on delete cascade,
157+ display_name text not null,
158+ email text,
159+ phone text,
160+ is_platform_admin boolean not null default false,
161+ created_at timestamptz not null default now(),
162+ updated_at timestamptz not null default now()
163+);
164+
165+create table if not exists tecs.facility_memberships (
166+ id uuid primary key default gen_random_uuid(),
167+ facility_id uuid not null references tecs.facilities(id) on delete cascade,
168+ user_id uuid not null references auth.users(id) on delete cascade,
169+ role tecs.member_role not null,
170+ is_active boolean not null default true,
171+ granted_by uuid references auth.users(id) on delete set null,
172+ created_at timestamptz not null default now(),
173+ updated_at timestamptz not null default now(),
174+ unique (facility_id, user_id, role)
175+);
176+
177+create index if not exists idx_facility_memberships_user_facility
178+ on tecs.facility_memberships(user_id, facility_id)
179+ where is_active = true;
180+
181+-- ---------------------------------------------------------------------------
182+-- 3. NURSE MASTER RECORDS AND FACILITY ASSIGNMENTS
183+-- ---------------------------------------------------------------------------
184+
185+create table if not exists tecs.nurses (
186+ id uuid primary key default gen_random_uuid(),
187+ legal_name text not null,
188+ preferred_name text,
189+ date_of_birth_hash text,
190+ license_number text not null,
191+ license_jurisdiction text not null default text-amber-300">'TX',
192+ license_type text not null default text-amber-300">'RN',
193+ ncsbn_id text,
194+ license_status tecs.license_status not null default text-amber-300">'unknown',
195+ compliance_status tecs.compliance_status not null default text-amber-300">'manual_review',
196+ bls_expiration date,
197+ tb_expiration date,
198+ compact_license boolean not null default false,
199+ last_verified_at timestamptz,
200+ next_verification_due_at timestamptz,
201+ created_at timestamptz not null default now(),
202+ updated_at timestamptz not null default now(),
203+ unique (license_jurisdiction, license_number)
204+);
205+
206+create index if not exists idx_nurses_status_due
207+ on tecs.nurses(compliance_status, next_verification_due_at);
208+
209+create table if not exists tecs.facility_nurses (
210+ id uuid primary key default gen_random_uuid(),
211+ facility_id uuid not null references tecs.facilities(id) on delete cascade,
212+ nurse_id uuid not null references tecs.nurses(id) on delete restrict,
213+ relationship_status text not null default text-amber-300">'active',
214+ onboarding_completed_at timestamptz,
215+ offboarded_at timestamptz,
216+ created_at timestamptz not null default now(),
217+ updated_at timestamptz not null default now(),
218+ unique (facility_id, nurse_id)
219+);
220+
221+create index if not exists idx_facility_nurses_nurse
222+ on tecs.facility_nurses(nurse_id);
223+
224+-- ---------------------------------------------------------------------------
225+-- 4. CREDENTIAL DOCUMENTS AND REAL-TIME VERIFICATION RECORDS
226+-- ---------------------------------------------------------------------------
227+
228+create table if not exists tecs.credential_documents (
229+ id uuid primary key default gen_random_uuid(),
230+ nurse_id uuid not null references tecs.nurses(id) on delete cascade,
231+ facility_id uuid references tecs.facilities(id) on delete cascade,
232+ document_type text not null,
233+ storage_bucket text not null,
234+ storage_path text not null,
235+ file_sha256 text not null,
236+ issued_at date,
237+ expires_at date,
238+ verified_by uuid references auth.users(id) on delete set null,
239+ verified_at timestamptz,
240+ created_at timestamptz not null default now(),
241+ unique (storage_bucket, storage_path)
242+);
243+
244+create table if not exists tecs.credential_verifications (
245+ id uuid primary key default gen_random_uuid(),
246+ nurse_id uuid not null references tecs.nurses(id) on delete cascade,
247+ source tecs.credential_source not null,
248+ result tecs.verification_result not null,
249+ license_status tecs.license_status not null default text-amber-300">'unknown',
250+ source_reference_id text,
251+ source_checked_at timestamptz not null default now(),
252+ raw_payload jsonb,
253+ raw_payload_sha256 text,
254+ parsed_payload jsonb,
255+ risk_score integer not null default 0 check (risk_score between 0 and 100),
256+ reason text,
257+ reviewed_by uuid references auth.users(id) on delete set null,
258+ reviewed_at timestamptz,
259+ created_at timestamptz not null default now()
260+);
261+
262+create index if not exists idx_credential_verifications_nurse_created
263+ on tecs.credential_verifications(nurse_id, created_at desc);
264+
265+create index if not exists idx_credential_verifications_result
266+ on tecs.credential_verifications(result, source_checked_at desc);
267+
268+-- ---------------------------------------------------------------------------
269+-- 5. SHIFT DISPATCH CORE
270+-- ---------------------------------------------------------------------------
271+
272+create table if not exists tecs.shifts (
273+ id uuid primary key default gen_random_uuid(),
274+ facility_id uuid not null references tecs.facilities(id) on delete cascade,
275+ nurse_id uuid references tecs.nurses(id) on delete set null,
276+ unit_type text not null,
277+ hourly_rate numeric(8,2) not null check (hourly_rate >= 0),
278+ shift_start timestamptz not null,
279+ shift_end timestamptz not null,
280+ status tecs.shift_status not null default text-amber-300">'open',
281+ locked_at timestamptz,
282+ released_reason text,
283+ created_by uuid references auth.users(id) on delete set null,
284+ created_at timestamptz not null default now(),
285+ updated_at timestamptz not null default now(),
286+ check (shift_end > shift_start),
287+ check ((status in (text-amber-300">'assigned',text-amber-300">'locked',text-amber-300">'completed') and nurse_id is not null) or status in (text-amber-300">'open',text-amber-300">'released',text-amber-300">'cancelled'))
288+);
289+
290+create index if not exists idx_shifts_facility_window
291+ on tecs.shifts(facility_id, shift_start, shift_end);
292+
293+create index if not exists idx_shifts_nurse_future
294+ on tecs.shifts(nurse_id, shift_start)
295+ where nurse_id is not null;
296+
297+-- ---------------------------------------------------------------------------
298+-- 6. ENCRYPTION-READY INCIDENT REPORTS AND ADDENDA
299+-- ---------------------------------------------------------------------------
300+
301+create table if not exists tecs.incident_reports (
302+ id uuid primary key default gen_random_uuid(),
303+ nurse_id uuid not null references tecs.nurses(id) on delete restrict,
304+ facility_id uuid not null references tecs.facilities(id) on delete restrict,
305+ severity_tier tecs.incident_severity not null,
306+ incident_type text not null,
307+ narrative_ciphertext bytea not null,
308+ narrative_sha256 text not null,
309+ encryption_key_ref text not null,
310+ encryption_algorithm text not null default text-amber-300">'app-managed-aead-xchacha20poly1305',
311+ is_legal_hold boolean not null default false,
312+ legal_hold_reason text,
313+ gps_latitude numeric(9,6),
314+ gps_longitude numeric(9,6),
315+ reported_by uuid references auth.users(id) on delete set null,
316+ created_at timestamptz not null default now(),
317+ updated_at timestamptz not null default now()
318+);
319+
320+create index if not exists idx_incident_reports_facility_created
321+ on tecs.incident_reports(facility_id, created_at desc);
322+
323+create index if not exists idx_incident_reports_nurse_created
324+ on tecs.incident_reports(nurse_id, created_at desc);
325+
326+create table if not exists tecs.incident_addenda (
327+ id uuid primary key default gen_random_uuid(),
328+ incident_id uuid not null references tecs.incident_reports(id) on delete restrict,
329+ addendum_ciphertext bytea not null,
330+ addendum_sha256 text not null,
331+ encryption_key_ref text not null,
332+ encryption_algorithm text not null default text-amber-300">'app-managed-aead-xchacha20poly1305',
333+ created_by uuid references auth.users(id) on delete set null,
334+ created_at timestamptz not null default now()
335+);
336+
337+create index if not exists idx_incident_addenda_incident_created
338+ on tecs.incident_addenda(incident_id, created_at asc);
339+
340+-- ---------------------------------------------------------------------------
341+-- 7. APPEND-ONLY AUDIT EVENTS
342+-- ---------------------------------------------------------------------------
343+
344+create table if not exists tecs.audit_events (
345+ id uuid primary key default gen_random_uuid(),
346+ facility_id uuid references tecs.facilities(id) on delete set null,
347+ nurse_id uuid references tecs.nurses(id) on delete set null,
348+ actor_user_id uuid references auth.users(id) on delete set null,
349+ actor_kind text not null default text-amber-300">'system',
350+ action tecs.audit_action not null,
351+ entity_schema text not null default text-amber-300">'tecs',
352+ entity_table text,
353+ entity_id uuid,
354+ reason text,
355+ metadata jsonb not null default text-amber-300">'{}'::jsonb,
356+ source_ip inet,
357+ user_agent text,
358+ request_id text,
359+ created_at timestamptz not null default now()
360+);
361+
362+create index if not exists idx_audit_events_facility_created
363+ on tecs.audit_events(facility_id, created_at desc);
364+
365+create index if not exists idx_audit_events_entity
366+ on tecs.audit_events(entity_table, entity_id, created_at desc);
367+
368+-- ---------------------------------------------------------------------------
369+-- 8. UPDATED_AT AUTOMATION
370+-- ---------------------------------------------------------------------------
371+
372+create or replace function tecs.set_updated_at()
373+returns trigger
374+language plpgsql
375+as $$
376+begin
377+ new.updated_at = now();
378+ return new;
379+end;
380+$$;
381+
382+create trigger trg_organizations_updated_at
383+before update on tecs.organizations
384+for each row execute function tecs.set_updated_at();
385+
386+create trigger trg_facilities_updated_at
387+before update on tecs.facilities
388+for each row execute function tecs.set_updated_at();
389+
390+create trigger trg_user_profiles_updated_at
391+before update on tecs.user_profiles
392+for each row execute function tecs.set_updated_at();
393+
394+create trigger trg_facility_memberships_updated_at
395+before update on tecs.facility_memberships
396+for each row execute function tecs.set_updated_at();
397+
398+create trigger trg_nurses_updated_at
399+before update on tecs.nurses
400+for each row execute function tecs.set_updated_at();
401+
402+create trigger trg_facility_nurses_updated_at
403+before update on tecs.facility_nurses
404+for each row execute function tecs.set_updated_at();
405+
406+create trigger trg_shifts_updated_at
407+before update on tecs.shifts
408+for each row execute function tecs.set_updated_at();
409+
410+create trigger trg_incident_reports_updated_at
411+before update on tecs.incident_reports
412+for each row execute function tecs.set_updated_at();
413+
414+-- ---------------------------------------------------------------------------
415+-- 9. IMMUTABILITY AND HARD-STOP TRIGGERS
416+-- ---------------------------------------------------------------------------
417+
418+create or replace function tecs.block_incident_mutation()
419+returns trigger
420+language plpgsql
421+security definer
422+set search_path = tecs, public
423+as $$
424+begin
425+ if tg_op = text-amber-300">'DELETE' then
426+ insert into tecs.audit_events(action, actor_kind, entity_table, entity_id, facility_id, nurse_id, reason)
427+ values (text-amber-300">'delete_attempt_blocked', text-amber-300">'database_trigger', text-amber-300">'incident_reports', old.id, old.facility_id, old.nurse_id,
428+ text-amber-300">'Attempted deletion of immutable incident report was blocked.');
429+ raise exception text-amber-300">'Audit violation: incident reports are append-only and cannot be deleted.';
430+ end if;
431+
432+ if old.narrative_ciphertext is distinct from new.narrative_ciphertext
433+ or old.narrative_sha256 is distinct from new.narrative_sha256
434+ or old.encryption_key_ref is distinct from new.encryption_key_ref then
435+ raise exception text-amber-300">'Audit violation: incident narratives are immutable. Create an incident_addenda record instead.';
436+ end if;
437+
438+ if old.is_legal_hold = true and new.is_legal_hold = false then
439+ raise exception text-amber-300">'Audit violation: legal holds cannot be removed through standard update paths.';
440+ end if;
441+
442+ return new;
443+end;
444+$$;
445+
446+create trigger trg_block_incident_update
447+before update on tecs.incident_reports
448+for each row execute function tecs.block_incident_mutation();
449+
450+create trigger trg_block_incident_delete
451+before delete on tecs.incident_reports
452+for each row execute function tecs.block_incident_mutation();
453+
454+create or replace function tecs.block_audit_event_mutation()
455+returns trigger
456+language plpgsql
457+as $$
458+begin
459+ raise exception text-amber-300">'Audit violation: audit_events are append-only and cannot be updated or deleted.';
460+end;
461+$$;
462+
463+create trigger trg_block_audit_event_update
464+before update on tecs.audit_events
465+for each row execute function tecs.block_audit_event_mutation();
466+
467+create trigger trg_block_audit_event_delete
468+before delete on tecs.audit_events
469+for each row execute function tecs.block_audit_event_mutation();
470+
471+create or replace function tecs.log_incident_insert()
472+returns trigger
473+language plpgsql
474+security definer
475+set search_path = tecs, public
476+as $$
477+begin
478+ insert into tecs.audit_events(action, actor_kind, actor_user_id, entity_table, entity_id, facility_id, nurse_id, reason)
479+ values (text-amber-300">'incident_created', text-amber-300">'authenticated_user', auth.uid(), text-amber-300">'incident_reports', new.id, new.facility_id, new.nurse_id,
480+ text-amber-300">'Incident report created as immutable encrypted record.');
481+ return new;
482+end;
483+$$;
484+
485+create trigger trg_log_incident_insert
486+after insert on tecs.incident_reports
487+for each row execute function tecs.log_incident_insert();
488+
489+create or replace function tecs.log_incident_addendum_insert()
490+returns trigger
491+language plpgsql
492+security definer
493+set search_path = tecs, public
494+as $$
495+declare
496+ v_facility_id uuid;
497+ v_nurse_id uuid;
498+begin
499+ select facility_id, nurse_id into v_facility_id, v_nurse_id
500+ from tecs.incident_reports
501+ where id = new.incident_id;
502+
503+ insert into tecs.audit_events(action, actor_kind, actor_user_id, entity_table, entity_id, facility_id, nurse_id, reason)
504+ values (text-amber-300">'incident_addendum_created', text-amber-300">'authenticated_user', auth.uid(), text-amber-300">'incident_addenda', new.id, v_facility_id, v_nurse_id,
505+ text-amber-300">'Incident addendum appended without modifying original incident narrative.');
506+ return new;
507+end;
508+$$;
509+
510+create trigger trg_log_incident_addendum_insert
511+after insert on tecs.incident_addenda
512+for each row execute function tecs.log_incident_addendum_insert();
513+
514+-- ---------------------------------------------------------------------------
515+-- 10. RLS HELPER FUNCTIONS
516+-- ---------------------------------------------------------------------------
517+
518+create or replace function tecs.is_platform_admin()
519+returns boolean
520+language sql
521+security definer
522+set search_path = tecs, public
523+stable
524+as $$
525+ select exists (
526+ select 1
527+ from tecs.user_profiles up
528+ where up.user_id = auth.uid()
529+ and up.is_platform_admin = true
530+ );
531+$$;
532+
533+create or replace function tecs.is_facility_member(p_facility_id uuid)
534+returns boolean
535+language sql
536+security definer
537+set search_path = tecs, public
538+stable
539+as $$
540+ select auth.uid() is not null and (
541+ tecs.is_platform_admin()
542+ or exists (
543+ select 1
544+ from tecs.facility_memberships fm
545+ where fm.facility_id = p_facility_id
546+ and fm.user_id = auth.uid()
547+ and fm.is_active = true
548+ )
549+ );
550+$$;
551+
552+create or replace function tecs.has_facility_role(p_facility_id uuid, p_roles tecs.member_role[])
553+returns boolean
554+language sql
555+security definer
556+set search_path = tecs, public
557+stable
558+as $$
559+ select auth.uid() is not null and (
560+ tecs.is_platform_admin()
561+ or exists (
562+ select 1
563+ from tecs.facility_memberships fm
564+ where fm.facility_id = p_facility_id
565+ and fm.user_id = auth.uid()
566+ and fm.role = any(p_roles)
567+ and fm.is_active = true
568+ )
569+ );
570+$$;
571+
572+create or replace function tecs.can_access_nurse(p_nurse_id uuid)
573+returns boolean
574+language sql
575+security definer
576+set search_path = tecs, public
577+stable
578+as $$
579+ select auth.uid() is not null and (
580+ tecs.is_platform_admin()
581+ or exists (
582+ select 1
583+ from tecs.facility_nurses fn
584+ join tecs.facility_memberships fm on fm.facility_id = fn.facility_id
585+ where fn.nurse_id = p_nurse_id
586+ and fm.user_id = auth.uid()
587+ and fm.is_active = true
588+ and fn.relationship_status = text-amber-300">'active'
589+ )
590+ );
591+$$;
592+
593+-- ---------------------------------------------------------------------------
594+-- 11. ROW LEVEL SECURITY
595+-- ---------------------------------------------------------------------------
596+
597+alter table tecs.organizations enable row level security;
598+alter table tecs.facilities enable row level security;
599+alter table tecs.user_profiles enable row level security;
600+alter table tecs.facility_memberships enable row level security;
601+alter table tecs.nurses enable row level security;
602+alter table tecs.facility_nurses enable row level security;
603+alter table tecs.credential_documents enable row level security;
604+alter table tecs.credential_verifications enable row level security;
605+alter table tecs.shifts enable row level security;
606+alter table tecs.incident_reports enable row level security;
607+alter table tecs.incident_addenda enable row level security;
608+alter table tecs.audit_events enable row level security;
609+
610+-- Organizations
611+create policy "organization members can read their organizations"
612+on tecs.organizations for select
613+using (
614+ tecs.is_platform_admin()
615+ or exists (
616+ select 1
617+ from tecs.facilities f
618+ join tecs.facility_memberships fm on fm.facility_id = f.id
619+ where f.organization_id = organizations.id
620+ and fm.user_id = auth.uid()
621+ and fm.is_active = true
622+ )
623+);
624+
625+create policy "platform admins can manage organizations"
626+on tecs.organizations for all
627+using (tecs.is_platform_admin())
628+with check (tecs.is_platform_admin());
629+
630+-- Facilities
631+create policy "facility members can read facilities"
632+on tecs.facilities for select
633+using (tecs.is_facility_member(id));
634+
635+create policy "facility admins can update their facilities"
636+on tecs.facilities for update
637+using (tecs.has_facility_role(id, array[text-amber-300">'facility_admin']::tecs.member_role[]))
638+with check (tecs.has_facility_role(id, array[text-amber-300">'facility_admin']::tecs.member_role[]));
639+
640+-- User profiles
641+create policy "users can read own profile"
642+on tecs.user_profiles for select
643+using (auth.uid() is not null and user_id = auth.uid());
644+
645+create policy "users can update own limited profile"
646+on tecs.user_profiles for update
647+using (auth.uid() is not null and user_id = auth.uid())
648+with check (auth.uid() is not null and user_id = auth.uid() and is_platform_admin = false);
649+
650+create policy "platform admins can read profiles"
651+on tecs.user_profiles for select
652+using (tecs.is_platform_admin());
653+
654+-- Facility memberships
655+create policy "facility admins can read memberships"
656+on tecs.facility_memberships for select
657+using (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[]));
658+
659+create policy "facility admins can manage memberships"
660+on tecs.facility_memberships for all
661+using (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin']::tecs.member_role[]))
662+with check (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin']::tecs.member_role[]));
663+
664+-- Nurses
665+create policy "facility members can read linked nurses"
666+on tecs.nurses for select
667+using (tecs.can_access_nurse(id));
668+
669+create policy "compliance officers can manage linked nurses"
670+on tecs.nurses for update
671+using (
672+ tecs.is_platform_admin()
673+ or exists (
674+ select 1
675+ from tecs.facility_nurses fn
676+ where fn.nurse_id = nurses.id
677+ and tecs.has_facility_role(fn.facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[])
678+ )
679+)
680+with check (
681+ tecs.is_platform_admin()
682+ or exists (
683+ select 1
684+ from tecs.facility_nurses fn
685+ where fn.nurse_id = nurses.id
686+ and tecs.has_facility_role(fn.facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[])
687+ )
688+);
689+
690+-- Facility-nurse assignments
691+create policy "facility members can read facility nurse assignments"
692+on tecs.facility_nurses for select
693+using (tecs.is_facility_member(facility_id));
694+
695+create policy "facility compliance can manage nurse assignments"
696+on tecs.facility_nurses for all
697+using (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[]))
698+with check (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[]));
699+
700+-- Credential documents
701+create policy "facility compliance can read credential documents"
702+on tecs.credential_documents for select
703+using (
704+ tecs.is_platform_admin()
705+ or (facility_id is not null and tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[]))
706+ or tecs.can_access_nurse(nurse_id)
707+);
708+
709+create policy "facility compliance can manage credential documents"
710+on tecs.credential_documents for all
711+using (tecs.is_platform_admin() or (facility_id is not null and tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[])))
712+with check (tecs.is_platform_admin() or (facility_id is not null and tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[])));
713+
714+-- Credential verifications
715+create policy "facility compliance can read verification history"
716+on tecs.credential_verifications for select
717+using (tecs.is_platform_admin() or tecs.can_access_nurse(nurse_id));
718+
719+-- Writes to credential_verifications should normally be performed by service_role
720+-- from Edge Functions or back-office jobs. Human users may only update review fields
721+-- through controlled RPC functions, not direct table updates.
722+
723+-- Shifts
724+create policy "facility members can read shifts"
725+on tecs.shifts for select
726+using (tecs.is_facility_member(facility_id));
727+
728+create policy "schedulers can manage shifts"
729+on tecs.shifts for all
730+using (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'scheduler',text-amber-300">'compliance_officer']::tecs.member_role[]))
731+with check (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'scheduler',text-amber-300">'compliance_officer']::tecs.member_role[]));
732+
733+-- Incident reports
734+create policy "clinical and compliance roles can read incident metadata"
735+on tecs.incident_reports for select
736+using (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer',text-amber-300">'clinical_reviewer',text-amber-300">'read_only_auditor']::tecs.member_role[]));
737+
738+create policy "clinical and compliance roles can create incident reports"
739+on tecs.incident_reports for insert
740+with check (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer',text-amber-300">'clinical_reviewer']::tecs.member_role[]));
741+
742+create policy "compliance officers can update legal hold metadata only"
743+on tecs.incident_reports for update
744+using (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[]))
745+with check (tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer']::tecs.member_role[]));
746+
747+-- Incident addenda
748+create policy "incident readers can read addenda"
749+on tecs.incident_addenda for select
750+using (
751+ exists (
752+ select 1
753+ from tecs.incident_reports ir
754+ where ir.id = incident_addenda.incident_id
755+ and tecs.has_facility_role(ir.facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer',text-amber-300">'clinical_reviewer',text-amber-300">'read_only_auditor']::tecs.member_role[])
756+ )
757+);
758+
759+create policy "clinical and compliance roles can append addenda"
760+on tecs.incident_addenda for insert
761+with check (
762+ exists (
763+ select 1
764+ from tecs.incident_reports ir
765+ where ir.id = incident_addenda.incident_id
766+ and tecs.has_facility_role(ir.facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer',text-amber-300">'clinical_reviewer']::tecs.member_role[])
767+ )
768+);
769+
770+-- Audit events
771+create policy "facility auditors can read audit events"
772+on tecs.audit_events for select
773+using (
774+ tecs.is_platform_admin()
775+ or (facility_id is not null and tecs.has_facility_role(facility_id, array[text-amber-300">'facility_admin',text-amber-300">'compliance_officer',text-amber-300">'read_only_auditor']::tecs.member_role[]))
776+);
777+
778+-- audit_events inserts are generated by database triggers and service_role jobs.
779+-- No direct authenticated insert/update/delete policy is intentionally provided.
780+
781+-- ---------------------------------------------------------------------------
782+-- 12. HARD-STOP PROCEDURE FOR CREDENTIAL FAILURES
783+-- ---------------------------------------------------------------------------
784+
785+create or replace function tecs.apply_credential_hard_stop(
786+ p_nurse_id uuid,
787+ p_reason text,
788+ p_verification_id uuid default null
789+)
790+returns void
791+language plpgsql
792+security definer
793+set search_path = tecs, public
794+as $$
795+declare
796+ v_shift record;
797+begin
798+ update tecs.nurses
799+ set compliance_status = text-amber-300">'red',
800+ license_status = case when license_status = text-amber-300">'active' then text-amber-300">'under_review' else license_status end,
801+ updated_at = now()
802+ where id = p_nurse_id;
803+
804+ for v_shift in
805+ select id, facility_id
806+ from tecs.shifts
807+ where nurse_id = p_nurse_id
808+ and shift_start > now()
809+ and status in (text-amber-300">'assigned', text-amber-300">'locked')
810+ loop
811+ update tecs.shifts
812+ set nurse_id = null,
813+ status = text-amber-300">'released',
814+ locked_at = null,
815+ released_reason = p_reason,
816+ updated_at = now()
817+ where id = v_shift.id;
818+
819+ insert into tecs.audit_events(facility_id, nurse_id, actor_kind, action, entity_table, entity_id, reason, metadata)
820+ values (
821+ v_shift.facility_id,
822+ p_nurse_id,
823+ text-amber-300">'system',
824+ text-amber-300">'shift_released',
825+ text-amber-300">'shifts',
826+ v_shift.id,
827+ p_reason,
828+ jsonb_build_object(text-amber-300">'verification_id', p_verification_id)
829+ );
830+ end loop;
831+
832+ insert into tecs.audit_events(nurse_id, actor_kind, action, entity_table, entity_id, reason, metadata)
833+ values (
834+ p_nurse_id,
835+ text-amber-300">'system',
836+ text-amber-300">'hard_stop_applied',
837+ text-amber-300">'nurses',
838+ p_nurse_id,
839+ p_reason,
840+ jsonb_build_object(text-amber-300">'verification_id', p_verification_id)
841+ );
842+end;
843+$$;
844+
845+-- ---------------------------------------------------------------------------
846+-- 13. OPERATIONAL VIEWS
847+-- ---------------------------------------------------------------------------
848+
849+create or replace view tecs.v_facility_shift_roster as
850+select
851+ s.id as shift_id,
852+ s.facility_id,
853+ f.name as facility_name,
854+ s.unit_type,
855+ s.shift_start,
856+ s.shift_end,
857+ s.status,
858+ n.id as nurse_id,
859+ n.legal_name as nurse_name,
860+ n.license_number,
861+ n.license_status,
862+ n.compliance_status,
863+ n.last_verified_at
864+from tecs.shifts s
865+join tecs.facilities f on f.id = s.facility_id
866+left join tecs.nurses n on n.id = s.nurse_id;
867+
868+-- RLS on underlying tables governs this view. In Supabase, prefer security_invoker
869+-- views where supported by the platform version:
870+-- alter view tecs.v_facility_shift_roster set (security_invoker = true);
871+
872+-- ---------------------------------------------------------------------------
873+-- 14. RECOMMENDED GRANTS
874+-- ---------------------------------------------------------------------------
875+
876+-- Adapt grants to the schemas exposed through Supabase API settings.
877+grant usage on schema tecs to authenticated;
878+grant select, insert, update, delete on all tables in schema tecs to authenticated;
879+grant usage, select on all sequences in schema tecs to authenticated;
880+
881+-- Do not grant anon access to TECS clinical tables unless a public endpoint is
882+-- explicitly designed and independently reviewed.
883+
884+-- ============================================================================
885+-- END OF TECS REVISED DATABASE SCHEMA
886+-- ============================================================================
887+

§ 8.0 Governance

Audit Logging and Compliance

The revised audit model uses an append-only audit_events table for business events and recommends PostgreSQL/PGAudit logging for database-level monitoring. Audit events should not include full clinical narratives — they use identifiers, timestamps, reason codes, and structured metadata.

Audit EventRequired FieldsPurpose
Credential scan started/completedSource, nurse count, job ID, timestampProves monitoring cadence
Credential flag detectedNurse ID, source, verification ID, reasonExplains compliance concern
Hard stop appliedNurse ID, rule ID, reason, verification IDSupports enforcement review
Shift releasedFacility ID, shift ID, nurse ID, reasonConnects staffing impact to compliance event
Incident createdFacility ID, nurse ID, severity, reporterPreserves clinical event creation trail
Incident addendum createdIncident ID, creator, timestampShows correction without overwriting original
Blocked deletion/mutationEntity ID, actor, timestamp, reasonDetects attempted evidence tampering

§ 9.0 Implementation

Implementation Roadmap

The implementation is divided into six phases, each producing both a technical output and a compliance output. This dual-track approach ensures that governance documentation keeps pace with engineering delivery.

1

Deploy Revised Schema

Technical Output

Tables, enums, triggers, helper functions, and RLS policies

Compliance Output

Approved role matrix and access-control policy

2

Validate RLS

Technical Output

Cross-facility isolation tests and role-limit tests

Compliance Output

Evidence that facility users cannot access other tenants

3

Implement Encryption

Technical Output

Application encryption layer and ciphertext migration pattern

Compliance Output

Key-management and decryption-authorization policy

4

Credential Source Integration

Technical Output

Nursys connector, Texas BON fallback, verification ingestion

Compliance Output

Source verification SOP and hard-stop decision policy

5

Audit Operations

Technical Output

Audit dashboard, SIEM/export path, blocked mutation alerts

Compliance Output

Audit review cadence and retention schedule

6

Pilot with Limited Facilities

Technical Output

Production-like pilot using controlled users and test nurses

Compliance Output

Go/no-go compliance sign-off

Critical Acceptance Tests

Facility A user attempts to read Facility B shifts→ Zero unauthorized rows returned
Scheduler attempts to alter credential verification status→ Operation denied
Incident narrative update attempted→ Exception raised, original ciphertext unchanged
Suspended license detected→ Nurse → red, future shifts released, audit events written
Source system unavailable→ Case moves to manual_review per documented policy
Audit event update/delete attempted→ Database raises exception