{
  "schema_version": "1.0",
  "baseline": "A-admin-control-plane-batch-4-readiness",
  "phase": "2b → 2c readiness",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "purpose_th": "Target schema สำหรับ Admin Control Plane persistence · ครอบคลุม approvals · signatures · TTL state · audit cross-reference · เป็น target สำหรับ 2c migration",
  "purpose_en": "Target persistence schema for Admin Control Plane Service. Covers approvals, signatures, TTL state, and audit cross-reference. Phase 2b ships file-backed (approval_examples.json). This schema is the Phase 2c migration target.",
  "honest_note": "No admin schema is deployed in Phase 2b. docker/ai/postgres/init/ has no admin_*.sql. Every table below must be created via alembic in Phase 2c migration with parity against approval_examples.json.",
  "target_engine": "PostgreSQL 15+",
  "target_schema_name": "admin",
  "tables": [
    {
      "name": "admin.approval",
      "purpose_en": "One row per approval · mirrors approval_examples.json seeds + approval_store_schema.json validation",
      "ddl": "CREATE TABLE admin.approval (\n  approval_id TEXT PRIMARY KEY CHECK (approval_id ~ '^APP-[0-9]{6}-[A-Za-z0-9]{4,}$'),\n  matrix_row TEXT NOT NULL,\n  target_type TEXT NOT NULL,\n  target_id TEXT NOT NULL,\n  requested_by TEXT NOT NULL,\n  requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  state TEXT NOT NULL CHECK (state IN ('pending','signed_partial','signed_full','expired','rejected','withdrawn','closed_approved','closed_denied','in_review','escalated')),\n  required_signer_count INTEGER NOT NULL CHECK (required_signer_count >= 1),\n  signer_roles TEXT[] NOT NULL,\n  sla_deadline TIMESTAMPTZ NULL,\n  closed_at TIMESTAMPTZ NULL,\n  closed_by TEXT NULL,\n  reason TEXT,\n  context JSONB,\n  CONSTRAINT closed_states_require_closed_at CHECK (state NOT IN ('closed_approved','closed_denied','withdrawn','rejected') OR closed_at IS NOT NULL)\n);",
      "indexes": [
        "CREATE INDEX admin_approval_state_idx ON admin.approval (state);",
        "CREATE INDEX admin_approval_matrix_row_idx ON admin.approval (matrix_row);",
        "CREATE INDEX admin_approval_target_idx ON admin.approval (target_type, target_id);",
        "CREATE INDEX admin_approval_sla_deadline_idx ON admin.approval (sla_deadline) WHERE sla_deadline IS NOT NULL AND state IN ('pending','signed_partial','in_review');",
        "CREATE INDEX admin_approval_requested_at_idx ON admin.approval (requested_at DESC);"
      ],
      "file_backed_equivalent": "docs/runtime/admin-control-plane-service/approval_examples.json",
      "constraints_mirror_schema": "docs/runtime/admin-control-plane-service/approval_store_schema.json"
    },
    {
      "name": "admin.signature",
      "purpose_en": "Signer-side attestations · many rows per approval up to required_signer_count",
      "ddl": "CREATE TABLE admin.signature (\n  signature_id TEXT PRIMARY KEY,\n  approval_id TEXT NOT NULL REFERENCES admin.approval(approval_id) ON DELETE CASCADE,\n  signer_id TEXT NOT NULL,\n  signer_role TEXT NOT NULL,\n  signed_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  kind TEXT NOT NULL CHECK (kind IN ('approve','deny','delegate')),\n  comment TEXT,\n  evidence JSONB,\n  UNIQUE (approval_id, signer_id)\n);",
      "indexes": [
        "CREATE INDEX admin_signature_approval_idx ON admin.signature (approval_id);",
        "CREATE INDEX admin_signature_signer_idx ON admin.signature (signer_id);"
      ],
      "file_backed_equivalent": "implicit · embedded inside approval_examples.json signature arrays",
      "invariant": "count(kind='approve') for an approval_id reaches admin.approval.required_signer_count before state transitions to signed_full"
    },
    {
      "name": "admin.approval_ttl_state",
      "purpose_en": "Derived view OR materialized view of TTL evaluation · surfaces ttl_state values from ttl_examples.json",
      "ddl": "CREATE VIEW admin.approval_ttl_state AS\n  SELECT\n    approval_id,\n    sla_deadline,\n    state,\n    CASE\n      WHEN state IN ('withdrawn','rejected','closed_denied') THEN 'not-applicable'\n      WHEN sla_deadline IS NULL THEN 'unknown'\n      WHEN sla_deadline < now() - INTERVAL '24 hours' THEN 'stale'\n      WHEN sla_deadline < now() THEN 'expired'\n      WHEN sla_deadline < now() + INTERVAL '15 minutes' THEN 'near-expiry'\n      ELSE 'valid'\n    END AS ttl_state\n  FROM admin.approval;",
      "purpose_runtime": "FF Service sensitive.check.2b consumes this view to get ttl_state per approval_ref",
      "current_2b_behaviour": "computed in memory from approval_examples.json · see policy_engine.py:sensitive_check_2b"
    },
    {
      "name": "admin.audit",
      "purpose_en": "Audit trail · additive to the existing boundary-only emission · target for Phase 2c sink delivery wiring",
      "ddl": "CREATE TABLE admin.audit (\n  audit_id TEXT PRIMARY KEY,\n  event_type TEXT NOT NULL,\n  event_category TEXT NOT NULL,\n  actor TEXT NOT NULL,\n  auth_source TEXT,\n  target_type TEXT,\n  target_id TEXT,\n  approval_id TEXT REFERENCES admin.approval(approval_id) ON DELETE SET NULL,\n  before JSONB,\n  after JSONB,\n  ts TIMESTAMPTZ NOT NULL DEFAULT now(),\n  sink_status TEXT NOT NULL DEFAULT 'pending' CHECK (sink_status IN ('pending','shipped','failed')),\n  retry_count INTEGER NOT NULL DEFAULT 0\n);",
      "indexes": [
        "CREATE INDEX admin_audit_ts_desc_idx ON admin.audit (ts DESC);",
        "CREATE INDEX admin_audit_actor_ts_idx ON admin.audit (actor, ts DESC);",
        "CREATE INDEX admin_audit_approval_idx ON admin.audit (approval_id) WHERE approval_id IS NOT NULL;",
        "CREATE INDEX admin_audit_sink_pending_idx ON admin.audit (ts) WHERE sink_status='pending';",
        "CREATE INDEX admin_audit_event_type_idx ON admin.audit (event_type);"
      ],
      "event_types_covered": "all 20 entries in audit_event_model.base_shape (see audit_sink_contract.json · event_type_coverage)",
      "retention": "hot 90 days in PG · warm → Kafka ptt.audit.trail (7yr WORM retention per docs/CLAUDE.md)",
      "current_2b_behaviour": "no table · no writes · POST /api/policy/audit/preview only renders envelope · sink_status='deferred' returned in response"
    }
  ],
  "migration_ordering": [
    "1. admin.approval (seed from approval_examples.json)",
    "2. admin.signature (seed from embedded signature arrays)",
    "3. admin.approval_ttl_state (CREATE VIEW · no data)",
    "4. admin.audit (empty · starts populating on first 2c mutation)",
    "5. Run parity harness: given the same policy_request, 2b file-backed and 2c db-backed must produce the same sensitive.check.2b response"
  ],
  "cross_service_cross_reference": {
    "ff_consumes": [
      "admin.approval_ttl_state (via sensitive.check.2b endpoint · FF passes approval_ref)",
      "admin.approval (for approval_ref validity check)"
    ],
    "ff_emits_to_admin_audit": "FF mutation events go to admin.audit with event_category='flag_mutation' · shipped via boundary call (NOT direct DB write)",
    "boundary_principle": "FF and Admin do NOT share the same DB connection · Admin is authoritative on audit · FF ships events via HTTP POST to admin.audit.ingest (not wired in 2b+)"
  },
  "deferred_items": [
    { "item": "Alembic migration scripts under admin-control-plane-service/alembic/", "state": "not_wired" },
    { "item": "Admin DB pool in app.py", "state": "not_wired" },
    { "item": "admin.audit.ingest boundary endpoint", "state": "not_wired · planned 2c" },
    { "item": "Background TTL expiry sweep worker", "state": "not_wired · TTL computed at read time (passive)" },
    { "item": "Signer workflow UI mutations", "state": "not_in_scope_batch_4" }
  ],
  "pair_with": {
    "phase_2b_contract": "docs/runtime/admin-control-plane-service/phase_2b_contract.json",
    "approval_store_schema": "docs/runtime/admin-control-plane-service/approval_store_schema.json",
    "approval_examples": "docs/runtime/admin-control-plane-service/approval_examples.json",
    "approval_store_examples": "docs/runtime/admin-control-plane-service/approval_store_examples.json",
    "ttl_examples": "docs/runtime/admin-control-plane-service/ttl_examples.json",
    "audit_sink_contract": "docs/runtime/admin-control-plane-service/audit_sink_contract.json",
    "cutover_readiness_notes": "docs/runtime/admin-control-plane-service/cutover_readiness_notes.json",
    "ff_persistence_schema": "docs/runtime/feature-flags-service/persistence_schema.json"
  }
}
