{
  "schema_version": "1.0",
  "baseline": "A-feature-flags-batch-4-readiness",
  "phase": "2b+ → 2c readiness",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "purpose_th": "DDL + index + FK + parity pointer · target schema สำหรับ FF Service · ใช้เป็น conformable spec สำหรับ Phase 2c alembic migration",
  "purpose_en": "Canonical target schema (PostgreSQL 15+) for FF Service persistence. Documents DDL, indexes, FK constraints, migration ordering, and parity pointers to the existing file-backed store. Phase 2b+ ships NO database. This document is the target Phase 2c must conform to.",
  "honest_note": "No migrations have run. No schema is deployed. docker/ai/postgres/init/ currently contains knowledge-schema.sql (pgvector) and timescale-schema.sql only — no FF tables. Adding ff_*.sql is a Phase 2c task, tracked in cutover_readiness_checklist.json.",
  "target_engine": "PostgreSQL 15+",
  "target_schema_name": "ff",
  "creation_order_rationale": "flag_registry first · overrides depend on flag_registry via FK · audit last (references all)",
  "tables": [
    {
      "name": "ff.flag_registry",
      "purpose_en": "Master list of flags · mirrors current docs/runtime/feature-flags/registry.json",
      "ddl": "CREATE TABLE ff.flag_registry (\n  flag_id TEXT PRIMARY KEY,\n  default_enabled BOOLEAN NOT NULL DEFAULT FALSE,\n  sensitive BOOLEAN NOT NULL DEFAULT FALSE,\n  requires_approval BOOLEAN NOT NULL DEFAULT FALSE,\n  approval_matrix_row TEXT NULL,\n  description TEXT,\n  owner TEXT,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);",
      "indexes": [
        "CREATE INDEX ff_flag_registry_sensitive_idx ON ff.flag_registry (sensitive) WHERE sensitive=true;",
        "CREATE INDEX ff_flag_registry_requires_approval_idx ON ff.flag_registry (requires_approval) WHERE requires_approval=true;"
      ],
      "file_backed_equivalent": "docs/runtime/feature-flags/registry.json",
      "mutability_2b_plus": "rare · manual edits",
      "mutability_2c_target": "admin-gated · approval-required for sensitive flag toggles"
    },
    {
      "name": "ff.tenant_override",
      "purpose_en": "Per-tenant override · keyed by (tenant_id, flag_id)",
      "ddl": "CREATE TABLE ff.tenant_override (\n  tenant_id TEXT NOT NULL,\n  flag_id TEXT NOT NULL REFERENCES ff.flag_registry(flag_id) ON DELETE CASCADE,\n  enabled BOOLEAN NOT NULL,\n  reason TEXT,\n  expires_at TIMESTAMPTZ NULL,\n  created_by TEXT NOT NULL,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  approval_ref TEXT NULL,\n  PRIMARY KEY (tenant_id, flag_id)\n);",
      "indexes": [
        "CREATE INDEX ff_tenant_override_tenant_idx ON ff.tenant_override (tenant_id);",
        "CREATE INDEX ff_tenant_override_expires_idx ON ff.tenant_override (expires_at) WHERE expires_at IS NOT NULL;",
        "CREATE INDEX ff_tenant_override_approval_idx ON ff.tenant_override (approval_ref) WHERE approval_ref IS NOT NULL;"
      ],
      "file_backed_equivalent": "docs/runtime/feature-flags-service/override_examples.json · rows with scope=tenant",
      "mutability_2b_plus": "dev-only writes (same JSON file in place)",
      "mutability_2c_target": "mutation via PUT /v1/flags/override/tenant/{tenant_id}/{flag_id} · DB-backed · approval_ref required if flag.requires_approval=true"
    },
    {
      "name": "ff.user_override",
      "purpose_en": "Per-user override · keyed by (user_id, flag_id) · higher precedence than tenant",
      "ddl": "CREATE TABLE ff.user_override (\n  user_id TEXT NOT NULL,\n  flag_id TEXT NOT NULL REFERENCES ff.flag_registry(flag_id) ON DELETE CASCADE,\n  enabled BOOLEAN NOT NULL,\n  reason TEXT,\n  expires_at TIMESTAMPTZ NULL,\n  created_by TEXT NOT NULL,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n  approval_ref TEXT NULL,\n  PRIMARY KEY (user_id, flag_id)\n);",
      "indexes": [
        "CREATE INDEX ff_user_override_user_idx ON ff.user_override (user_id);",
        "CREATE INDEX ff_user_override_expires_idx ON ff.user_override (expires_at) WHERE expires_at IS NOT NULL;",
        "CREATE INDEX ff_user_override_approval_idx ON ff.user_override (approval_ref) WHERE approval_ref IS NOT NULL;"
      ],
      "file_backed_equivalent": "docs/runtime/feature-flags-service/override_examples.json · rows with scope=user",
      "precedence": "higher than tenant_override (see evaluator.py:resolve_precedence)"
    },
    {
      "name": "ff.audit",
      "purpose_en": "Append-only audit of mutations · mirrors audit_event_model envelope shape from Admin 2b",
      "ddl": "CREATE TABLE ff.audit (\n  audit_id TEXT PRIMARY KEY,\n  event_type TEXT NOT NULL,\n  actor TEXT NOT NULL,\n  flag_id TEXT,\n  tenant_id TEXT,\n  user_id TEXT,\n  before JSONB,\n  after JSONB,\n  approval_ref TEXT,\n  auth_source TEXT,\n  ts TIMESTAMPTZ NOT NULL DEFAULT now(),\n  sink_status TEXT NOT NULL DEFAULT 'pending'\n);",
      "indexes": [
        "CREATE INDEX ff_audit_ts_desc_idx ON ff.audit (ts DESC);",
        "CREATE INDEX ff_audit_actor_ts_idx ON ff.audit (actor, ts DESC);",
        "CREATE INDEX ff_audit_flag_ts_idx ON ff.audit (flag_id, ts DESC);",
        "CREATE INDEX ff_audit_sink_status_idx ON ff.audit (sink_status) WHERE sink_status='pending';"
      ],
      "retention": "hot: 90 days in PG · warm: shipped to audit_sink per audit_sink_contract.json (Admin 2b) for 7-year WORM retention",
      "relation": "FF emits audit rows with sink_status='pending' · downstream worker ships to Admin 2b sink · marks 'shipped' on ack · on failure marks 'failed' with retry_count",
      "2b_plus_current_state": "NO table · NO writes · audit emission is boundary-only (the shape is honored but no persisted trail)"
    }
  ],
  "logical_entities": {
    "backing_state_values": ["file_backed", "db_backed", "dual_write_parity", "db_authoritative"],
    "current_default": "file_backed",
    "cutover_target": "db_authoritative after parity window"
  },
  "migration_ordering": [
    "1. ff.flag_registry (seed from registry.json)",
    "2. ff.tenant_override (seed from override_examples.json · scope=tenant)",
    "3. ff.user_override (seed from override_examples.json · scope=user)",
    "4. ff.audit (empty · starts populating on first mutation)",
    "5. CREATE INDEXES in order listed per table",
    "6. Analyze + vacuum after seed",
    "7. Run parity harness · file-backed vs db-backed · 100% match required before flipping FF_BACKING=db"
  ],
  "alembic_notes": {
    "planned_location": "docs/runtime/feature-flags-service/alembic/versions/ (not created in 2b+)",
    "env_var": "FF_DATABASE_URL=postgresql://...",
    "connection_pool": "SQLAlchemy + asyncpg · pool_size=5 · max_overflow=10 · pool_pre_ping=true",
    "current_state": "no alembic directory · no DB client in app.py"
  },
  "fk_and_cascade_rules": {
    "on_delete_flag_registry": "CASCADE to tenant_override + user_override (overrides lose their flag)",
    "on_delete_override": "NO cascade to audit (audit keeps history of removed overrides)",
    "on_update_flag_id": "restrict · IDs should be stable"
  },
  "deferred_items": [
    { "item": "Alembic migration scripts", "state": "not_wired" },
    { "item": "asyncpg connection pool in app.py", "state": "not_wired" },
    { "item": "Read replica config", "state": "not_planned_yet" },
    { "item": "Sharding strategy", "state": "not_needed_under_expected_load" },
    { "item": "Multi-region replication", "state": "out_of_scope" },
    { "item": "Audit shipment worker", "state": "not_wired" }
  ],
  "pair_with": {
    "persistence_backend_notes": "docs/runtime/feature-flags-service/persistence_backend_notes.json",
    "override_store_schema": "docs/runtime/feature-flags-service/override_store_schema.json",
    "override_examples": "docs/runtime/feature-flags-service/override_examples.json",
    "cutover_parity_matrix": "docs/runtime/feature-flags-service/cutover_parity_matrix.json",
    "cutover_readiness_checklist": "docs/runtime/feature-flags-service/cutover_readiness_checklist.json",
    "registry": "docs/runtime/feature-flags/registry.json",
    "admin_persistence_schema": "docs/runtime/admin-control-plane-service/persistence_schema.json"
  }
}
