AI Gateway Service — Data Model
Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · standards/NAMING.md
1. ID prefix registry (for DATA_MODEL authority)
| Prefix | Entity |
|---|---|
dec_ | AIDecision |
prv_ | AIProvenance |
prt_ | PromptTemplate |
rrl_ | ProviderRoutingRule |
mfd_ | ModerationFinding |
rev_ | DecisionReviewEvent |
att_ | ProviderAttempt |
qtw_ | QuotaWindow |
2. TypeScript interfaces
export type DecisionState = 'draft' | 'under_review' | 'accepted' | 'rejected' | 'archived';
export type Provider = 'anthropic' | 'openai' | 'azure_openai' | 'bedrock' | 'onprem_vllm' | 'ollama' | 'mock';
export type ModerationVerdict = 'allow' | 'flag' | 'block';
export interface AIDecision {
id: string; // dec_...
tenantId: string;
actorId: string;
consumerService: string; // e.g. "patient-chart-service"
featureKey: string;
resourceType: string;
nodeId: string | null;
state: DecisionState;
hitlRequired: boolean;
provenanceId: string; // prv_...
correlationId: string;
inputChars: number;
outputChars: number | null;
// PHI-safe: draft text encrypted at rest (optional, DPIA-gated)
draftTextEnc: Buffer | null;
version: number;
createdAt: Date;
updatedAt: Date;
archivedAt: Date | null;
}
export interface AIProvenance {
id: string; // prv_...
decisionId: string;
tenantId: string;
actorId: string;
provider: Provider;
modelVersion: string;
promptTemplateKey: string;
promptTemplateVersion: string;
promptTemplateHash: string;
guardrailsHash: string;
policyDecisionId: string | null;
moderationInput: ModerationVerdict;
moderationOutput: ModerationVerdict | null;
latencyMs: number;
requestedAt: Date;
completedAt: Date | null;
residency: string;
}
export interface ProviderAttempt {
id: string; // att_...
decisionId: string;
provider: Provider;
modelVersion: string;
outcome: 'success' | 'error' | 'timeout' | 'circuit_open';
errorCode: string | null;
latencyMs: number;
tokensPrompt: number | null;
tokensCompletion: number | null;
attemptedAt: Date;
}
export interface DecisionReviewEvent {
id: string; // rev_...
decisionId: string;
reviewerId: string;
verdict: 'accepted' | 'rejected' | 'commented';
comment: string | null;
editDiffHash: string | null;
createdAt: Date;
}
export interface ModerationFinding {
id: string; // mfd_...
decisionId: string;
stage: 'input' | 'output';
verdict: ModerationVerdict;
categories: Array<{ name: string; score: number; threshold: number }>;
classifierVersion: string;
createdAt: Date;
}
export interface PromptTemplate {
id: string; // prt_...
key: string;
version: string; // semver
tenantId: string | null; // null = global
template: string; // stored hashed; raw kept in secure registry
guardrails: string;
featureKeys: string[];
publishedBy: string;
publishedAt: Date;
status: 'draft' | 'published' | 'deprecated';
}
export interface ProviderRoutingRule {
id: string; // rrl_...
tenantId: string | null; // null = global
featureKey: string;
residency: string[]; // allowed residencies
providers: Array<{ provider: Provider; modelVersion: string; priority: number }>;
fallback: Array<{ provider: Provider; modelVersion: string }>;
version: number;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface QuotaWindow {
id: string; // qtw_...
tenantId: string;
featureKey: string;
windowStart: Date;
windowSec: number;
limit: number;
used: number;
}
3. Postgres schema
CREATE TABLE ai_decision (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
consumer_service TEXT NOT NULL,
feature_key TEXT NOT NULL,
resource_type TEXT NOT NULL,
node_id TEXT,
state TEXT NOT NULL,
hitl_required BOOLEAN NOT NULL DEFAULT FALSE,
provenance_id TEXT NOT NULL,
correlation_id UUID NOT NULL,
input_chars INT NOT NULL,
output_chars INT,
draft_text_enc BYTEA,
version INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
archived_at TIMESTAMPTZ
);
CREATE INDEX ix_ai_decision_tenant_state ON ai_decision(tenant_id, state, created_at DESC);
CREATE INDEX ix_ai_decision_feature ON ai_decision(tenant_id, feature_key, created_at DESC);
CREATE INDEX ix_ai_decision_correlation ON ai_decision(correlation_id);
CREATE TABLE ai_provenance (
id TEXT PRIMARY KEY,
decision_id TEXT NOT NULL REFERENCES ai_decision(id),
tenant_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
provider TEXT NOT NULL,
model_version TEXT NOT NULL,
prompt_template_key TEXT NOT NULL,
prompt_template_version TEXT NOT NULL,
prompt_template_hash TEXT NOT NULL,
guardrails_hash TEXT NOT NULL,
policy_decision_id TEXT,
moderation_input TEXT NOT NULL,
moderation_output TEXT,
latency_ms INT NOT NULL,
requested_at TIMESTAMPTZ NOT NULL,
completed_at TIMESTAMPTZ,
residency TEXT NOT NULL
);
-- provenance is append-only: no UPDATE, no DELETE
REVOKE UPDATE, DELETE ON ai_provenance FROM ai_gateway_app;
CREATE TABLE provider_attempt (
id TEXT PRIMARY KEY,
decision_id TEXT NOT NULL REFERENCES ai_decision(id),
provider TEXT NOT NULL,
model_version TEXT NOT NULL,
outcome TEXT NOT NULL,
error_code TEXT,
latency_ms INT NOT NULL,
tokens_prompt INT,
tokens_completion INT,
attempted_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX ix_provider_attempt_decision ON provider_attempt(decision_id);
CREATE TABLE decision_review_event (
id TEXT PRIMARY KEY,
decision_id TEXT NOT NULL REFERENCES ai_decision(id),
reviewer_id TEXT NOT NULL,
verdict TEXT NOT NULL,
comment TEXT,
edit_diff_hash TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE moderation_finding (
id TEXT PRIMARY KEY,
decision_id TEXT NOT NULL REFERENCES ai_decision(id),
stage TEXT NOT NULL,
verdict TEXT NOT NULL,
categories JSONB NOT NULL,
classifier_version TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE prompt_template (
id TEXT PRIMARY KEY,
key TEXT NOT NULL,
version TEXT NOT NULL,
tenant_id TEXT,
template_hash TEXT NOT NULL,
guardrails_hash TEXT NOT NULL,
feature_keys TEXT[] NOT NULL,
status TEXT NOT NULL,
published_by TEXT NOT NULL,
published_at TIMESTAMPTZ NOT NULL,
UNIQUE (key, version, tenant_id)
);
CREATE TABLE provider_routing_rule (
id TEXT PRIMARY KEY,
tenant_id TEXT,
feature_key TEXT NOT NULL,
residency TEXT[] NOT NULL,
providers JSONB NOT NULL,
fallback JSONB NOT NULL,
version INT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE quota_window (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
feature_key TEXT NOT NULL,
window_start TIMESTAMPTZ NOT NULL,
window_sec INT NOT NULL,
used INT NOT NULL DEFAULT 0,
UNIQUE(tenant_id, feature_key, window_start)
);
CREATE TABLE outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
CREATE INDEX ix_outbox_unpublished ON outbox(published_at) WHERE published_at IS NULL;
4. Row Level Security (RLS)
ALTER TABLE ai_decision ENABLE ROW LEVEL SECURITY;
CREATE POLICY ai_decision_tenant_isolation ON ai_decision
USING (tenant_id = current_setting('app.tenant_id', true));
ALTER TABLE ai_provenance ENABLE ROW LEVEL SECURITY;
CREATE POLICY ai_provenance_tenant_isolation ON ai_provenance
USING (tenant_id = current_setting('app.tenant_id', true));
-- repeat for provider_attempt, decision_review_event, moderation_finding, quota_window
Admin paths (platform admin) open a superuser DB session that bypasses RLS and sets app.tenant_id explicitly.
5. Encryption
| Data | Class | Mechanism |
|---|---|---|
draft_text_enc | Confidential (may contain PHI) | Envelope encryption via tenant KMS key, decrypted only when caller re-fetches and auth passes |
prompt_template.template_hash | Integrity | SHA-256 of raw template; raw body stored in secure registry outside app DB |
| Columns at rest | Default | AES-256 via Postgres TDE (operator level) |
6. Sizing
| Table | Growth estimate | Partitioning |
|---|---|---|
ai_decision | ~2M rows/month/tenant | monthly range partition on created_at |
ai_provenance | 1:1 with decision | monthly partition |
provider_attempt | ~1–3 per decision | monthly partition |
outbox | trimmed post-publish | none |