-- Praxis v0.4 operator-tier Postgres schema (reference). -- Applied in order by db/pg_migrate.py via db/pg_migrations/*.sql. -- The canonical migration is 0001_operator_tier.sql; this file is the -- human-readable reference (kept in sync). Uses gen_random_uuid() which -- is in PG16 core (no extension needed — R-MT-05 verified). -- -- Tables: -- operators — operator accounts (argon2id password hash) -- issued_credentials — VC issuance log (learner_ref is opaque, no FK) -- mastery_gate_events — mastery gate audit log (REQ-NFR-MAST-02) -- cohort_aggregates — k-anonymized cohort metrics (plain table, D-050) -- issuer_keys — Ed25519 issuer key lifecycle (active/superseded) -- -- No cross-DB FKs (D-031). learner_ref is an opaque string in Postgres. CREATE TABLE IF NOT EXISTS operators ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, display_name TEXT, role TEXT NOT NULL DEFAULT 'operator', is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_login_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS issued_credentials ( id UUID PRIMARY KEY, operator_id UUID REFERENCES operators(id), learner_ref TEXT NOT NULL, vc_type TEXT, payload_jsonb JSONB NOT NULL, signature_b64 TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', issued_at TIMESTAMPTZ NOT NULL DEFAULT now(), revoked_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS mastery_gate_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), learner_ref TEXT NOT NULL, scenario_id TEXT, path_id TEXT NOT NULL, gate_outcome TEXT, rubric_scores_jsonb JSONB, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), source TEXT NOT NULL DEFAULT 'sync' ); CREATE TABLE IF NOT EXISTS cohort_aggregates ( path TEXT NOT NULL, metric TEXT NOT NULL, window_start DATE NOT NULL, window_end DATE NOT NULL, value NUMERIC, cell_count INTEGER NOT NULL DEFAULT 0, cell_suppressed BOOLEAN NOT NULL DEFAULT FALSE, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (path, metric, window_start) ); -- Plain table, NOT partitioned (D-050..D-053; add partitioning post-pilot). CREATE INDEX IF NOT EXISTS cohort_aggregates_path_window_idx ON cohort_aggregates (path, window_start); CREATE TABLE IF NOT EXISTS issuer_keys ( id TEXT PRIMARY KEY, public_key TEXT NOT NULL, private_key_enc BYTEA, status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT now() );