Skip to main content

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)

PrefixEntity
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

DataClassMechanism
draft_text_encConfidential (may contain PHI)Envelope encryption via tenant KMS key, decrypted only when caller re-fetches and auth passes
prompt_template.template_hashIntegritySHA-256 of raw template; raw body stored in secure registry outside app DB
Columns at restDefaultAES-256 via Postgres TDE (operator level)

6. Sizing

TableGrowth estimatePartitioning
ai_decision~2M rows/month/tenantmonthly range partition on created_at
ai_provenance1:1 with decisionmonthly partition
provider_attempt~1–3 per decisionmonthly partition
outboxtrimmed post-publishnone