Document Service — Data Model
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD
1. ID Prefix Registry
| Prefix | Entity |
|---|---|
tmpl_ | DocumentTemplate |
tv_ | DocumentTemplateVersion |
rjob_ | DocumentRenderJob |
upl_ | DocumentUpload (transient) |
All IDs are ULIDs stored as text in PostgreSQL.
2. TypeScript Interfaces
// src/domain/document-template.ts
export type TemplateId = Branded<string, 'TemplateId'>;
export enum TemplateStatus {
Draft = 'draft',
Published = 'published',
Retired = 'retired',
}
export enum DocumentCategory {
Prescription = 'prescription',
LabRequisition = 'lab_requisition',
Report = 'report',
Letter = 'letter',
Consent = 'consent',
DischargeSummary = 'discharge_summary',
Referral = 'referral',
ScannedRecord = 'scanned_record',
Other = 'other',
}
export enum DocumentOrigin {
Platform = 'platform',
Tenant = 'tenant',
}
export interface DocumentTemplate {
id: TemplateId;
tenantId: TenantId;
name: string;
category: DocumentCategory;
facilityId: string | null;
status: TemplateStatus;
currentVersionId: TemplateVersionId | null;
origin: DocumentOrigin;
platformFormKey: string | null;
forkedFromPlatformFormKey: string | null;
createdBy: UserId;
updatedAt: Date;
}
// src/domain/document-template-version.ts
export type TemplateVersionId = Branded<string, 'TemplateVersionId'>;
export interface DocumentTemplateVersion {
id: TemplateVersionId;
templateId: TemplateId;
tenantId: TenantId;
semver: string;
definition: Record<string, unknown>;
effectiveFrom: Date;
effectiveTo: Date | null;
publishedAt: Date | null;
checksum: string;
platformPackageVersion: string | null;
}
// src/domain/document-render-job.ts
export type RenderJobId = Branded<string, 'RenderJobId'>;
export enum RenderJobStatus {
Queued = 'queued',
Running = 'running',
Completed = 'completed',
Failed = 'failed',
}
export interface DocumentRenderJob {
id: RenderJobId;
tenantId: TenantId;
templateVersionId: TemplateVersionId;
patientId: string;
encounterId: string | null;
context: { resourceType: string; id: string };
status: RenderJobStatus;
inputSnapshotHash: string;
binaryId: string | null;
documentReferenceId: string | null;
errorCode: string | null;
clientMutationId: string | null;
createdAt: Date;
completedAt: Date | null;
}
3. PostgreSQL Schema
-- document_templates
CREATE TABLE document_templates (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
category TEXT NOT NULL,
facility_id TEXT,
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft','published','retired')),
current_version_id TEXT,
origin TEXT NOT NULL DEFAULT 'tenant'
CHECK (origin IN ('platform','tenant')),
platform_form_key TEXT,
forked_from_platform_form_key TEXT,
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, name)
);
-- document_template_versions
CREATE TABLE document_template_versions (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL REFERENCES document_templates(id),
tenant_id TEXT NOT NULL,
semver TEXT NOT NULL,
definition JSONB NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE,
published_at TIMESTAMPTZ,
checksum TEXT NOT NULL,
platform_package_version TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (template_id, semver)
);
-- document_render_jobs
CREATE TABLE document_render_jobs (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
template_version_id TEXT NOT NULL REFERENCES document_template_versions(id),
patient_id TEXT NOT NULL,
encounter_id TEXT,
context JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'queued'
CHECK (status IN ('queued','running','completed','failed')),
input_snapshot_hash TEXT NOT NULL,
binary_id TEXT,
document_reference_id TEXT,
error_code TEXT,
client_mutation_id TEXT,
locale TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
completed_at TIMESTAMPTZ
);
-- document_uploads (transient; cleaned up after finalize)
CREATE TABLE document_uploads (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
patient_id TEXT NOT NULL,
encounter_id TEXT,
category TEXT NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL,
staged_key TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','scanning','clean','quarantined')),
document_reference_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL
);
-- outbox (transactional outbox pattern)
CREATE TABLE outbox (
id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- inbox (deduplication)
CREATE TABLE inbox (
event_id TEXT PRIMARY KEY,
source TEXT NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
4. Indexes
CREATE INDEX ix_document_templates_tenant_status ON document_templates(tenant_id, status);
CREATE INDEX ix_document_templates_platform_key ON document_templates(platform_form_key) WHERE platform_form_key IS NOT NULL;
CREATE INDEX ix_document_template_versions_template ON document_template_versions(template_id, published_at);
CREATE INDEX ix_render_jobs_tenant_status ON document_render_jobs(tenant_id, status);
CREATE INDEX ix_render_jobs_patient ON document_render_jobs(tenant_id, patient_id);
CREATE INDEX ix_render_jobs_client_mutation ON document_render_jobs(tenant_id, client_mutation_id) WHERE client_mutation_id IS NOT NULL;
CREATE INDEX ix_document_uploads_tenant_status ON document_uploads(tenant_id, status);
CREATE INDEX ix_outbox_undelivered ON outbox(created_at) WHERE delivered_at IS NULL;
5. Row-Level Security
ALTER TABLE document_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_template_versions ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_render_jobs ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_uploads ENABLE ROW LEVEL SECURITY;
CREATE POLICY document_templates_tenant_isolation ON document_templates
USING (tenant_id = current_setting('app.current_tenant_id'));
CREATE POLICY document_template_versions_tenant_isolation ON document_template_versions
USING (tenant_id = current_setting('app.current_tenant_id'));
CREATE POLICY document_render_jobs_tenant_isolation ON document_render_jobs
USING (tenant_id = current_setting('app.current_tenant_id'));
CREATE POLICY document_uploads_tenant_isolation ON document_uploads
USING (tenant_id = current_setting('app.current_tenant_id'));
6. Object Storage Layout
/{tenantId}/
documents/
{documentId}.pdf ← finalized artifacts
quarantine/
{uploadId} ← pending virus scan
quarantine-infected/
{uploadId} ← failed virus scan; retained per policy
Object tags: { tenantId, documentReferenceId, contentType, origin }
All objects encrypted at rest (AES-256 SSE-S3 or SSE-KMS per deployment).
7. Migrations
Migration files in src/migrations/:
YYYYMMDDHHMMSS_{description}.sql
synchronize: false — all schema changes via explicit migrations.