Original/Copy/Amendment Tracking System
Executive Summary
This document defines the comprehensive system for tracking original records, true copies, certified copies, and amendments within the BIO-QMS platform, ensuring full compliance with FDA 21 CFR Part 11 requirements for electronic records in regulated biomedical and pharmaceutical environments.
Regulatory Context:
- 21 CFR 11.10(a): System validation to ensure accuracy, reliability, and performance
- 21 CFR 11.10(e): Documentation of record copies as true copies
- 21 CFR 11.10(k)(1): Records retained in non-rewritable, non-erasable form
- 21 CFR 11.50: Signed records contain signature manifestations
Key Capabilities:
- Immutable original record identification with cryptographic fingerprinting
- Verified true copy generation with automatic hash validation
- Certified copy workflow with qualified e-signatures
- Complete amendment chain with before/after diff tracking
- Comprehensive chain of custody logging
- Version control with semantic versioning
1. Original Record Identification
1.1 Data Model
TypeScript Interface
/**
* Original record metadata and identification
* Compliance: 21 CFR 11.10(k)(1) - non-rewritable original identification
*/
interface OriginalRecord {
// Immutable identifiers
original_id: string; // UUID v7 (time-ordered)
record_status: RecordStatus; // Enum, set once at creation
is_original: boolean; // Immutable flag, always true for originals
// Creation metadata
created_by: string; // User ID
created_at: Date; // ISO 8601 timestamp (UTC)
created_from_ip: string; // IPv4/IPv6 address
created_from_device: string; // Device fingerprint
// System context
system_version: string; // Semantic version (e.g., "2.3.1")
module: string; // Module name (e.g., "capa", "deviation")
record_type: string; // Type identifier (e.g., "CAPA_RECORD")
tenant_id: string; // Multi-tenant isolation
// Cryptographic integrity
content_hash: string; // SHA-256 of record content at creation
hash_algorithm: 'SHA-256'; // Algorithm identifier
hash_timestamp: Date; // When hash was computed
// Content snapshot
original_content: JSONB; // Immutable copy of initial state
content_schema_version: string; // Schema version for future migrations
// Audit metadata
regulatory_context: RegulatoryContext;
retention_policy: RetentionPolicy;
classification: DataClassification;
}
enum RecordStatus {
ORIGINAL = 'ORIGINAL',
TRUE_COPY = 'TRUE_COPY',
CERTIFIED_COPY = 'CERTIFIED_COPY',
AMENDMENT = 'AMENDMENT'
}
interface RegulatoryContext {
applicable_regulations: string[]; // e.g., ["21_CFR_PART_11", "ISO_13485"]
regulatory_event_type?: string; // e.g., "PRODUCT_RELEASE", "AUDIT"
gxp_category: 'GMP' | 'GLP' | 'GCP' | 'GDP';
}
interface RetentionPolicy {
retention_period_years: number; // Minimum retention (e.g., 7 years)
destruction_date?: Date; // Calculated destruction eligibility
legal_hold: boolean; // Litigation/investigation hold
retention_basis: string; // Regulatory citation
}
enum DataClassification {
CRITICAL_QUALITY = 'CRITICAL_QUALITY',
QUALITY_IMPACT = 'QUALITY_IMPACT',
NON_QUALITY = 'NON_QUALITY'
}
PostgreSQL Schema
-- Original records table
-- Compliance: 21 CFR 11.10(k)(1) - non-rewritable records
CREATE TABLE original_records (
-- Primary identifiers
original_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
record_status VARCHAR(20) NOT NULL CHECK (record_status IN ('ORIGINAL', 'TRUE_COPY', 'CERTIFIED_COPY', 'AMENDMENT')),
is_original BOOLEAN NOT NULL DEFAULT TRUE,
-- Creation metadata
created_by UUID NOT NULL REFERENCES users(user_id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_from_ip INET NOT NULL,
created_from_device VARCHAR(255),
-- System context
system_version VARCHAR(20) NOT NULL,
module VARCHAR(50) NOT NULL,
record_type VARCHAR(100) NOT NULL,
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Cryptographic integrity
content_hash VARCHAR(64) NOT NULL, -- SHA-256 = 64 hex chars
hash_algorithm VARCHAR(20) NOT NULL DEFAULT 'SHA-256',
hash_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Content snapshot (immutable)
original_content JSONB NOT NULL,
content_schema_version VARCHAR(20) NOT NULL,
-- Regulatory metadata
applicable_regulations TEXT[] NOT NULL,
regulatory_event_type VARCHAR(100),
gxp_category VARCHAR(10) NOT NULL CHECK (gxp_category IN ('GMP', 'GLP', 'GCP', 'GDP')),
-- Retention policy
retention_period_years INTEGER NOT NULL,
destruction_date DATE,
legal_hold BOOLEAN NOT NULL DEFAULT FALSE,
retention_basis TEXT NOT NULL,
-- Data classification
data_classification VARCHAR(30) NOT NULL CHECK (data_classification IN ('CRITICAL_QUALITY', 'QUALITY_IMPACT', 'NON_QUALITY')),
-- Audit trail
created_at_server TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_accessed_at TIMESTAMPTZ,
access_count INTEGER NOT NULL DEFAULT 0,
-- Constraints
CONSTRAINT original_status_check CHECK (
(is_original = TRUE AND record_status = 'ORIGINAL') OR
(is_original = FALSE AND record_status != 'ORIGINAL')
)
);
-- Immutability enforcement via trigger
CREATE OR REPLACE FUNCTION enforce_original_immutability()
RETURNS TRIGGER AS $$
BEGIN
-- Allow INSERT only
IF TG_OP = 'UPDATE' THEN
-- Only allow updates to audit fields
IF OLD.original_id IS DISTINCT FROM NEW.original_id OR
OLD.record_status IS DISTINCT FROM NEW.record_status OR
OLD.is_original IS DISTINCT FROM NEW.is_original OR
OLD.content_hash IS DISTINCT FROM NEW.content_hash OR
OLD.original_content IS DISTINCT FROM NEW.original_content THEN
RAISE EXCEPTION 'Original record fields are immutable (21 CFR 11.10(k)(1))';
END IF;
END IF;
IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'Original records cannot be deleted (21 CFR 11.10(k)(1))';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_original_immutability_trigger
BEFORE UPDATE OR DELETE ON original_records
FOR EACH ROW EXECUTE FUNCTION enforce_original_immutability();
-- Indexes for performance
CREATE INDEX idx_original_records_created_by ON original_records(created_by);
CREATE INDEX idx_original_records_created_at ON original_records(created_at);
CREATE INDEX idx_original_records_tenant ON original_records(tenant_id);
CREATE INDEX idx_original_records_module_type ON original_records(module, record_type);
CREATE INDEX idx_original_records_hash ON original_records(content_hash);
-- GIN index for JSONB content search
CREATE INDEX idx_original_records_content_gin ON original_records USING GIN(original_content);
1.2 Hash Generation
/**
* Generate cryptographic hash of record content
* Compliance: 21 CFR 11.10(k)(1) - integrity verification
*/
import crypto from 'crypto';
interface HashableContent {
[key: string]: any;
}
export class RecordHashGenerator {
private readonly algorithm = 'sha256';
/**
* Generate deterministic SHA-256 hash of record content
* Normalizes JSON to ensure consistent ordering
*/
generateHash(content: HashableContent): string {
// Normalize JSON (sort keys, remove whitespace)
const normalized = this.normalizeContent(content);
// Generate hash
const hash = crypto
.createHash(this.algorithm)
.update(normalized)
.digest('hex');
return hash;
}
/**
* Normalize content for deterministic hashing
*/
private normalizeContent(content: HashableContent): string {
// Sort keys recursively, stringify without whitespace
return JSON.stringify(content, Object.keys(content).sort());
}
/**
* Verify content against existing hash
*/
verifyHash(content: HashableContent, expectedHash: string): boolean {
const actualHash = this.generateHash(content);
return actualHash === expectedHash;
}
/**
* Generate hash with metadata
*/
generateHashWithMetadata(content: HashableContent): {
hash: string;
algorithm: string;
timestamp: Date;
} {
return {
hash: this.generateHash(content),
algorithm: this.algorithm.toUpperCase(),
timestamp: new Date()
};
}
}
1.3 API Endpoints
/**
* Original record creation API
* POST /api/v1/records/original
*/
interface CreateOriginalRecordRequest {
module: string;
record_type: string;
content: HashableContent;
regulatory_context: RegulatoryContext;
retention_policy: RetentionPolicy;
data_classification: DataClassification;
}
interface CreateOriginalRecordResponse {
original_id: string;
content_hash: string;
created_at: Date;
record_status: 'ORIGINAL';
}
/**
* Original record retrieval
* GET /api/v1/records/original/:original_id
*/
interface GetOriginalRecordResponse extends OriginalRecord {
current_copy_count: number;
current_amendment_count: number;
latest_amendment_version?: string;
}
2. True Copy Generation
2.1 Data Model
TypeScript Interface
/**
* True copy metadata
* Compliance: 21 CFR 11.10(e) - documentation of copies as true copies
*/
interface TrueCopy {
// Copy identifiers
copy_id: string; // UUID v7
source_original_id: string; // Reference to original
copy_number: number; // Sequential per original
record_status: RecordStatus.TRUE_COPY;
// Copy generation metadata
copy_created_at: Date;
copy_created_by: string; // User ID
copy_purpose: CopyPurpose;
copy_destination?: string; // e.g., "Regulatory Submission Package"
// Verification
verification_hash: string; // Must match original content_hash
verification_status: VerificationStatus;
verification_timestamp: Date;
hash_match_confirmed: boolean;
// Copy content
copy_content: JSONB; // Exact replica of original_content
// Watermark/distinguishing marks
watermark_text: string; // "TRUE COPY - NOT ORIGINAL"
copy_metadata: CopyMetadata;
// Audit
accessed_count: number;
last_accessed_at?: Date;
exported: boolean;
export_timestamp?: Date;
}
enum CopyPurpose {
REGULATORY_SUBMISSION = 'REGULATORY_SUBMISSION',
EXTERNAL_AUDIT = 'EXTERNAL_AUDIT',
INTERNAL_REVIEW = 'INTERNAL_REVIEW',
BACKUP = 'BACKUP',
ARCHIVAL = 'ARCHIVAL',
LEGAL_DISCOVERY = 'LEGAL_DISCOVERY'
}
enum VerificationStatus {
PENDING = 'PENDING',
VERIFIED = 'VERIFIED',
FAILED = 'FAILED'
}
interface CopyMetadata {
original_created_at: Date;
original_created_by: string;
original_module: string;
original_record_type: string;
copy_generation_method: 'AUTOMATIC' | 'MANUAL_REQUEST';
verification_algorithm: string;
}
PostgreSQL Schema
-- True copies table
CREATE TABLE true_copies (
-- Primary identifiers
copy_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_original_id UUID NOT NULL REFERENCES original_records(original_id),
copy_number INTEGER NOT NULL,
record_status VARCHAR(20) NOT NULL DEFAULT 'TRUE_COPY',
-- Copy generation metadata
copy_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
copy_created_by UUID NOT NULL REFERENCES users(user_id),
copy_purpose VARCHAR(50) NOT NULL,
copy_destination VARCHAR(255),
-- Verification
verification_hash VARCHAR(64) NOT NULL,
verification_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
verification_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
hash_match_confirmed BOOLEAN NOT NULL,
-- Copy content
copy_content JSONB NOT NULL,
-- Watermark
watermark_text TEXT NOT NULL DEFAULT 'TRUE COPY - NOT ORIGINAL',
copy_metadata JSONB NOT NULL,
-- Audit
accessed_count INTEGER NOT NULL DEFAULT 0,
last_accessed_at TIMESTAMPTZ,
exported BOOLEAN NOT NULL DEFAULT FALSE,
export_timestamp TIMESTAMPTZ,
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Constraints
UNIQUE(source_original_id, copy_number),
CHECK (record_status = 'TRUE_COPY'),
CHECK (verification_status IN ('PENDING', 'VERIFIED', 'FAILED'))
);
-- Auto-increment copy_number per original
CREATE OR REPLACE FUNCTION set_copy_number()
RETURNS TRIGGER AS $$
BEGIN
SELECT COALESCE(MAX(copy_number), 0) + 1
INTO NEW.copy_number
FROM true_copies
WHERE source_original_id = NEW.source_original_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_copy_number_trigger
BEFORE INSERT ON true_copies
FOR EACH ROW
WHEN (NEW.copy_number IS NULL)
EXECUTE FUNCTION set_copy_number();
-- Indexes
CREATE INDEX idx_true_copies_source ON true_copies(source_original_id);
CREATE INDEX idx_true_copies_created_at ON true_copies(copy_created_at);
CREATE INDEX idx_true_copies_created_by ON true_copies(copy_created_by);
CREATE INDEX idx_true_copies_tenant ON true_copies(tenant_id);
2.2 True Copy Generation Logic
/**
* True copy generation service
* Compliance: 21 CFR 11.10(e) - true copy documentation
*/
export class TrueCopyService {
constructor(
private hashGenerator: RecordHashGenerator,
private auditLogger: AuditLogger
) {}
/**
* Generate verified true copy of original record
*/
async generateTrueCopy(request: {
original_id: string;
purpose: CopyPurpose;
destination?: string;
created_by: string;
}): Promise<TrueCopy> {
// 1. Fetch original record
const original = await this.fetchOriginal(request.original_id);
// 2. Verify original integrity
const hashValid = this.hashGenerator.verifyHash(
original.original_content,
original.content_hash
);
if (!hashValid) {
throw new Error('Original record integrity check failed - hash mismatch');
}
// 3. Generate copy hash from original content
const copyHash = this.hashGenerator.generateHash(original.original_content);
// 4. Verify copy hash matches original
const hashMatch = copyHash === original.content_hash;
// 5. Create true copy record
const trueCopy: TrueCopy = {
copy_id: this.generateUUID(),
source_original_id: original.original_id,
copy_number: await this.getNextCopyNumber(original.original_id),
record_status: RecordStatus.TRUE_COPY,
copy_created_at: new Date(),
copy_created_by: request.created_by,
copy_purpose: request.purpose,
copy_destination: request.destination,
verification_hash: copyHash,
verification_status: hashMatch ? VerificationStatus.VERIFIED : VerificationStatus.FAILED,
verification_timestamp: new Date(),
hash_match_confirmed: hashMatch,
copy_content: original.original_content, // Exact replica
watermark_text: 'TRUE COPY - NOT ORIGINAL',
copy_metadata: {
original_created_at: original.created_at,
original_created_by: original.created_by,
original_module: original.module,
original_record_type: original.record_type,
copy_generation_method: 'AUTOMATIC',
verification_algorithm: 'SHA-256'
},
accessed_count: 0,
exported: false
};
// 6. Persist true copy
await this.saveTrueCopy(trueCopy);
// 7. Audit log
await this.auditLogger.log({
event_type: 'TRUE_COPY_GENERATED',
original_id: original.original_id,
copy_id: trueCopy.copy_id,
user_id: request.created_by,
verification_status: trueCopy.verification_status,
purpose: request.purpose
});
return trueCopy;
}
/**
* Batch true copy generation
*/
async generateBatchTrueCopies(request: {
original_ids: string[];
purpose: CopyPurpose;
destination?: string;
created_by: string;
}): Promise<TrueCopy[]> {
const copies = await Promise.all(
request.original_ids.map(original_id =>
this.generateTrueCopy({
original_id,
purpose: request.purpose,
destination: request.destination,
created_by: request.created_by
})
)
);
return copies;
}
/**
* Export true copy package (e.g., for regulatory submission)
*/
async exportTrueCopyPackage(request: {
copy_ids: string[];
package_format: 'PDF' | 'ZIP' | 'XML';
include_metadata: boolean;
}): Promise<Blob> {
// Implementation for export package generation
// Include watermarks, metadata, verification certificates
throw new Error('Not implemented');
}
}
2.3 API Endpoints
/**
* POST /api/v1/records/true-copy
*/
interface GenerateTrueCopyRequest {
original_id: string;
purpose: CopyPurpose;
destination?: string;
}
interface GenerateTrueCopyResponse {
copy_id: string;
copy_number: number;
verification_status: VerificationStatus;
hash_match_confirmed: boolean;
copy_created_at: Date;
}
/**
* POST /api/v1/records/true-copy/batch
*/
interface GenerateBatchTrueCopiesRequest {
original_ids: string[];
purpose: CopyPurpose;
destination?: string;
}
interface GenerateBatchTrueCopiesResponse {
copies: GenerateTrueCopyResponse[];
total_count: number;
successful_count: number;
failed_count: number;
}
3. Certified Copy Workflow
3.1 Data Model
TypeScript Interface
/**
* Certified copy with qualified e-signature
* Compliance: 21 CFR 11.50 - signature manifestations
*/
interface CertifiedCopy extends TrueCopy {
record_status: RecordStatus.CERTIFIED_COPY;
// Certification metadata
certification_id: string;
certifier_id: string; // Must have Compliance Officer role
certification_date: Date;
certification_purpose: string; // Free text, required
// E-signature
e_signature: ESignature;
signature_manifestation: SignatureManifestation;
// Certification statement
certification_statement: string; // Template-based
certification_template_id: string;
// Certificate of authenticity
certificate_id: string;
certificate_generated_at: Date;
certificate_pdf_url: string; // S3/blob storage URL
// Workflow tracking
workflow_status: CertificationWorkflowStatus;
workflow_initiated_at: Date;
workflow_completed_at?: Date;
approval_chain: ApprovalStep[];
}
interface ESignature {
signature_id: string;
signer_id: string;
signer_name: string;
signer_role: string;
signature_timestamp: Date;
signature_meaning: SignatureMeaning;
signature_method: 'PASSWORD' | 'BIOMETRIC' | 'TOKEN' | 'PKI';
password_hash?: string; // If password-based
biometric_hash?: string; // If biometric
certificate_serial?: string; // If PKI
}
enum SignatureMeaning {
CERTIFIED_AS_TRUE_COPY = 'CERTIFIED_AS_TRUE_COPY',
APPROVED = 'APPROVED',
REVIEWED = 'REVIEWED',
WITNESSED = 'WITNESSED'
}
interface SignatureManifestation {
// 21 CFR 11.50 required elements
printed_name: string;
signature_date: Date;
signature_meaning_text: string; // Human-readable
// Additional context
signature_reason: string;
signature_location?: string; // e.g., "Quality Assurance Department"
}
enum CertificationWorkflowStatus {
INITIATED = 'INITIATED',
PENDING_REVIEW = 'PENDING_REVIEW',
PENDING_APPROVAL = 'PENDING_APPROVAL',
CERTIFIED = 'CERTIFIED',
REJECTED = 'REJECTED'
}
interface ApprovalStep {
step_number: number;
step_type: 'REVIEW' | 'APPROVE' | 'CERTIFY';
assigned_to: string; // User ID or role
completed_by?: string;
completed_at?: Date;
decision: 'PENDING' | 'APPROVED' | 'REJECTED';
comments?: string;
e_signature?: ESignature;
}
PostgreSQL Schema
-- Certified copies table
CREATE TABLE certified_copies (
-- Inherits from true_copies structure
copy_id UUID PRIMARY KEY REFERENCES true_copies(copy_id),
-- Certification metadata
certification_id UUID NOT NULL DEFAULT gen_random_uuid(),
certifier_id UUID NOT NULL REFERENCES users(user_id),
certification_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
certification_purpose TEXT NOT NULL,
-- E-signature
e_signature JSONB NOT NULL,
signature_manifestation JSONB NOT NULL,
-- Certification statement
certification_statement TEXT NOT NULL,
certification_template_id UUID REFERENCES certification_templates(template_id),
-- Certificate of authenticity
certificate_id UUID NOT NULL DEFAULT gen_random_uuid(),
certificate_generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
certificate_pdf_url TEXT,
-- Workflow tracking
workflow_status VARCHAR(30) NOT NULL DEFAULT 'INITIATED',
workflow_initiated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
workflow_completed_at TIMESTAMPTZ,
approval_chain JSONB NOT NULL DEFAULT '[]'::jsonb,
-- Constraints
CHECK (workflow_status IN ('INITIATED', 'PENDING_REVIEW', 'PENDING_APPROVAL', 'CERTIFIED', 'REJECTED'))
);
-- Certification templates
CREATE TABLE certification_templates (
template_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
template_name VARCHAR(255) NOT NULL,
regulation VARCHAR(100) NOT NULL, -- e.g., "FDA_21_CFR_PART_11"
template_text TEXT NOT NULL, -- With placeholders like {{certifier_name}}
variables JSONB NOT NULL, -- List of available placeholders
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES users(user_id),
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id)
);
-- E-signatures table (for audit trail)
CREATE TABLE e_signatures (
signature_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
signer_id UUID NOT NULL REFERENCES users(user_id),
signature_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
signature_meaning VARCHAR(50) NOT NULL,
signature_method VARCHAR(20) NOT NULL,
password_hash VARCHAR(255),
biometric_hash VARCHAR(255),
certificate_serial VARCHAR(255),
-- What was signed
signed_object_type VARCHAR(50) NOT NULL, -- e.g., "CERTIFIED_COPY"
signed_object_id UUID NOT NULL,
-- Signature manifestation
printed_name VARCHAR(255) NOT NULL,
signature_meaning_text TEXT NOT NULL,
signature_reason TEXT,
signature_location VARCHAR(255),
-- Audit
ip_address INET NOT NULL,
user_agent TEXT,
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id)
);
CREATE INDEX idx_e_signatures_signer ON e_signatures(signer_id);
CREATE INDEX idx_e_signatures_timestamp ON e_signatures(signature_timestamp);
CREATE INDEX idx_e_signatures_object ON e_signatures(signed_object_type, signed_object_id);
3.2 Certification Workflow
/**
* Certification workflow service
* Compliance: 21 CFR 11.50 - signature manifestations
*/
export class CertificationWorkflowService {
constructor(
private trueCopyService: TrueCopyService,
private eSignatureService: ESignatureService,
private templateService: CertificationTemplateService,
private pdfGenerator: CertificatePDFGenerator
) {}
/**
* Initiate certification workflow
* Step 1: Generate true copy
*/
async initiateCertification(request: {
original_id: string;
certification_purpose: string;
certification_template_id: string;
initiated_by: string;
}): Promise<CertifiedCopy> {
// 1. Generate true copy first
const trueCopy = await this.trueCopyService.generateTrueCopy({
original_id: request.original_id,
purpose: CopyPurpose.REGULATORY_SUBMISSION,
created_by: request.initiated_by
});
// 2. Verify copy is valid
if (trueCopy.verification_status !== VerificationStatus.VERIFIED) {
throw new Error('Cannot certify copy - verification failed');
}
// 3. Initialize certification workflow
const certifiedCopy: CertifiedCopy = {
...trueCopy,
record_status: RecordStatus.CERTIFIED_COPY,
certification_id: this.generateUUID(),
certifier_id: '', // Assigned during approval
certification_date: new Date(),
certification_purpose: request.certification_purpose,
certification_template_id: request.certification_template_id,
workflow_status: CertificationWorkflowStatus.INITIATED,
workflow_initiated_at: new Date(),
approval_chain: [
{
step_number: 1,
step_type: 'REVIEW',
assigned_to: 'ROLE:QA_REVIEWER',
decision: 'PENDING'
},
{
step_number: 2,
step_type: 'APPROVE',
assigned_to: 'ROLE:QA_MANAGER',
decision: 'PENDING'
},
{
step_number: 3,
step_type: 'CERTIFY',
assigned_to: 'ROLE:COMPLIANCE_OFFICER',
decision: 'PENDING'
}
]
};
await this.saveCertifiedCopy(certifiedCopy);
return certifiedCopy;
}
/**
* Step 2: Review content
*/
async reviewCertification(request: {
certification_id: string;
reviewer_id: string;
decision: 'APPROVED' | 'REJECTED';
comments?: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<CertifiedCopy> {
const certifiedCopy = await this.fetchCertifiedCopy(request.certification_id);
// Verify reviewer has authority
await this.verifyUserRole(request.reviewer_id, 'QA_REVIEWER');
// Apply e-signature
const signature = await this.eSignatureService.sign({
signer_id: request.reviewer_id,
signed_object_type: 'CERTIFIED_COPY',
signed_object_id: certifiedCopy.certification_id,
signature_meaning: SignatureMeaning.REVIEWED,
credentials: request.e_signature_credentials
});
// Update approval chain
const reviewStep = certifiedCopy.approval_chain.find(s => s.step_type === 'REVIEW');
reviewStep.completed_by = request.reviewer_id;
reviewStep.completed_at = new Date();
reviewStep.decision = request.decision;
reviewStep.comments = request.comments;
reviewStep.e_signature = signature;
// Update workflow status
if (request.decision === 'APPROVED') {
certifiedCopy.workflow_status = CertificationWorkflowStatus.PENDING_APPROVAL;
} else {
certifiedCopy.workflow_status = CertificationWorkflowStatus.REJECTED;
}
await this.updateCertifiedCopy(certifiedCopy);
return certifiedCopy;
}
/**
* Step 3: Approve certification
*/
async approveCertification(request: {
certification_id: string;
approver_id: string;
decision: 'APPROVED' | 'REJECTED';
comments?: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<CertifiedCopy> {
const certifiedCopy = await this.fetchCertifiedCopy(request.certification_id);
// Verify approver has authority
await this.verifyUserRole(request.approver_id, 'QA_MANAGER');
// Apply e-signature
const signature = await this.eSignatureService.sign({
signer_id: request.approver_id,
signed_object_type: 'CERTIFIED_COPY',
signed_object_id: certifiedCopy.certification_id,
signature_meaning: SignatureMeaning.APPROVED,
credentials: request.e_signature_credentials
});
// Update approval chain
const approveStep = certifiedCopy.approval_chain.find(s => s.step_type === 'APPROVE');
approveStep.completed_by = request.approver_id;
approveStep.completed_at = new Date();
approveStep.decision = request.decision;
approveStep.comments = request.comments;
approveStep.e_signature = signature;
// Update workflow status
if (request.decision === 'APPROVED') {
certifiedCopy.workflow_status = CertificationWorkflowStatus.PENDING_APPROVAL;
} else {
certifiedCopy.workflow_status = CertificationWorkflowStatus.REJECTED;
}
await this.updateCertifiedCopy(certifiedCopy);
return certifiedCopy;
}
/**
* Step 4: Final certification by Compliance Officer
*/
async finalizeCertification(request: {
certification_id: string;
certifier_id: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<CertifiedCopy> {
const certifiedCopy = await this.fetchCertifiedCopy(request.certification_id);
// Verify certifier has Compliance Officer role
await this.verifyUserRole(request.certifier_id, 'COMPLIANCE_OFFICER');
// Generate certification statement from template
const template = await this.templateService.getTemplate(
certifiedCopy.certification_template_id
);
const statement = await this.templateService.renderTemplate(template, {
certifier_name: await this.getUserName(request.certifier_id),
certification_date: new Date().toISOString(),
original_id: certifiedCopy.source_original_id,
copy_id: certifiedCopy.copy_id,
purpose: certifiedCopy.certification_purpose
});
// Apply e-signature
const signature = await this.eSignatureService.sign({
signer_id: request.certifier_id,
signed_object_type: 'CERTIFIED_COPY',
signed_object_id: certifiedCopy.certification_id,
signature_meaning: SignatureMeaning.CERTIFIED_AS_TRUE_COPY,
credentials: request.e_signature_credentials
});
// Update certification
certifiedCopy.certifier_id = request.certifier_id;
certifiedCopy.certification_date = new Date();
certifiedCopy.certification_statement = statement;
certifiedCopy.e_signature = signature;
certifiedCopy.signature_manifestation = {
printed_name: signature.signer_name,
signature_date: signature.signature_timestamp,
signature_meaning_text: 'Certified as True Copy',
signature_reason: certifiedCopy.certification_purpose,
signature_location: 'Quality Assurance Department'
};
// Update approval chain
const certifyStep = certifiedCopy.approval_chain.find(s => s.step_type === 'CERTIFY');
certifyStep.completed_by = request.certifier_id;
certifyStep.completed_at = new Date();
certifyStep.decision = 'APPROVED';
certifyStep.e_signature = signature;
// Update workflow status
certifiedCopy.workflow_status = CertificationWorkflowStatus.CERTIFIED;
certifiedCopy.workflow_completed_at = new Date();
// Generate certificate of authenticity PDF
const certificatePdf = await this.pdfGenerator.generateCertificate(certifiedCopy);
certifiedCopy.certificate_pdf_url = await this.uploadCertificate(certificatePdf);
certifiedCopy.certificate_generated_at = new Date();
await this.updateCertifiedCopy(certifiedCopy);
return certifiedCopy;
}
}
3.3 Certification Statement Template
/**
* Default FDA 21 CFR Part 11 certification template
*/
const FDA_CERTIFICATION_TEMPLATE = `
CERTIFICATE OF AUTHENTICITY
TRUE COPY CERTIFICATION
I, {{certifier_name}}, Compliance Officer, hereby certify that this document is a true, accurate, and complete copy of the original electronic record maintained in the BIO-QMS system.
Original Record Details:
- Original Record ID: {{original_id}}
- Copy ID: {{copy_id}}
- Original Creation Date: {{original_created_at}}
- Original Created By: {{original_created_by}}
- Module: {{module}}
- Record Type: {{record_type}}
Verification:
- Hash Algorithm: SHA-256
- Original Hash: {{original_hash}}
- Copy Hash: {{copy_hash}}
- Hash Match: {{hash_match}}
- Verification Date: {{verification_date}}
This certification is made in accordance with:
- 21 CFR Part 11.10(e) - Documentation of copies as true copies
- 21 CFR Part 11.50 - Signature manifestations
Purpose of Certification:
{{certification_purpose}}
This certified copy is intended for: {{destination}}
I affirm that this copy was generated through a validated electronic system that ensures the accuracy, reliability, and integrity of electronic records.
Date: {{certification_date}}
Electronically Signed By:
{{certifier_name}}
Compliance Officer
{{signature_timestamp}}
Signature Meaning: Certified as True Copy
Signature Method: {{signature_method}}
---
This is a certified copy. The original electronic record is maintained in the BIO-QMS system and is available upon request.
Certificate ID: {{certificate_id}}
Generated: {{certificate_generated_at}}
`;
4. Amendment Chain
4.1 Data Model
TypeScript Interface
/**
* Amendment record
* Compliance: 21 CFR 11.10(e) - audit trail of amendments
*/
interface Amendment {
// Amendment identifiers
amendment_id: string; // UUID v7
original_id: string; // Reference to original record
amendment_number: number; // Sequential, starts at 1
version: string; // Semantic version (e.g., "1.1.0")
// Amendment metadata
amendment_date: Date;
amended_by: string; // User ID
amendment_reason_category: AmendmentReasonCategory;
amendment_reason_text: string; // Free text explanation
// Before/after tracking
content_before: JSONB; // State before amendment
content_after: JSONB; // State after amendment
diff: JSONB; // Structured diff
// Change summary
fields_modified: string[]; // List of modified field paths
change_type: ChangeType;
impact_assessment: ImpactAssessment;
// Approval workflow
amendment_status: AmendmentStatus;
approval_chain: ApprovalStep[];
// E-signatures
author_signature: ESignature; // Person who made change
reviewer_signature?: ESignature; // Optional reviewer
approver_signature?: ESignature; // Required for critical amendments
// Rollback capability
can_rollback: boolean;
rollback_amendment_id?: string; // If this amendment was a rollback
rolled_back_by?: string; // Amendment that rolled this back
// Regulatory
regulatory_impact: boolean; // Does this affect compliance?
validation_impact: boolean; // Does this require revalidation?
notification_sent: boolean; // Were stakeholders notified?
notification_timestamp?: Date;
}
enum AmendmentReasonCategory {
CORRECTION = 'CORRECTION', // Fix an error
CLARIFICATION = 'CLARIFICATION', // Add clarity, no substantive change
UPDATE = 'UPDATE', // New information
REGULATORY_CHANGE = 'REGULATORY_CHANGE', // Regulation update
PROCESS_IMPROVEMENT = 'PROCESS_IMPROVEMENT',
ROLLBACK = 'ROLLBACK' // Undo previous amendment
}
enum ChangeType {
MINOR = 'MINOR', // Formatting, typos
MODERATE = 'MODERATE', // Clarifications, updates
MAJOR = 'MAJOR' // Substantive changes affecting compliance
}
interface ImpactAssessment {
quality_impact: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
compliance_impact: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
validation_required: boolean;
affected_processes: string[];
stakeholders_to_notify: string[];
}
enum AmendmentStatus {
DRAFT = 'DRAFT',
PENDING_REVIEW = 'PENDING_REVIEW',
PENDING_APPROVAL = 'PENDING_APPROVAL',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
ACTIVE = 'ACTIVE'
}
PostgreSQL Schema
-- Amendments table
CREATE TABLE amendments (
-- Primary identifiers
amendment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
original_id UUID NOT NULL REFERENCES original_records(original_id),
amendment_number INTEGER NOT NULL,
version VARCHAR(20) NOT NULL, -- Semantic version
-- Amendment metadata
amendment_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
amended_by UUID NOT NULL REFERENCES users(user_id),
amendment_reason_category VARCHAR(50) NOT NULL,
amendment_reason_text TEXT NOT NULL,
-- Before/after tracking
content_before JSONB NOT NULL,
content_after JSONB NOT NULL,
diff JSONB NOT NULL,
-- Change summary
fields_modified TEXT[] NOT NULL,
change_type VARCHAR(20) NOT NULL,
impact_assessment JSONB NOT NULL,
-- Approval workflow
amendment_status VARCHAR(30) NOT NULL DEFAULT 'DRAFT',
approval_chain JSONB NOT NULL DEFAULT '[]'::jsonb,
-- E-signatures
author_signature JSONB NOT NULL,
reviewer_signature JSONB,
approver_signature JSONB,
-- Rollback capability
can_rollback BOOLEAN NOT NULL DEFAULT TRUE,
rollback_amendment_id UUID REFERENCES amendments(amendment_id),
rolled_back_by UUID REFERENCES amendments(amendment_id),
-- Regulatory
regulatory_impact BOOLEAN NOT NULL DEFAULT FALSE,
validation_impact BOOLEAN NOT NULL DEFAULT FALSE,
notification_sent BOOLEAN NOT NULL DEFAULT FALSE,
notification_timestamp TIMESTAMPTZ,
-- Multi-tenant
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Constraints
UNIQUE(original_id, amendment_number),
CHECK (amendment_status IN ('DRAFT', 'PENDING_REVIEW', 'PENDING_APPROVAL', 'APPROVED', 'REJECTED', 'ACTIVE')),
CHECK (change_type IN ('MINOR', 'MODERATE', 'MAJOR'))
);
-- Auto-increment amendment_number
CREATE OR REPLACE FUNCTION set_amendment_number()
RETURNS TRIGGER AS $$
BEGIN
SELECT COALESCE(MAX(amendment_number), 0) + 1
INTO NEW.amendment_number
FROM amendments
WHERE original_id = NEW.original_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_amendment_number_trigger
BEFORE INSERT ON amendments
FOR EACH ROW
WHEN (NEW.amendment_number IS NULL)
EXECUTE FUNCTION set_amendment_number();
-- Indexes
CREATE INDEX idx_amendments_original ON amendments(original_id);
CREATE INDEX idx_amendments_date ON amendments(amendment_date);
CREATE INDEX idx_amendments_amended_by ON amendments(amended_by);
CREATE INDEX idx_amendments_status ON amendments(amendment_status);
CREATE INDEX idx_amendments_tenant ON amendments(tenant_id);
-- GIN index for diff search
CREATE INDEX idx_amendments_diff_gin ON amendments USING GIN(diff);
4.2 Amendment Workflow
/**
* Amendment service
* Compliance: 21 CFR 11.10(e) - audit trail of changes
*/
export class AmendmentService {
constructor(
private diffGenerator: DiffGenerator,
private versionGenerator: VersionGenerator,
private eSignatureService: ESignatureService,
private notificationService: NotificationService
) {}
/**
* Create amendment draft
*/
async createAmendment(request: {
original_id: string;
amended_by: string;
reason_category: AmendmentReasonCategory;
reason_text: string;
content_after: JSONB;
change_type: ChangeType;
impact_assessment: ImpactAssessment;
}): Promise<Amendment> {
// 1. Fetch current state
const original = await this.fetchOriginal(request.original_id);
const currentContent = await this.getCurrentContent(request.original_id);
// 2. Generate diff
const diff = this.diffGenerator.generateDiff(
currentContent,
request.content_after
);
// 3. Extract modified fields
const fieldsModified = this.diffGenerator.extractModifiedFields(diff);
// 4. Generate next version number
const currentVersion = await this.getCurrentVersion(request.original_id);
const nextVersion = this.versionGenerator.generateNextVersion(
currentVersion,
request.change_type
);
// 5. Create amendment draft
const amendment: Amendment = {
amendment_id: this.generateUUID(),
original_id: request.original_id,
amendment_number: 0, // Set by trigger
version: nextVersion,
amendment_date: new Date(),
amended_by: request.amended_by,
amendment_reason_category: request.reason_category,
amendment_reason_text: request.reason_text,
content_before: currentContent,
content_after: request.content_after,
diff: diff,
fields_modified: fieldsModified,
change_type: request.change_type,
impact_assessment: request.impact_assessment,
amendment_status: AmendmentStatus.DRAFT,
approval_chain: this.generateApprovalChain(request.change_type),
author_signature: null, // Applied on submit
can_rollback: true,
regulatory_impact: request.impact_assessment.compliance_impact !== 'NONE',
validation_impact: request.impact_assessment.validation_required,
notification_sent: false
};
await this.saveAmendment(amendment);
return amendment;
}
/**
* Submit amendment for approval
*/
async submitAmendment(request: {
amendment_id: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<Amendment> {
const amendment = await this.fetchAmendment(request.amendment_id);
// Apply author signature
const signature = await this.eSignatureService.sign({
signer_id: amendment.amended_by,
signed_object_type: 'AMENDMENT',
signed_object_id: amendment.amendment_id,
signature_meaning: SignatureMeaning.APPROVED,
credentials: request.e_signature_credentials
});
amendment.author_signature = signature;
// Update status based on change type
if (amendment.change_type === ChangeType.MINOR) {
// Minor changes auto-approve
amendment.amendment_status = AmendmentStatus.ACTIVE;
} else {
amendment.amendment_status = AmendmentStatus.PENDING_REVIEW;
}
await this.updateAmendment(amendment);
// Notify reviewers/approvers
await this.notificationService.notifyApprovers(amendment);
return amendment;
}
/**
* Approve amendment
*/
async approveAmendment(request: {
amendment_id: string;
approver_id: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<Amendment> {
const amendment = await this.fetchAmendment(request.amendment_id);
// Verify approver has authority
await this.verifyApprovalAuthority(request.approver_id, amendment.change_type);
// Apply approver signature
const signature = await this.eSignatureService.sign({
signer_id: request.approver_id,
signed_object_type: 'AMENDMENT',
signed_object_id: amendment.amendment_id,
signature_meaning: SignatureMeaning.APPROVED,
credentials: request.e_signature_credentials
});
amendment.approver_signature = signature;
amendment.amendment_status = AmendmentStatus.ACTIVE;
await this.updateAmendment(amendment);
// Apply the amendment to the record
await this.applyAmendment(amendment);
// Notify stakeholders
if (amendment.impact_assessment.stakeholders_to_notify.length > 0) {
await this.notificationService.notifyStakeholders(amendment);
amendment.notification_sent = true;
amendment.notification_timestamp = new Date();
await this.updateAmendment(amendment);
}
return amendment;
}
/**
* Rollback amendment (creates new amendment)
*/
async rollbackAmendment(request: {
amendment_id_to_rollback: string;
rolled_back_by: string;
rollback_reason: string;
e_signature_credentials: ESignatureCredentials;
}): Promise<Amendment> {
const originalAmendment = await this.fetchAmendment(request.amendment_id_to_rollback);
if (!originalAmendment.can_rollback) {
throw new Error('This amendment cannot be rolled back');
}
// Create new amendment that reverses the original
const rollbackAmendment = await this.createAmendment({
original_id: originalAmendment.original_id,
amended_by: request.rolled_back_by,
reason_category: AmendmentReasonCategory.ROLLBACK,
reason_text: `Rollback of Amendment #${originalAmendment.amendment_number}: ${request.rollback_reason}`,
content_after: originalAmendment.content_before, // Revert to before state
change_type: originalAmendment.change_type,
impact_assessment: originalAmendment.impact_assessment
});
rollbackAmendment.rollback_amendment_id = originalAmendment.amendment_id;
// Mark original as rolled back
originalAmendment.rolled_back_by = rollbackAmendment.amendment_id;
originalAmendment.can_rollback = false;
await this.updateAmendment(originalAmendment);
// Submit rollback amendment
await this.submitAmendment({
amendment_id: rollbackAmendment.amendment_id,
e_signature_credentials: request.e_signature_credentials
});
return rollbackAmendment;
}
/**
* Generate approval chain based on change type
*/
private generateApprovalChain(changeType: ChangeType): ApprovalStep[] {
switch (changeType) {
case ChangeType.MINOR:
return []; // No approval needed
case ChangeType.MODERATE:
return [
{
step_number: 1,
step_type: 'REVIEW',
assigned_to: 'ROLE:QA_REVIEWER',
decision: 'PENDING'
}
];
case ChangeType.MAJOR:
return [
{
step_number: 1,
step_type: 'REVIEW',
assigned_to: 'ROLE:QA_REVIEWER',
decision: 'PENDING'
},
{
step_number: 2,
step_type: 'APPROVE',
assigned_to: 'ROLE:QA_MANAGER',
decision: 'PENDING'
}
];
}
}
}
4.3 Diff Generation
/**
* Generate structured diff between two JSONB objects
*/
export class DiffGenerator {
/**
* Generate diff using deep comparison
*/
generateDiff(before: JSONB, after: JSONB): JSONB {
const diff = {
added: {},
modified: {},
removed: {}
};
this.comparePaths(before, after, '', diff);
return diff;
}
/**
* Recursively compare object paths
*/
private comparePaths(before: any, after: any, path: string, diff: any): void {
// Handle null/undefined
if (before === null || before === undefined) {
if (after !== null && after !== undefined) {
diff.added[path] = after;
}
return;
}
if (after === null || after === undefined) {
diff.removed[path] = before;
return;
}
// Handle objects
if (typeof before === 'object' && typeof after === 'object') {
const beforeKeys = Object.keys(before);
const afterKeys = Object.keys(after);
// Check for added keys
for (const key of afterKeys) {
if (!beforeKeys.includes(key)) {
const newPath = path ? `${path}.${key}` : key;
diff.added[newPath] = after[key];
}
}
// Check for removed keys
for (const key of beforeKeys) {
if (!afterKeys.includes(key)) {
const newPath = path ? `${path}.${key}` : key;
diff.removed[newPath] = before[key];
}
}
// Check for modified keys
for (const key of beforeKeys) {
if (afterKeys.includes(key)) {
const newPath = path ? `${path}.${key}` : key;
this.comparePaths(before[key], after[key], newPath, diff);
}
}
return;
}
// Handle primitives
if (before !== after) {
diff.modified[path] = {
before: before,
after: after
};
}
}
/**
* Extract list of modified field paths
*/
extractModifiedFields(diff: JSONB): string[] {
const fields = new Set<string>();
for (const path of Object.keys(diff.added)) {
fields.add(path);
}
for (const path of Object.keys(diff.modified)) {
fields.add(path);
}
for (const path of Object.keys(diff.removed)) {
fields.add(path);
}
return Array.from(fields).sort();
}
}
4.4 Version Generator
/**
* Semantic version generator
*/
export class VersionGenerator {
/**
* Generate next version based on change type
* Major: X.0.0 (breaking changes affecting compliance)
* Minor: X.Y.0 (clarifications, updates)
* Patch: X.Y.Z (typo corrections, formatting)
*/
generateNextVersion(current: string, changeType: ChangeType): string {
const parts = current.split('.').map(Number);
const [major, minor, patch] = parts;
switch (changeType) {
case ChangeType.MAJOR:
return `${major + 1}.0.0`;
case ChangeType.MODERATE:
return `${major}.${minor + 1}.0`;
case ChangeType.MINOR:
return `${major}.${minor}.${patch + 1}`;
}
}
/**
* Parse semantic version
*/
parseVersion(version: string): { major: number; minor: number; patch: number } {
const parts = version.split('.').map(Number);
return {
major: parts[0],
minor: parts[1],
patch: parts[2]
};
}
/**
* Compare versions
*/
compareVersions(v1: string, v2: string): number {
const p1 = this.parseVersion(v1);
const p2 = this.parseVersion(v2);
if (p1.major !== p2.major) return p1.major - p2.major;
if (p1.minor !== p2.minor) return p1.minor - p2.minor;
return p1.patch - p2.patch;
}
}
5. Version Control
5.1 Version History Query
/**
* Version history service
*/
export class VersionHistoryService {
/**
* Get complete version history for a record
*/
async getVersionHistory(original_id: string): Promise<VersionHistoryTimeline> {
const original = await this.fetchOriginal(original_id);
const amendments = await this.fetchAmendments(original_id);
const timeline: VersionHistoryTimeline = {
original_id,
original_created_at: original.created_at,
original_version: '1.0.0',
current_version: await this.getCurrentVersion(original_id),
total_amendments: amendments.length,
versions: []
};
// Add original as version 1.0.0
timeline.versions.push({
version: '1.0.0',
amendment_number: 0,
amendment_date: original.created_at,
amended_by: original.created_by,
change_type: null,
reason: 'Original creation',
fields_modified: [],
content_snapshot: original.original_content
});
// Add each amendment
for (const amendment of amendments) {
timeline.versions.push({
version: amendment.version,
amendment_number: amendment.amendment_number,
amendment_date: amendment.amendment_date,
amended_by: amendment.amended_by,
change_type: amendment.change_type,
reason: amendment.amendment_reason_text,
fields_modified: amendment.fields_modified,
content_snapshot: amendment.content_after,
diff: amendment.diff
});
}
return timeline;
}
/**
* Compare two versions
*/
async compareVersions(
original_id: string,
version1: string,
version2: string
): Promise<VersionComparison> {
const history = await this.getVersionHistory(original_id);
const v1 = history.versions.find(v => v.version === version1);
const v2 = history.versions.find(v => v.version === version2);
if (!v1 || !v2) {
throw new Error('Version not found');
}
const diff = this.diffGenerator.generateDiff(
v1.content_snapshot,
v2.content_snapshot
);
return {
original_id,
version_from: version1,
version_to: version2,
date_from: v1.amendment_date,
date_to: v2.amendment_date,
diff,
fields_modified: this.diffGenerator.extractModifiedFields(diff)
};
}
/**
* Get version tree visualization data
*/
async getVersionTree(original_id: string): Promise<VersionTreeNode> {
const history = await this.getVersionHistory(original_id);
// Build tree structure
const root: VersionTreeNode = {
version: '1.0.0',
amendment_number: 0,
date: history.original_created_at,
author: history.versions[0].amended_by,
change_type: null,
children: []
};
let currentNode = root;
for (let i = 1; i < history.versions.length; i++) {
const version = history.versions[i];
const node: VersionTreeNode = {
version: version.version,
amendment_number: version.amendment_number,
date: version.amendment_date,
author: version.amended_by,
change_type: version.change_type,
children: []
};
currentNode.children.push(node);
currentNode = node;
}
return root;
}
}
interface VersionHistoryTimeline {
original_id: string;
original_created_at: Date;
original_version: string;
current_version: string;
total_amendments: number;
versions: VersionSnapshot[];
}
interface VersionSnapshot {
version: string;
amendment_number: number;
amendment_date: Date;
amended_by: string;
change_type: ChangeType | null;
reason: string;
fields_modified: string[];
content_snapshot: JSONB;
diff?: JSONB;
}
interface VersionComparison {
original_id: string;
version_from: string;
version_to: string;
date_from: Date;
date_to: Date;
diff: JSONB;
fields_modified: string[];
}
interface VersionTreeNode {
version: string;
amendment_number: number;
date: Date;
author: string;
change_type: ChangeType | null;
children: VersionTreeNode[];
}
6. Chain of Custody
6.1 Data Model
/**
* Chain of custody tracking
* Compliance: 21 CFR 11.10(e) - audit trail of access
*/
interface CustodyEvent {
// Event identifiers
custody_event_id: string; // UUID v7
original_id: string; // Record being accessed
event_type: CustodyEventType;
event_timestamp: Date;
// Actor
user_id: string;
user_name: string;
user_role: string;
// Access details
access_method: AccessMethod;
access_purpose: string; // Required free text
access_location: string; // IP address
access_device: string; // Device fingerprint
// Transfer details (if applicable)
transferred_to?: string; // User ID or external party
transfer_destination?: string; // e.g., "FDA Submission Portal"
transfer_method?: TransferMethod;
// Download/print details (if applicable)
download_format?: DownloadFormat;
download_file_hash?: string; // SHA-256 of downloaded file
print_job_id?: string;
// External distribution
external_party?: string; // e.g., "FDA", "Auditor"
distribution_tracking_number?: string;
// Audit
session_id: string;
tenant_id: string;
}
enum CustodyEventType {
VIEW = 'VIEW',
DOWNLOAD = 'DOWNLOAD',
PRINT = 'PRINT',
TRANSFER = 'TRANSFER',
EXTERNAL_DISTRIBUTION = 'EXTERNAL_DISTRIBUTION',
EXPORT = 'EXPORT',
COPY_GENERATED = 'COPY_GENERATED'
}
enum AccessMethod {
WEB_UI = 'WEB_UI',
API = 'API',
MOBILE_APP = 'MOBILE_APP',
EXPORT_PACKAGE = 'EXPORT_PACKAGE'
}
enum TransferMethod {
EMAIL = 'EMAIL',
SECURE_FILE_TRANSFER = 'SECURE_FILE_TRANSFER',
PHYSICAL_MEDIA = 'PHYSICAL_MEDIA',
PORTAL_UPLOAD = 'PORTAL_UPLOAD'
}
enum DownloadFormat {
PDF = 'PDF',
JSON = 'JSON',
XML = 'XML',
CSV = 'CSV'
}
PostgreSQL Schema
-- Chain of custody events
CREATE TABLE custody_events (
-- Event identifiers
custody_event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
original_id UUID NOT NULL REFERENCES original_records(original_id),
event_type VARCHAR(50) NOT NULL,
event_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Actor
user_id UUID NOT NULL REFERENCES users(user_id),
user_name VARCHAR(255) NOT NULL,
user_role VARCHAR(100) NOT NULL,
-- Access details
access_method VARCHAR(30) NOT NULL,
access_purpose TEXT NOT NULL,
access_location INET NOT NULL,
access_device VARCHAR(255) NOT NULL,
-- Transfer details
transferred_to UUID REFERENCES users(user_id),
transfer_destination VARCHAR(255),
transfer_method VARCHAR(50),
-- Download/print details
download_format VARCHAR(20),
download_file_hash VARCHAR(64),
print_job_id VARCHAR(100),
-- External distribution
external_party VARCHAR(255),
distribution_tracking_number VARCHAR(100),
-- Audit
session_id UUID NOT NULL,
tenant_id UUID NOT NULL REFERENCES tenants(tenant_id),
-- Constraints
CHECK (event_type IN ('VIEW', 'DOWNLOAD', 'PRINT', 'TRANSFER', 'EXTERNAL_DISTRIBUTION', 'EXPORT', 'COPY_GENERATED'))
);
-- Indexes
CREATE INDEX idx_custody_events_original ON custody_events(original_id);
CREATE INDEX idx_custody_events_timestamp ON custody_events(event_timestamp);
CREATE INDEX idx_custody_events_user ON custody_events(user_id);
CREATE INDEX idx_custody_events_type ON custody_events(event_type);
CREATE INDEX idx_custody_events_tenant ON custody_events(tenant_id);
6.2 Custody Tracking Service
/**
* Chain of custody service
*/
export class ChainOfCustodyService {
/**
* Log custody event
*/
async logCustodyEvent(event: Partial<CustodyEvent>): Promise<CustodyEvent> {
const custodyEvent: CustodyEvent = {
custody_event_id: this.generateUUID(),
event_timestamp: new Date(),
...event
} as CustodyEvent;
await this.saveCustodyEvent(custodyEvent);
return custodyEvent;
}
/**
* Log view access
*/
async logView(request: {
original_id: string;
user_id: string;
purpose: string;
access_method: AccessMethod;
}): Promise<void> {
await this.logCustodyEvent({
original_id: request.original_id,
event_type: CustodyEventType.VIEW,
user_id: request.user_id,
access_purpose: request.purpose,
access_method: request.access_method,
access_location: await this.getClientIP(),
access_device: await this.getDeviceFingerprint(),
session_id: await this.getSessionID()
});
}
/**
* Log download with hash verification
*/
async logDownload(request: {
original_id: string;
user_id: string;
purpose: string;
format: DownloadFormat;
file_content: Buffer;
}): Promise<void> {
const fileHash = this.hashGenerator.generateHash(request.file_content);
await this.logCustodyEvent({
original_id: request.original_id,
event_type: CustodyEventType.DOWNLOAD,
user_id: request.user_id,
access_purpose: request.purpose,
access_method: AccessMethod.WEB_UI,
access_location: await this.getClientIP(),
access_device: await this.getDeviceFingerprint(),
download_format: request.format,
download_file_hash: fileHash,
session_id: await this.getSessionID()
});
}
/**
* Log external distribution (e.g., regulatory submission)
*/
async logExternalDistribution(request: {
original_id: string;
user_id: string;
external_party: string;
destination: string;
tracking_number?: string;
transfer_method: TransferMethod;
}): Promise<void> {
await this.logCustodyEvent({
original_id: request.original_id,
event_type: CustodyEventType.EXTERNAL_DISTRIBUTION,
user_id: request.user_id,
access_purpose: `External distribution to ${request.external_party}`,
access_method: AccessMethod.EXPORT_PACKAGE,
access_location: await this.getClientIP(),
access_device: await this.getDeviceFingerprint(),
external_party: request.external_party,
transfer_destination: request.destination,
transfer_method: request.transfer_method,
distribution_tracking_number: request.tracking_number,
session_id: await this.getSessionID()
});
}
/**
* Get custody chain report
*/
async getCustodyChainReport(original_id: string): Promise<CustodyChainReport> {
const events = await this.fetchCustodyEvents(original_id);
return {
original_id,
total_events: events.length,
first_access: events[0]?.event_timestamp,
last_access: events[events.length - 1]?.event_timestamp,
view_count: events.filter(e => e.event_type === CustodyEventType.VIEW).length,
download_count: events.filter(e => e.event_type === CustodyEventType.DOWNLOAD).length,
print_count: events.filter(e => e.event_type === CustodyEventType.PRINT).length,
external_distribution_count: events.filter(e => e.event_type === CustodyEventType.EXTERNAL_DISTRIBUTION).length,
unique_users: new Set(events.map(e => e.user_id)).size,
events
};
}
}
interface CustodyChainReport {
original_id: string;
total_events: number;
first_access?: Date;
last_access?: Date;
view_count: number;
download_count: number;
print_count: number;
external_distribution_count: number;
unique_users: number;
events: CustodyEvent[];
}
7. React UI Components
7.1 Amendment History Timeline
/**
* Amendment history timeline component
*/
import React from 'react';
import { Timeline, Card, Tag, Tooltip } from 'antd';
import { FileTextOutlined, UserOutlined, CalendarOutlined } from '@ant-design/icons';
interface AmendmentHistoryTimelineProps {
original_id: string;
}
export const AmendmentHistoryTimeline: React.FC<AmendmentHistoryTimelineProps> = ({
original_id
}) => {
const [history, setHistory] = React.useState<VersionHistoryTimeline | null>(null);
React.useEffect(() => {
// Fetch version history
fetch(`/api/v1/records/${original_id}/history`)
.then(res => res.json())
.then(setHistory);
}, [original_id]);
if (!history) return <div>Loading...</div>;
return (
<Card title="Amendment History">
<Timeline mode="left">
{history.versions.map((version, idx) => (
<Timeline.Item
key={version.version}
color={idx === 0 ? 'green' : version.change_type === 'MAJOR' ? 'red' : 'blue'}
label={<span>{new Date(version.amendment_date).toLocaleDateString()}</span>}
>
<div>
<strong>Version {version.version}</strong>
{version.change_type && (
<Tag color={
version.change_type === 'MAJOR' ? 'red' :
version.change_type === 'MODERATE' ? 'orange' : 'green'
}>
{version.change_type}
</Tag>
)}
</div>
<div style={{ color: '#666', fontSize: '0.9em' }}>
<UserOutlined /> {version.amended_by}
</div>
<div style={{ marginTop: 8 }}>
{version.reason}
</div>
{version.fields_modified.length > 0 && (
<div style={{ marginTop: 8 }}>
<strong>Modified fields:</strong>{' '}
{version.fields_modified.slice(0, 3).join(', ')}
{version.fields_modified.length > 3 && (
<Tooltip title={version.fields_modified.join(', ')}>
<span style={{ color: '#1890ff', cursor: 'pointer' }}>
{' '}+{version.fields_modified.length - 3} more
</span>
</Tooltip>
)}
</div>
)}
</Timeline.Item>
))}
</Timeline>
</Card>
);
};
7.2 Version Comparison Viewer
/**
* Version comparison diff viewer
*/
import React from 'react';
import { Card, Select, Button, Descriptions } from 'antd';
import { DiffEditor } from '@monaco-editor/react';
interface VersionComparisonViewerProps {
original_id: string;
}
export const VersionComparisonViewer: React.FC<VersionComparisonViewerProps> = ({
original_id
}) => {
const [history, setHistory] = React.useState<VersionHistoryTimeline | null>(null);
const [version1, setVersion1] = React.useState<string>('');
const [version2, setVersion2] = React.useState<string>('');
const [comparison, setComparison] = React.useState<VersionComparison | null>(null);
React.useEffect(() => {
fetch(`/api/v1/records/${original_id}/history`)
.then(res => res.json())
.then(data => {
setHistory(data);
if (data.versions.length >= 2) {
setVersion1(data.versions[0].version);
setVersion2(data.versions[1].version);
}
});
}, [original_id]);
const handleCompare = () => {
fetch(`/api/v1/records/${original_id}/compare?v1=${version1}&v2=${version2}`)
.then(res => res.json())
.then(setComparison);
};
if (!history) return <div>Loading...</div>;
return (
<Card title="Version Comparison">
<div style={{ marginBottom: 16 }}>
<Select
value={version1}
onChange={setVersion1}
style={{ width: 200, marginRight: 16 }}
>
{history.versions.map(v => (
<Select.Option key={v.version} value={v.version}>
Version {v.version}
</Select.Option>
))}
</Select>
<span style={{ margin: '0 8px' }}>vs</span>
<Select
value={version2}
onChange={setVersion2}
style={{ width: 200, marginRight: 16 }}
>
{history.versions.map(v => (
<Select.Option key={v.version} value={v.version}>
Version {v.version}
</Select.Option>
))}
</Select>
<Button type="primary" onClick={handleCompare}>
Compare
</Button>
</div>
{comparison && (
<>
<Descriptions bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="From Version">
{comparison.version_from}
</Descriptions.Item>
<Descriptions.Item label="To Version">
{comparison.version_to}
</Descriptions.Item>
<Descriptions.Item label="Date Range">
{new Date(comparison.date_from).toLocaleDateString()} -{' '}
{new Date(comparison.date_to).toLocaleDateString()}
</Descriptions.Item>
<Descriptions.Item label="Fields Modified" span={3}>
{comparison.fields_modified.join(', ')}
</Descriptions.Item>
</Descriptions>
<DiffEditor
height="500px"
language="json"
original={JSON.stringify(
history.versions.find(v => v.version === version1)?.content_snapshot,
null,
2
)}
modified={JSON.stringify(
history.versions.find(v => v.version === version2)?.content_snapshot,
null,
2
)}
options={{
readOnly: true,
renderSideBySide: true
}}
/>
</>
)}
</Card>
);
};
7.3 Chain of Custody Report
/**
* Chain of custody report component
*/
import React from 'react';
import { Card, Table, Tag, Tooltip } from 'antd';
import {
EyeOutlined,
DownloadOutlined,
PrinterOutlined,
SendOutlined
} from '@ant-design/icons';
interface ChainOfCustodyReportProps {
original_id: string;
}
export const ChainOfCustodyReport: React.FC<ChainOfCustodyReportProps> = ({
original_id
}) => {
const [report, setReport] = React.useState<CustodyChainReport | null>(null);
React.useEffect(() => {
fetch(`/api/v1/records/${original_id}/custody-chain`)
.then(res => res.json())
.then(setReport);
}, [original_id]);
if (!report) return <div>Loading...</div>;
const getEventIcon = (eventType: string) => {
switch (eventType) {
case 'VIEW': return <EyeOutlined />;
case 'DOWNLOAD': return <DownloadOutlined />;
case 'PRINT': return <PrinterOutlined />;
case 'EXTERNAL_DISTRIBUTION': return <SendOutlined />;
default: return null;
}
};
const columns = [
{
title: 'Timestamp',
dataIndex: 'event_timestamp',
key: 'timestamp',
render: (ts: string) => new Date(ts).toLocaleString(),
sorter: (a, b) => new Date(a.event_timestamp).getTime() - new Date(b.event_timestamp).getTime()
},
{
title: 'Event Type',
dataIndex: 'event_type',
key: 'type',
render: (type: string) => (
<Tag icon={getEventIcon(type)} color="blue">
{type}
</Tag>
)
},
{
title: 'User',
dataIndex: 'user_name',
key: 'user',
render: (name: string, record: CustodyEvent) => (
<Tooltip title={record.user_role}>
{name}
</Tooltip>
)
},
{
title: 'Purpose',
dataIndex: 'access_purpose',
key: 'purpose',
ellipsis: true
},
{
title: 'Location',
dataIndex: 'access_location',
key: 'location'
},
{
title: 'Method',
dataIndex: 'access_method',
key: 'method'
},
{
title: 'External Party',
dataIndex: 'external_party',
key: 'external',
render: (party: string) => party || '-'
}
];
return (
<Card
title="Chain of Custody Report"
extra={
<div>
Total Events: {report.total_events} |
Unique Users: {report.unique_users} |
External Distributions: {report.external_distribution_count}
</div>
}
>
<Table
columns={columns}
dataSource={report.events}
rowKey="custody_event_id"
pagination={{ pageSize: 20 }}
/>
</Card>
);
};
8. API Endpoints Summary
8.1 Original Records
// Create original record
POST /api/v1/records/original
Request: CreateOriginalRecordRequest
Response: CreateOriginalRecordResponse
// Get original record
GET /api/v1/records/original/:original_id
Response: GetOriginalRecordResponse
// Get original record metadata
GET /api/v1/records/original/:original_id/metadata
Response: OriginalRecord (metadata only)
8.2 True Copies
// Generate true copy
POST /api/v1/records/true-copy
Request: GenerateTrueCopyRequest
Response: GenerateTrueCopyResponse
// Generate batch true copies
POST /api/v1/records/true-copy/batch
Request: GenerateBatchTrueCopiesRequest
Response: GenerateBatchTrueCopiesResponse
// Get true copy
GET /api/v1/records/true-copy/:copy_id
Response: TrueCopy
// Export true copy package
POST /api/v1/records/true-copy/export
Request: ExportTrueCopyPackageRequest
Response: Blob (ZIP/PDF)
8.3 Certified Copies
// Initiate certification
POST /api/v1/records/certified-copy/initiate
Request: InitiateCertificationRequest
Response: CertifiedCopy
// Review certification
POST /api/v1/records/certified-copy/:certification_id/review
Request: ReviewCertificationRequest
Response: CertifiedCopy
// Approve certification
POST /api/v1/records/certified-copy/:certification_id/approve
Request: ApproveCertificationRequest
Response: CertifiedCopy
// Finalize certification
POST /api/v1/records/certified-copy/:certification_id/finalize
Request: FinalizeCertificationRequest
Response: CertifiedCopy
// Get certified copy
GET /api/v1/records/certified-copy/:copy_id
Response: CertifiedCopy
// Download certificate PDF
GET /api/v1/records/certified-copy/:copy_id/certificate.pdf
Response: Blob (PDF)
8.4 Amendments
// Create amendment draft
POST /api/v1/records/:original_id/amendments
Request: CreateAmendmentRequest
Response: Amendment
// Submit amendment for approval
POST /api/v1/records/amendments/:amendment_id/submit
Request: SubmitAmendmentRequest
Response: Amendment
// Approve amendment
POST /api/v1/records/amendments/:amendment_id/approve
Request: ApproveAmendmentRequest
Response: Amendment
// Rollback amendment
POST /api/v1/records/amendments/:amendment_id/rollback
Request: RollbackAmendmentRequest
Response: Amendment
// Get amendment
GET /api/v1/records/amendments/:amendment_id
Response: Amendment
// Get amendments for record
GET /api/v1/records/:original_id/amendments
Response: Amendment[]
8.5 Version History
// Get version history
GET /api/v1/records/:original_id/history
Response: VersionHistoryTimeline
// Compare versions
GET /api/v1/records/:original_id/compare?v1={version1}&v2={version2}
Response: VersionComparison
// Get version tree
GET /api/v1/records/:original_id/version-tree
Response: VersionTreeNode
// Get specific version snapshot
GET /api/v1/records/:original_id/versions/:version
Response: VersionSnapshot
8.6 Chain of Custody
// Log custody event
POST /api/v1/records/:original_id/custody
Request: LogCustodyEventRequest
Response: CustodyEvent
// Get custody chain report
GET /api/v1/records/:original_id/custody-chain
Response: CustodyChainReport
// Get custody events (paginated)
GET /api/v1/records/:original_id/custody-events?page={page}&limit={limit}
Response: PaginatedCustodyEvents
9. Compliance Mapping
9.1 FDA 21 CFR Part 11 Requirements
| Regulation | Requirement | Implementation |
|---|---|---|
| §11.10(a) | Validation of systems to ensure accuracy, reliability, consistent intended performance | - Automated hash verification - Immutability enforcement via DB triggers - E-signature validation |
| §11.10(e) | Use of secure, computer-generated, time-stamped audit trails to independently record the date and time of operator entries and actions | - custody_events table with timestamptz - Immutable audit trail (no DELETE allowed) - Automatic timestamp generation |
| §11.10(e) | Documentation of record copies as true copies | - True copy verification via hash matching - Watermark distinguishing copies - Copy metadata tracking - Copy count per original |
| §11.10(k)(1) | Use of electronic records in non-rewritable, non-erasable format | - original_records table immutability trigger - Amendment chain (no UPDATE/DELETE on originals) - SHA-256 hash for integrity verification |
| §11.10(k)(2) | Protection of records to enable their accurate and ready retrieval throughout the records retention period | - retention_policy with calculated destruction dates - legal_hold flag - Multi-level backup strategy |
| §11.50 | Signed electronic records shall contain information associated with the signing | - signature_manifestation with printed name, date, meaning - e_signatures table with full audit trail - Signature method tracking |
| §11.70 | Signature/record linking | - e_signature.signed_object_id foreign key - Signature embedded in certified_copies and amendments - Hash verification of signed content |
9.2 ISO 13485 Medical Device QMS
| Requirement | Implementation |
|---|---|
| 4.2.4 Control of records | - Immutable original records - Version control with semantic versioning - Retention policy enforcement |
| 4.2.5 Documentation requirements | - Complete amendment history - Before/after diff tracking - Approval chain documentation |
| 7.5.11 Traceability | - Chain of custody tracking - External distribution logging - Transfer tracking |
9.3 EU GMP Annex 11 Computerised Systems
| Requirement | Implementation |
|---|---|
| 6. Audit Trail | - custody_events immutable log - Timestamped with user identification - Reason for change captured (amendment_reason_text) |
| 8. Printouts | - Print job logging in custody_events - Watermark on copies - Certificate of authenticity for certified copies |
| 9. Audit Trail Review | - Chain of custody report - Version history timeline - Amendment history visualization |
10. Testing & Validation
10.1 Unit Tests
/**
* Hash generation tests
*/
describe('RecordHashGenerator', () => {
it('should generate consistent hash for same content', () => {
const generator = new RecordHashGenerator();
const content = { field1: 'value1', field2: 'value2' };
const hash1 = generator.generateHash(content);
const hash2 = generator.generateHash(content);
expect(hash1).toBe(hash2);
});
it('should generate different hash for different content', () => {
const generator = new RecordHashGenerator();
const content1 = { field1: 'value1' };
const content2 = { field1: 'value2' };
const hash1 = generator.generateHash(content1);
const hash2 = generator.generateHash(content2);
expect(hash1).not.toBe(hash2);
});
it('should verify hash correctly', () => {
const generator = new RecordHashGenerator();
const content = { field1: 'value1' };
const hash = generator.generateHash(content);
const isValid = generator.verifyHash(content, hash);
expect(isValid).toBe(true);
});
});
/**
* True copy tests
*/
describe('TrueCopyService', () => {
it('should generate verified true copy', async () => {
const service = new TrueCopyService(hashGenerator, auditLogger);
const trueCopy = await service.generateTrueCopy({
original_id: 'original-123',
purpose: CopyPurpose.REGULATORY_SUBMISSION,
created_by: 'user-456'
});
expect(trueCopy.verification_status).toBe(VerificationStatus.VERIFIED);
expect(trueCopy.hash_match_confirmed).toBe(true);
});
it('should fail verification if original is tampered', async () => {
// Test implementation
});
});
/**
* Amendment tests
*/
describe('AmendmentService', () => {
it('should create amendment with diff', async () => {
const service = new AmendmentService(diffGenerator, versionGenerator, eSignatureService, notificationService);
const amendment = await service.createAmendment({
original_id: 'original-123',
amended_by: 'user-456',
reason_category: AmendmentReasonCategory.CORRECTION,
reason_text: 'Fix typo in field1',
content_after: { field1: 'corrected_value' },
change_type: ChangeType.MINOR,
impact_assessment: { /* ... */ }
});
expect(amendment.fields_modified).toContain('field1');
expect(amendment.diff.modified).toHaveProperty('field1');
});
it('should auto-approve minor amendments', async () => {
// Test implementation
});
it('should require approval for major amendments', async () => {
// Test implementation
});
});
10.2 Integration Tests
/**
* End-to-end certification workflow test
*/
describe('Certification Workflow', () => {
it('should complete full certification workflow', async () => {
// 1. Create original
const original = await createOriginalRecord({ /* ... */ });
// 2. Initiate certification
const certification = await initiateCertification({
original_id: original.original_id,
certification_purpose: 'FDA submission',
certification_template_id: 'fda-template-1'
});
expect(certification.workflow_status).toBe(CertificationWorkflowStatus.INITIATED);
// 3. Review
await reviewCertification({
certification_id: certification.certification_id,
reviewer_id: 'qa-reviewer',
decision: 'APPROVED'
});
// 4. Approve
await approveCertification({
certification_id: certification.certification_id,
approver_id: 'qa-manager',
decision: 'APPROVED'
});
// 5. Finalize
const finalCertification = await finalizeCertification({
certification_id: certification.certification_id,
certifier_id: 'compliance-officer'
});
expect(finalCertification.workflow_status).toBe(CertificationWorkflowStatus.CERTIFIED);
expect(finalCertification.certificate_pdf_url).toBeTruthy();
});
});
11. Deployment & Operations
11.1 Database Migration
-- Run migrations in order
\i migrations/001_create_original_records.sql
\i migrations/002_create_true_copies.sql
\i migrations/003_create_certified_copies.sql
\i migrations/004_create_amendments.sql
\i migrations/005_create_custody_events.sql
\i migrations/006_create_e_signatures.sql
\i migrations/007_create_indexes.sql
\i migrations/008_create_triggers.sql
11.2 Monitoring & Alerts
# Datadog monitors
monitors:
- name: "Original Record Hash Verification Failures"
type: metric alert
query: "sum(last_5m):sum:bioqms.true_copy.verification_failed{*} > 5"
message: "Hash verification failures detected - potential data integrity issue"
- name: "High Custody Event Volume"
type: metric alert
query: "sum(last_1h):sum:bioqms.custody_events.count{*} > 10000"
message: "Unusual custody event volume - investigate potential audit log attack"
- name: "Certification Workflow Stuck"
type: metric alert
query: "avg(last_1h):avg:bioqms.certification.pending_duration{*} > 86400"
message: "Certification workflows stuck in pending state for >24 hours"
12. Summary
This document defines a comprehensive original/copy/amendment tracking system that ensures full compliance with FDA 21 CFR Part 11, ISO 13485, and EU GMP Annex 11 requirements.
Key Features:
- Immutable originals with cryptographic hash verification
- Verified true copies with automatic hash validation
- Certified copies with qualified e-signatures and approval workflows
- Complete amendment chain with before/after diff tracking
- Semantic versioning (major.minor.patch)
- Chain of custody logging for all access and distribution
- Version comparison and timeline visualization
- Rollback capability (creates new amendment, not DELETE)
Compliance Coverage:
- ✅ 21 CFR 11.10(a) - Validated system with hash verification
- ✅ 21 CFR 11.10(e) - Audit trails and true copy documentation
- ✅ 21 CFR 11.10(k)(1) - Non-rewritable, non-erasable records
- ✅ 21 CFR 11.50 - Signature manifestations
- ✅ ISO 13485 4.2.4 - Record control
- ✅ EU GMP Annex 11.6 - Audit trail
Next Steps:
- Implement React UI components (amendment timeline, version comparison, custody report)
- Deploy database schema and triggers
- Conduct IQ/OQ/PQ validation
- Train users on amendment workflows
- Establish SOPs for certified copy generation and regulatory submissions
Document Control:
- Version: 1.0.0
- Approved By: [Compliance Officer Name]
- Approval Date: [YYYY-MM-DD]
- Next Review: [YYYY-MM-DD + 1 year]
- Change History: See amendment chain in QMS
This document was generated as part of Task D.5.6: Build Original/Copy/Amendment Tracking System for the BIO-QMS platform.