ALCOA+ Format Preservation Controls Specification
Document ID: CODITECT-BIO-ALCOA-001 Version: 1.0.0 Effective Date: 2026-02-16 Classification: Internal - Restricted Owner: Chief Information Security Officer (CISO)
Document Control
Approval History
| Role | Name | Signature | Date |
|---|---|---|---|
| Chief Information Security Officer | [Pending] | [Digital Signature] | YYYY-MM-DD |
| VP Quality Assurance | [Pending] | [Digital Signature] | YYYY-MM-DD |
| VP Engineering | [Pending] | [Digital Signature] | YYYY-MM-DD |
| Regulatory Affairs Director | [Pending] | [Digital Signature] | YYYY-MM-DD |
Revision History
| Version | Date | Author | Changes | Approval Status |
|---|---|---|---|---|
| 1.0.0 | 2026-02-16 | CISO Office | Initial release | Draft |
Distribution List
- Executive Leadership Team
- Information Security Team
- Quality Assurance Team
- Engineering Leadership
- Compliance and Regulatory Affairs
- Internal Audit
- External Auditors (as needed)
Review Schedule
| Review Type | Frequency | Next Review Date | Responsible Party |
|---|---|---|---|
| Annual Review | 12 months | 2027-02-16 | CISO |
| Regulatory Update Review | As needed | N/A | Regulatory Affairs |
| Post-Incident Review | As needed | N/A | Security Incident Response Team |
| Data Integrity Audit | Quarterly | 2026-05-16 | Compliance Officer |
Executive Summary
ALCOA+ represents the gold standard for data integrity in GxP-regulated environments. This specification establishes comprehensive technical and procedural controls ensuring all electronic records in the BIO-QMS platform meet ALCOA+ principles:
- ALCOA (Core Principles): Attributable, Legible, Contemporaneous, Original, Accurate
- Plus (Extended Principles): Complete, Consistent, Enduring, Available
This document provides:
- Detailed control requirements for each ALCOA+ principle
- TypeScript/SQL implementations
- Database schema extensions
- Validation and monitoring strategies
- Regulatory mapping to FDA, WHO, EMA, MHRA guidance
Compliance Scope:
- FDA 21 CFR Part 11 (Electronic Records and Signatures)
- FDA Data Integrity and Compliance with Drug CGMP (2018)
- WHO TRS 996 Annex 5: Guidance on Good Data and Record Management Practices
- EMA Guideline on Data Integrity
- MHRA GXP Data Integrity Guidance
Table of Contents
- Purpose and Scope
- ALCOA+ Core Principles
- Principle 1: Attributable
- Principle 2: Legible
- Principle 3: Contemporaneous
- Principle 4: Original
- Principle 5: Accurate
- Principle 6: Complete
- Principle 7: Consistent
- Principle 8: Enduring
- Principle 9: Available
- Implementation Architecture
- Validation and Testing
- Monitoring and Metrics
- Regulatory Mapping
1. Purpose and Scope
1.1 Purpose
This specification establishes comprehensive ALCOA+ format preservation controls for the BIO-QMS platform to ensure:
- Data Integrity - Electronic records remain trustworthy, reliable, and tamper-evident throughout their lifecycle
- Regulatory Compliance - All records meet FDA, EMA, MHRA, and WHO data integrity requirements
- Audit Readiness - Complete audit trail with attribution, timestamping, and versioning
- Long-Term Preservation - Records remain readable and accessible for retention periods (typically 5-30 years)
1.2 Scope
In Scope:
- All electronic records in the QMS platform (protocols, SOPs, reports, data, signatures)
- Multi-tenant SaaS architecture with tenant-isolated data
- FDA-regulated pharmaceutical and medical device manufacturers
- Cloud-native deployment (Google Cloud Platform)
- Integration with external systems (LIMS, ERP, eTMF)
Out of Scope:
- Paper-based records (covered by separate SOPs)
- Non-GxP business systems (CRM, HR, Finance)
- Development and staging environments (non-validated)
1.3 Definitions
| Term | Definition |
|---|---|
| ALCOA | Core data integrity principles: Attributable, Legible, Contemporaneous, Original, Accurate |
| ALCOA+ | Extended principles adding: Complete, Consistent, Enduring, Available |
| GxP | Good Practice (GMP, GLP, GCP, GDP, GVP) - regulated quality standards |
| Audit Trail | Secure, computer-generated, timestamped electronic record documenting all record creation and modifications |
| True Copy | Exact replica of original record preserving content, context, and metadata |
| Certified Copy | True copy with compliance officer attestation of accuracy and completeness |
2. ALCOA+ Core Principles
2.1 Regulatory Foundation
FDA Data Integrity Guidance (December 2018):
"Data should be attributable, legible, contemporaneous, original, and accurate (ALCOA). FDA expects that data also be complete, consistent, enduring, and available (ALCOA+) throughout the data lifecycle."
WHO TRS 996 Annex 5 (2016):
"ALCOA principles establish the foundation for data integrity in pharmaceutical quality systems."
EMA GMP Data Integrity (2016):
"Data integrity requires all nine ALCOA+ principles to be met throughout the entire data lifecycle."
2.2 ALCOA+ Principles Matrix
| Principle | FDA Requirement | Technical Control | Validation Method |
|---|---|---|---|
| Attributable | §11.10(e) | User/session/device tracking | Audit log review |
| Legible | §11.10(b) | Format preservation + rendering | Visual verification |
| Contemporaneous | §11.10(e) | Server-side NTP timestamps | Clock sync monitoring |
| Original | §11.10(c) | Immutability + versioning | Hash verification |
| Accurate | §11.10(h) | Validation rules + error checking | Accuracy metrics |
| Complete | Data Integrity Guidance | Mandatory fields + audit completeness | Completeness checks |
| Consistent | Data Integrity Guidance | Standardization + cross-checks | Consistency validation |
| Enduring | §11.10(c) | PDF/A archival + migration | Readability testing |
| Available | §11.10(b) | Multi-region storage + backup | Retrieval SLA |
3. Principle 1: Attributable
3.1 Definition
Regulatory Requirement:
"It should be possible to identify the individual responsible for performing an activity and the time at which it was performed." - FDA Data Integrity Guidance
Every record must clearly identify:
- WHO performed the action (user identity)
- WHAT was done (action type)
- WHEN it occurred (timestamp)
- WHERE it happened (device/IP/location)
- WHY it was done (business context/meaning)
3.2 Attribution Requirements
3.2.1 User Attribution
| Attribute | Source | Storage | Immutability |
|---|---|---|---|
| User ID | Authentication service | audit_trail.user_id | Immutable |
| Full Name | User profile | audit_trail.user_name | Snapshot at event time |
| User profile | audit_trail.user_email | Snapshot at event time | |
| Role | RBAC system | audit_trail.user_role | Snapshot at event time |
| Department | Org structure | audit_trail.department | Snapshot at event time |
Rationale for Snapshots: User profiles change over time (name changes, role changes). Audit trail must capture user attributes at the time of action, not current values.
3.2.2 Session Attribution
| Attribute | Source | Storage | Purpose |
|---|---|---|---|
| Session ID | Auth service | audit_trail.session_id | Track user session |
| IP Address | HTTP headers | audit_trail.ip_address | Network forensics |
| Device Type | User-Agent | audit_trail.device_type | Device tracking |
| Browser/Version | User-Agent | audit_trail.user_agent | Client environment |
| Geographic Location | IP geolocation | audit_trail.geo_location | Location tracking |
3.2.3 System vs User Actions
Requirement: Distinguish system-generated actions from user-initiated actions.
// audit-attribution.types.ts
export enum ActionInitiator {
USER = 'user', // Direct user action
SYSTEM = 'system', // Scheduled job, automation
API = 'api', // External system via API
SERVICE_ACCOUNT = 'service' // Service account (attribute to initiating user)
}
export interface AttributionContext {
initiator: ActionInitiator;
userId?: string; // Always present for USER/SERVICE
systemProcess?: string; // Present for SYSTEM (e.g., 'nightly-backup')
apiClientId?: string; // Present for API (OAuth client ID)
delegatedBy?: string; // For SERVICE: actual user who initiated
businessReason?: string; // Optional: why action was performed
}
Service Account Attribution Rule:
Service accounts performing actions on behalf of users MUST record the initiating user. Example: Automated report generation triggered by user schedule setup.
// Example: Service account action attributed to user
const attribution: AttributionContext = {
initiator: ActionInitiator.SERVICE_ACCOUNT,
userId: 'service-account-reporting',
delegatedBy: 'user-jane-smith-12345',
businessReason: 'Scheduled monthly compliance report'
};
3.3 Database Schema
-- audit_trail table (existing, extend with attribution fields)
CREATE TABLE audit_trail (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Core audit fields
entity_type VARCHAR(100) NOT NULL,
entity_id UUID NOT NULL,
action VARCHAR(50) NOT NULL,
performed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- User attribution (snapshot at event time)
user_id UUID NOT NULL REFERENCES users(id),
user_name VARCHAR(255) NOT NULL,
user_email VARCHAR(255) NOT NULL,
user_role VARCHAR(100) NOT NULL,
department VARCHAR(255),
-- Session attribution
session_id UUID NOT NULL,
ip_address INET NOT NULL,
device_type VARCHAR(50),
user_agent TEXT,
geo_location JSONB, -- {country, region, city, lat, lon}
-- Action initiator
initiator VARCHAR(20) NOT NULL, -- 'user', 'system', 'api', 'service'
system_process VARCHAR(100), -- e.g., 'nightly-backup'
api_client_id VARCHAR(100),
delegated_by UUID REFERENCES users(id), -- for service accounts
business_reason TEXT,
-- Signature attribution (for signed records)
signature_meaning TEXT, -- 'author', 'reviewer', 'approver', 'witness'
signature_id UUID REFERENCES electronic_signatures(id),
-- Change tracking
previous_value JSONB,
new_value JSONB,
-- Immutability
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Indexes for performance
INDEX idx_audit_tenant_entity (tenant_id, entity_type, entity_id),
INDEX idx_audit_user (tenant_id, user_id, performed_at),
INDEX idx_audit_performed_at (tenant_id, performed_at)
) PARTITION BY RANGE (performed_at);
-- Prevent modifications/deletions
CREATE TRIGGER prevent_audit_trail_modification
BEFORE UPDATE OR DELETE ON audit_trail
FOR EACH ROW EXECUTE FUNCTION prevent_modification();
3.4 Implementation
// src/services/audit/attribution-service.ts
import { Request } from 'express';
import { UAParser } from 'ua-parser-js';
import geoip from 'geoip-lite';
export interface AuditAttributionData {
// User attribution
userId: string;
userName: string;
userEmail: string;
userRole: string;
department?: string;
// Session attribution
sessionId: string;
ipAddress: string;
deviceType: string;
userAgent: string;
geoLocation?: {
country: string;
region: string;
city: string;
lat: number;
lon: number;
};
// Action initiator
initiator: ActionInitiator;
systemProcess?: string;
apiClientId?: string;
delegatedBy?: string;
businessReason?: string;
}
export class AttributionService {
/**
* Extract attribution context from HTTP request
*/
static extractFromRequest(req: Request): AuditAttributionData {
const user = req.user; // From authentication middleware
const session = req.session;
// Parse User-Agent
const ua = new UAParser(req.headers['user-agent']);
const deviceType = ua.getDevice().type || 'desktop';
// IP address (handle proxies)
const ipAddress = req.headers['x-forwarded-for'] as string ||
req.headers['x-real-ip'] as string ||
req.socket.remoteAddress || '';
const clientIp = ipAddress.split(',')[0].trim();
// Geolocation
const geo = geoip.lookup(clientIp);
const geoLocation = geo ? {
country: geo.country,
region: geo.region,
city: geo.city,
lat: geo.ll[0],
lon: geo.ll[1]
} : undefined;
return {
userId: user.id,
userName: user.fullName,
userEmail: user.email,
userRole: user.role,
department: user.department,
sessionId: session.id,
ipAddress: clientIp,
deviceType,
userAgent: req.headers['user-agent'] || '',
geoLocation,
initiator: ActionInitiator.USER
};
}
/**
* Create attribution for system process
*/
static forSystemProcess(
processName: string,
systemUserId: string
): AuditAttributionData {
return {
userId: systemUserId,
userName: 'System Process',
userEmail: 'system@bio-qms.internal',
userRole: 'system',
sessionId: `system-${Date.now()}`,
ipAddress: '127.0.0.1',
deviceType: 'server',
userAgent: 'BIO-QMS/1.0 (System Process)',
initiator: ActionInitiator.SYSTEM,
systemProcess: processName
};
}
/**
* Create attribution for service account (delegated)
*/
static forServiceAccount(
serviceAccountId: string,
delegatedByUserId: string,
delegatedByName: string,
businessReason: string
): AuditAttributionData {
return {
userId: serviceAccountId,
userName: 'Service Account',
userEmail: 'service@bio-qms.internal',
userRole: 'service',
sessionId: `service-${Date.now()}`,
ipAddress: '127.0.0.1',
deviceType: 'server',
userAgent: 'BIO-QMS/1.0 (Service Account)',
initiator: ActionInitiator.SERVICE_ACCOUNT,
delegatedBy: delegatedByUserId,
businessReason
};
}
/**
* Validate attribution completeness
*/
static validate(attribution: AuditAttributionData): ValidationResult {
const errors: string[] = [];
if (!attribution.userId) errors.push('userId is required');
if (!attribution.userName) errors.push('userName is required');
if (!attribution.sessionId) errors.push('sessionId is required');
if (!attribution.ipAddress) errors.push('ipAddress is required');
// Service accounts must have delegatedBy
if (attribution.initiator === ActionInitiator.SERVICE_ACCOUNT) {
if (!attribution.delegatedBy) {
errors.push('delegatedBy is required for service accounts');
}
}
return {
valid: errors.length === 0,
errors
};
}
}
3.5 Signature Attribution
Electronic Signature Meaning (§11.50(a)(3) requirement):
// src/models/electronic-signature.model.ts
export enum SignatureMeaning {
AUTHOR = 'author', // Document author
REVIEWER = 'reviewer', // Technical review
APPROVER = 'approver', // Final approval
WITNESS = 'witness', // Witnessed action
QA_REVIEW = 'qa_review', // Quality assurance review
SUPERVISOR_APPROVAL = 'supervisor_approval',
MEDICAL_REVIEW = 'medical_review',
REGULATORY_APPROVAL = 'regulatory_approval'
}
export interface ElectronicSignature {
id: string;
signerId: string;
signedAt: Date;
meaning: SignatureMeaning;
meaningText: string; // Free-text explanation
recordId: string;
recordType: string;
recordVersion: number; // Version signed
ipAddress: string;
deviceType: string;
// Cryptographic binding (§11.70)
signatureHash: string; // SHA-256(recordVersion + signerId + signedAt + meaning)
}
3.6 Validation and Testing
Test Case AT-001: User Attribution
- Given: User "Jane Smith" performs document approval
- When: Audit trail is queried for the approval event
- Then: Record shows user_name="Jane Smith", user_email="jane.smith@company.com", action="approve"
Test Case AT-002: Service Account Attribution
- Given: Nightly backup process archives documents
- When: Audit trail is reviewed
- Then: Records show initiator="service", delegated_by="system-scheduler", system_process="nightly-backup"
Test Case AT-003: Signature Meaning
- Given: QA reviewer signs off on validation protocol
- When: Signature record is retrieved
- Then: meaning="qa_review", meaningText="QA review and approval of IQ/OQ protocol"
4. Principle 2: Legible
4.1 Definition
Regulatory Requirement:
"Data should be recorded permanently in a durable medium and be readable throughout the retention period." - FDA Data Integrity Guidance
Records must be:
- Human-readable without specialized software
- Rendered consistently across platforms and time
- Preserved in their original format or format-migrated with validation
- Accessible to regulators without proprietary tools
4.2 Format Preservation Requirements
4.2.1 Supported Source Formats
| Format | Use Case | Preservation Strategy | Long-Term Format |
|---|---|---|---|
| Final reports, signed documents | PDF/A-2b conversion | PDF/A-2b | |
| DOCX | Protocols, SOPs (draft) | Pandoc → PDF/A | PDF/A-2b |
| XLSX | Data tables, validation data | LibreOffice → PDF/A | PDF/A-2b |
| HTML | Web forms, dashboards | Puppeteer → PDF/A | PDF/A-2b |
| PNG/JPEG | Images, charts | Embed in PDF/A | PDF/A-2b |
| CSV | Raw data export | Preserve + PDF report | CSV + PDF/A |
| XML | Structured data | Preserve + XSLT → PDF | XML + PDF/A |
PDF/A-2b Standard (ISO 19005-2):
- Self-contained (embedded fonts, images)
- No external dependencies
- Device-independent rendering
- Long-term archival quality
4.2.2 Rendering Guarantees
Resolution Standards:
- Scanned documents: Minimum 300 DPI
- Generated PDFs: Vector graphics preferred, minimum 150 DPI for raster
- Screenshots: Minimum 1920x1080 native resolution
Font Embedding:
// src/services/pdf/pdf-generation.service.ts
import PDFDocument from 'pdfkit';
export class PDFGenerationService {
/**
* Generate PDF/A-2b with embedded fonts
*/
static async generatePDFA(
content: DocumentContent,
metadata: PDFMetadata
): Promise<Buffer> {
const doc = new PDFDocument({
pdfVersion: '1.7',
subset: 'PDF/A-2b',
tagged: true, // For accessibility
displayTitle: true
});
// Embed fonts (required for PDF/A)
doc.registerFont('DejaVu-Sans', './fonts/DejaVuSans.ttf');
doc.registerFont('DejaVu-Serif', './fonts/DejaVuSerif.ttf');
doc.registerFont('DejaVu-Mono', './fonts/DejaVuMono.ttf');
// Set metadata
doc.info.Title = metadata.title;
doc.info.Author = metadata.author;
doc.info.Subject = metadata.subject;
doc.info.Keywords = metadata.keywords;
doc.info.CreationDate = metadata.createdAt;
// Add content
this.renderContent(doc, content);
// Validate PDF/A compliance
const buffer = await this.finalizeDocument(doc);
await this.validatePDFA(buffer);
return buffer;
}
/**
* Validate PDF/A-2b compliance
*/
private static async validatePDFA(pdfBuffer: Buffer): Promise<void> {
// Use verapdf for validation
const { exec } = require('child_process');
const tmpFile = `/tmp/validate-${Date.now()}.pdf`;
fs.writeFileSync(tmpFile, pdfBuffer);
return new Promise((resolve, reject) => {
exec(`verapdf --flavour 2b ${tmpFile}`, (error, stdout) => {
fs.unlinkSync(tmpFile);
if (error || !stdout.includes('compliant="true"')) {
reject(new Error('PDF/A validation failed'));
} else {
resolve();
}
});
});
}
}
4.2.3 Color Preservation
Requirement: Charts, graphs, and color-coded data must preserve color information.
// Color space embedding
doc.addColorSpace('sRGB', {
type: 'CalRGB',
whitePoint: [0.9505, 1.0000, 1.0890], // D65 illuminant
gamma: [2.2, 2.2, 2.2]
});
// Use sRGB for all colors
doc.fillColor('#FF5733', 1.0, 'sRGB');
4.2.4 Accessibility (WCAG 2.1 AA)
Screen Reader Support:
// Tagged PDF structure
doc.addStructure(PDFStructureElement.DOCUMENT, () => {
doc.addStructure(PDFStructureElement.H1, () => {
doc.text('Validation Protocol VP-001', { tag: 'H1' });
});
doc.addStructure(PDFStructureElement.P, () => {
doc.text('Purpose: Validate data integrity controls', { tag: 'P' });
});
doc.addStructure(PDFStructureElement.TABLE, () => {
// Table with proper headers
});
});
// Alt text for images
doc.image('chart.png', {
alt: 'Bar chart showing accuracy rates 2024-2026'
});
4.3 Rendering Verification
Automated Testing:
// test/integration/rendering-verification.test.ts
describe('Rendering Verification', () => {
test('RT-001: PDF renders consistently across viewers', async () => {
const pdf = await generateTestPDF();
// Render with multiple engines
const render1 = await renderWithPDFJS(pdf);
const render2 = await renderWithMuPDF(pdf);
const render3 = await renderWithPoppler(pdf);
// Compare pixel-by-pixel
const diff1 = compareImages(render1, render2);
const diff2 = compareImages(render2, render3);
expect(diff1.percentDifference).toBeLessThan(0.01); // <1% difference
expect(diff2.percentDifference).toBeLessThan(0.01);
});
test('RT-002: Archived documents remain readable', async () => {
// Retrieve 5-year-old archived document
const archivedPdf = await archiveService.retrieve('DOC-2021-001');
// Verify PDF/A compliance still valid
const validation = await validatePDFA(archivedPdf);
expect(validation.compliant).toBe(true);
// Verify fonts still embedded
const fonts = await extractFonts(archivedPdf);
expect(fonts.every(f => f.embedded)).toBe(true);
// Verify rendering
const rendered = await renderWithPDFJS(archivedPdf);
expect(rendered.pages).toBeGreaterThan(0);
});
test('RT-003: Screen reader accessibility', async () => {
const pdf = await generateTestPDF();
// Extract structure
const structure = await extractPDFStructure(pdf);
// Verify tags present
expect(structure.hasHeadings).toBe(true);
expect(structure.hasParagraphs).toBe(true);
expect(structure.tablesHaveHeaders).toBe(true);
// Verify alt text on images
const images = structure.images;
expect(images.every(img => img.altText)).toBe(true);
});
});
4.4 Database Schema
-- document_formats table
CREATE TABLE document_formats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
document_id UUID NOT NULL REFERENCES documents(id),
-- Original format
original_format VARCHAR(20) NOT NULL, -- 'pdf', 'docx', 'xlsx', 'html'
original_filename VARCHAR(500) NOT NULL,
original_size_bytes BIGINT NOT NULL,
original_hash_sha256 CHAR(64) NOT NULL,
original_storage_path TEXT NOT NULL,
-- Preservation format (PDF/A)
preservation_format VARCHAR(20) NOT NULL DEFAULT 'pdf/a-2b',
preservation_filename VARCHAR(500) NOT NULL,
preservation_size_bytes BIGINT NOT NULL,
preservation_hash_sha256 CHAR(64) NOT NULL,
preservation_storage_path TEXT NOT NULL,
-- Conversion metadata
converted_at TIMESTAMPTZ NOT NULL,
converted_by UUID NOT NULL REFERENCES users(id),
conversion_tool VARCHAR(100) NOT NULL, -- 'pandoc', 'libreoffice', 'puppeteer'
conversion_tool_version VARCHAR(50) NOT NULL,
-- Quality validation
pdfa_compliance_verified BOOLEAN NOT NULL DEFAULT false,
pdfa_validation_date TIMESTAMPTZ,
pdfa_validator_version VARCHAR(50),
font_embedding_verified BOOLEAN NOT NULL DEFAULT false,
resolution_dpi INTEGER,
accessibility_score DECIMAL(3,2), -- WCAG score 0.00-1.00
-- Rendering verification
last_render_test_date TIMESTAMPTZ,
render_test_result JSONB, -- {viewer: 'pdfjs', version: '3.1', success: true}
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_document_formats_document (tenant_id, document_id),
INDEX idx_document_formats_verification (tenant_id, pdfa_validation_date)
);
4.5 Monitoring Dashboard
Key Metrics:
| Metric | Target | Alert Threshold | Query |
|---|---|---|---|
| PDF/A Compliance Rate | 100% | <99.5% | % of documents with pdfa_compliance_verified=true |
| Font Embedding Rate | 100% | <99.9% | % of PDFs with all fonts embedded |
| Render Test Failure Rate | 0% | >0.1% | % of documents failing render verification |
| Accessibility Score | >0.95 | <0.90 | Average WCAG compliance score |
// Dashboard query
SELECT
COUNT(*) FILTER (WHERE pdfa_compliance_verified) * 100.0 / COUNT(*) AS pdfa_compliance_rate,
COUNT(*) FILTER (WHERE font_embedding_verified) * 100.0 / COUNT(*) AS font_embedding_rate,
AVG(accessibility_score) AS avg_accessibility_score,
COUNT(*) FILTER (WHERE last_render_test_date < NOW() - INTERVAL '1 year') AS overdue_render_tests
FROM document_formats
WHERE tenant_id = $1
AND created_at > NOW() - INTERVAL '30 days';
5. Principle 3: Contemporaneous
5.1 Definition
Regulatory Requirement:
"Data should be recorded at the time the work is performed, and date/time stamps should follow a defined sequence." - FDA Data Integrity Guidance
Requirements:
- Timestamps assigned by trusted server source (never client)
- Time synchronization via NTP (<100ms drift)
- Timezone normalization to UTC with original timezone preserved
- Microsecond precision for sequencing
- Immutable timestamps (no backdating)
5.2 Server-Side Timestamp Architecture
Never Trust Client Time:
// ❌ WRONG: Client-supplied timestamp
app.post('/api/records', (req, res) => {
const record = {
...req.body,
createdAt: req.body.timestamp // CLIENT TIME - FORBIDDEN
};
});
// ✅ CORRECT: Server-assigned timestamp
app.post('/api/records', (req, res) => {
const record = {
...req.body,
createdAt: new Date() // SERVER TIME ONLY
};
});
Database-Level Enforcement:
-- All timestamp columns must use DEFAULT CURRENT_TIMESTAMP
CREATE TABLE records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- Server-assigned timestamps (client cannot override)
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Prevent client from setting these fields
CHECK (created_at IS NOT NULL),
CHECK (updated_at IS NOT NULL)
);
-- Trigger to enforce server time on INSERT
CREATE OR REPLACE FUNCTION enforce_server_timestamp()
RETURNS TRIGGER AS $$
BEGIN
-- Override any client-supplied timestamps
NEW.created_at := CURRENT_TIMESTAMP;
NEW.updated_at := CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_timestamp_on_insert
BEFORE INSERT ON records
FOR EACH ROW
EXECUTE FUNCTION enforce_server_timestamp();
5.3 Time Synchronization
NTP Configuration:
# /etc/chrony/chrony.conf (Cloud Run containers)
server time.google.com iburst
server time1.google.com iburst
server time2.google.com iburst
server time3.google.com iburst
# Maximum allowed drift: 100ms
maxdistance 0.1
# Log time adjustments
logdir /var/log/chrony
log measurements statistics tracking
Time Sync Monitoring:
// src/services/monitoring/time-sync-monitor.ts
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export class TimeSyncMonitor {
/**
* Check NTP synchronization status
*/
static async checkSync(): Promise<TimeSyncStatus> {
try {
const { stdout } = await execAsync('chronyc tracking');
// Parse output
const offsetMatch = stdout.match(/System time\s+:\s+([\d.]+) seconds (fast|slow)/);
const offsetSeconds = offsetMatch ?
parseFloat(offsetMatch[1]) * (offsetMatch[2] === 'slow' ? -1 : 1) :
null;
const stratum = stdout.match(/Stratum\s+:\s+(\d+)/)?.[1];
const refTime = stdout.match(/Ref time.*:\s+(.+)/)?.[1];
return {
synchronized: offsetSeconds !== null && Math.abs(offsetSeconds) < 0.1,
offsetSeconds,
stratum: stratum ? parseInt(stratum) : null,
referenceTime: refTime,
lastCheck: new Date()
};
} catch (error) {
return {
synchronized: false,
error: error.message,
lastCheck: new Date()
};
}
}
/**
* Alert if drift exceeds threshold
*/
static async monitorDrift(): Promise<void> {
const status = await this.checkSync();
if (!status.synchronized) {
await alertingService.sendAlert({
severity: 'critical',
title: 'NTP Synchronization Lost',
message: `Server time drift detected: ${status.offsetSeconds}s`,
component: 'time-sync',
action: 'Investigate NTP configuration and network connectivity'
});
} else if (status.offsetSeconds && Math.abs(status.offsetSeconds) > 0.05) {
await alertingService.sendAlert({
severity: 'warning',
title: 'Time Drift Warning',
message: `Server time offset: ${status.offsetSeconds}s (threshold: 50ms)`,
component: 'time-sync'
});
}
}
}
// Run every 5 minutes
setInterval(() => TimeSyncMonitor.monitorDrift(), 5 * 60 * 1000);
5.4 Timezone Handling
UTC Normalization:
// src/utils/timestamp.util.ts
export class TimestampUtil {
/**
* Create timestamp with UTC normalization and original timezone preservation
*/
static createTimestamp(
userTimezone: string = 'UTC'
): TimestampRecord {
const now = new Date();
return {
utc: now.toISOString(), // Always store as UTC ISO 8601
epochMs: now.getTime(), // Unix milliseconds
timezone: userTimezone,
localDisplay: this.formatInTimezone(now, userTimezone)
};
}
/**
* Format timestamp in user's timezone (for display only)
*/
static formatInTimezone(date: Date, timezone: string): string {
return new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
hour12: false
}).format(date);
}
/**
* ISO 8601 with microsecond precision
*/
static toISO8601Precise(date: Date): string {
const ms = date.getTime();
const us = (ms % 1) * 1000; // Microseconds (if available)
return `${date.toISOString().slice(0, -1)}${us.toFixed(0).padStart(3, '0')}Z`;
}
}
Database Storage:
-- All timestamps stored as TIMESTAMPTZ (timezone-aware)
CREATE TABLE events (
id UUID PRIMARY KEY,
-- UTC timestamp (PostgreSQL internally stores as UTC)
occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- User's timezone (for display purposes)
user_timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
-- High-precision epoch microseconds (for sequencing)
occurred_at_us BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000000)::BIGINT
);
-- Function to extract microseconds
CREATE OR REPLACE FUNCTION get_timestamp_us()
RETURNS BIGINT AS $$
SELECT (EXTRACT(EPOCH FROM CLOCK_TIMESTAMP()) * 1000000)::BIGINT;
$$ LANGUAGE SQL;
5.5 Timestamp Immutability
Prevent Backdating:
// src/middleware/timestamp-validation.middleware.ts
export function preventBackdating(req: Request, res: Response, next: NextFunction) {
const body = req.body;
// Check for client-supplied timestamps
const suspiciousFields = [
'createdAt', 'created_at', 'timestamp', 'date',
'updatedAt', 'updated_at', 'performedAt'
];
for (const field of suspiciousFields) {
if (field in body) {
return res.status(400).json({
error: 'Client-supplied timestamps are forbidden',
field,
message: 'Timestamps are assigned by the server only'
});
}
}
next();
}
// Apply globally
app.use(preventBackdating);
Audit Trail Sequence Validation:
-- Detect gaps or out-of-sequence timestamps
CREATE OR REPLACE FUNCTION validate_audit_sequence()
RETURNS TABLE(
entity_id UUID,
event_number BIGINT,
occurred_at TIMESTAMPTZ,
next_occurred_at TIMESTAMPTZ,
sequence_violation BOOLEAN
) AS $$
SELECT
entity_id,
ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY occurred_at) AS event_number,
occurred_at,
LEAD(occurred_at) OVER (PARTITION BY entity_id ORDER BY occurred_at) AS next_occurred_at,
occurred_at > LEAD(occurred_at) OVER (PARTITION BY entity_id ORDER BY occurred_at) AS sequence_violation
FROM audit_trail
WHERE tenant_id = current_setting('app.tenant_id')::UUID
HAVING sequence_violation = true;
$$ LANGUAGE SQL;
5.6 Validation and Testing
Test Case TS-001: Server Time Only
- Given: Client attempts to POST record with
createdAtfield - When: Request is processed
- Then: 400 error returned: "Client-supplied timestamps are forbidden"
Test Case TS-002: NTP Synchronization
- Given: Server has been running for 24 hours
- When: Time sync status is checked
- Then: Clock drift < 100ms, stratum ≤ 4
Test Case TS-003: Timestamp Precision
- Given: Two events occur in rapid succession
- When: Timestamps are compared
- Then: Timestamps differ by at least 1 microsecond, sequence is maintained
6. Principle 4: Original
6.1 Definition
Regulatory Requirement:
"Original records and true copies should be preserved. If data are transcribed, the original record must be preserved." - FDA Data Integrity Guidance
Requirements:
- Original record clearly identified and protected from modification
- Distinction between original, true copy, and certified copy
- Versioning system for tracking amendments
- Chain of custody for record transfers
- Hash verification for copies
6.2 Record Versioning System
Schema:
-- document_versions table
CREATE TABLE document_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
document_id UUID NOT NULL REFERENCES documents(id),
-- Version identification
version_number INTEGER NOT NULL,
is_original BOOLEAN NOT NULL DEFAULT false,
is_current BOOLEAN NOT NULL DEFAULT true,
-- Content
content_hash_sha256 CHAR(64) NOT NULL,
content_storage_path TEXT NOT NULL,
content_size_bytes BIGINT NOT NULL,
format VARCHAR(20) NOT NULL,
-- Metadata
title VARCHAR(500) NOT NULL,
description TEXT,
status VARCHAR(50) NOT NULL, -- 'draft', 'review', 'approved', 'archived'
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by UUID NOT NULL REFERENCES users(id),
-- Amendment tracking
amendment_reason TEXT,
supersedes_version INTEGER REFERENCES document_versions(version_number),
-- Immutability
CHECK (version_number > 0),
UNIQUE (tenant_id, document_id, version_number),
INDEX idx_document_versions_doc (tenant_id, document_id, version_number DESC),
INDEX idx_document_versions_current (tenant_id, document_id, is_current) WHERE is_current = true
);
-- Prevent modification of versions
CREATE TRIGGER prevent_version_modification
BEFORE UPDATE OR DELETE ON document_versions
FOR EACH ROW EXECUTE FUNCTION prevent_modification();
Versioning Logic:
// src/services/document/versioning.service.ts
export class DocumentVersioningService {
/**
* Create initial version (original)
*/
async createOriginal(
tenantId: string,
documentId: string,
content: Buffer,
metadata: DocumentMetadata,
userId: string
): Promise<DocumentVersion> {
const hash = crypto.createHash('sha256').update(content).digest('hex');
const version = await db.documentVersions.create({
data: {
tenantId,
documentId,
versionNumber: 1,
isOriginal: true,
isCurrent: true,
contentHashSha256: hash,
contentStoragePath: await storage.save(content),
contentSizeBytes: content.length,
format: metadata.format,
title: metadata.title,
status: 'draft',
createdBy: userId
}
});
await this.createAuditEntry({
tenantId,
entityType: 'document_version',
entityId: version.id,
action: 'create_original',
userId,
message: `Original document version 1 created`
});
return version;
}
/**
* Create amendment (new version)
*/
async createAmendment(
tenantId: string,
documentId: string,
content: Buffer,
amendmentReason: string,
userId: string
): Promise<DocumentVersion> {
// Get current version
const currentVersion = await this.getCurrentVersion(tenantId, documentId);
// Mark current as not current
await db.documentVersions.update({
where: { id: currentVersion.id },
data: { isCurrent: false }
});
// Create new version
const hash = crypto.createHash('sha256').update(content).digest('hex');
const newVersion = await db.documentVersions.create({
data: {
tenantId,
documentId,
versionNumber: currentVersion.versionNumber + 1,
isOriginal: false,
isCurrent: true,
contentHashSha256: hash,
contentStoragePath: await storage.save(content),
contentSizeBytes: content.length,
format: currentVersion.format,
title: currentVersion.title,
status: 'draft',
createdBy: userId,
amendmentReason,
supersedesVersion: currentVersion.versionNumber
}
});
await this.createAuditEntry({
tenantId,
entityType: 'document_version',
entityId: newVersion.id,
action: 'create_amendment',
userId,
message: `Amendment created: version ${newVersion.versionNumber}`,
metadata: { amendmentReason }
});
return newVersion;
}
/**
* Get original version
*/
async getOriginal(
tenantId: string,
documentId: string
): Promise<DocumentVersion> {
return db.documentVersions.findFirst({
where: {
tenantId,
documentId,
isOriginal: true
}
});
}
}
6.3 True Copy vs Certified Copy
Definitions:
| Type | Definition | Verification | Use Case |
|---|---|---|---|
| Original | First instance of record created in system | is_original=true | Master record |
| True Copy | Exact replica with hash match | SHA-256 match | Internal transfers |
| Certified Copy | True copy with compliance officer attestation | Signature + hash match | Regulatory submissions |
Schema:
-- document_copies table
CREATE TABLE document_copies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Original reference
original_document_id UUID NOT NULL REFERENCES documents(id),
original_version_id UUID NOT NULL REFERENCES document_versions(id),
original_hash_sha256 CHAR(64) NOT NULL,
-- Copy details
copy_type VARCHAR(20) NOT NULL, -- 'true_copy', 'certified_copy'
copy_hash_sha256 CHAR(64) NOT NULL,
copy_storage_path TEXT NOT NULL,
-- Verification
hash_verified BOOLEAN NOT NULL DEFAULT false,
hash_verification_date TIMESTAMPTZ,
verified_by UUID REFERENCES users(id),
-- Certification (for certified copies)
certified_by UUID REFERENCES users(id),
certification_date TIMESTAMPTZ,
certification_statement TEXT,
certification_signature_id UUID REFERENCES electronic_signatures(id),
-- Purpose and chain of custody
purpose TEXT NOT NULL, -- Why copy was created
destination TEXT, -- Where copy is going (e.g., 'FDA submission')
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by UUID NOT NULL REFERENCES users(id),
CHECK (copy_type IN ('true_copy', 'certified_copy')),
CHECK (original_hash_sha256 = copy_hash_sha256), -- Enforce true copy
INDEX idx_document_copies_original (tenant_id, original_document_id)
);
Implementation:
// src/services/document/copy.service.ts
export class DocumentCopyService {
/**
* Create true copy with hash verification
*/
async createTrueCopy(
tenantId: string,
originalDocumentId: string,
originalVersionId: string,
purpose: string,
userId: string
): Promise<DocumentCopy> {
// Retrieve original
const original = await storage.retrieve(originalVersionId);
const originalHash = crypto.createHash('sha256').update(original).digest('hex');
// Create copy
const copyPath = await storage.save(original, {
metadata: {
copyOf: originalVersionId,
purpose
}
});
const copyHash = crypto.createHash('sha256').update(original).digest('hex');
// Verify hashes match
if (originalHash !== copyHash) {
throw new Error('Hash mismatch: Copy integrity verification failed');
}
const copy = await db.documentCopies.create({
data: {
tenantId,
originalDocumentId,
originalVersionId,
originalHashSha256: originalHash,
copyType: 'true_copy',
copyHashSha256: copyHash,
copyStoragePath: copyPath,
hashVerified: true,
hashVerificationDate: new Date(),
verifiedBy: userId,
purpose,
createdBy: userId
}
});
await this.createAuditEntry({
tenantId,
action: 'create_true_copy',
userId,
message: `True copy created for: ${purpose}`
});
return copy;
}
/**
* Create certified copy (requires compliance officer)
*/
async createCertifiedCopy(
tenantId: string,
originalDocumentId: string,
originalVersionId: string,
purpose: string,
destination: string,
certifyingUserId: string
): Promise<DocumentCopy> {
// Verify certifying user has compliance officer role
const user = await userService.getUser(certifyingUserId);
if (!user.roles.includes('compliance_officer')) {
throw new Error('Only compliance officers can certify copies');
}
// Create true copy first
const trueCopy = await this.createTrueCopy(
tenantId,
originalDocumentId,
originalVersionId,
purpose,
certifyingUserId
);
// Add certification
const certificationStatement = `I, ${user.fullName}, Compliance Officer, hereby certify that this is a true and complete copy of the original document, accurate in all respects.`;
const signature = await signatureService.createSignature({
tenantId,
signerId: certifyingUserId,
recordId: trueCopy.id,
recordType: 'document_copy',
meaning: 'certified_copy_attestation',
meaningText: certificationStatement
});
const certifiedCopy = await db.documentCopies.update({
where: { id: trueCopy.id },
data: {
copyType: 'certified_copy',
certifiedBy: certifyingUserId,
certificationDate: new Date(),
certificationStatement,
certificationSignatureId: signature.id,
destination
}
});
await this.createAuditEntry({
tenantId,
action: 'certify_copy',
userId: certifyingUserId,
message: `Copy certified for ${destination}`
});
return certifiedCopy;
}
/**
* Verify copy integrity
*/
async verifyCopyIntegrity(copyId: string): Promise<CopyVerificationResult> {
const copy = await db.documentCopies.findUnique({ where: { id: copyId } });
// Retrieve both original and copy
const originalContent = await storage.retrieve(copy.originalVersionId);
const copyContent = await storage.retrieve(copy.copyStoragePath);
// Compute hashes
const originalHash = crypto.createHash('sha256').update(originalContent).digest('hex');
const copyHash = crypto.createHash('sha256').update(copyContent).digest('hex');
// Compare
const matches = originalHash === copyHash &&
originalHash === copy.originalHashSha256 &&
copyHash === copy.copyHashSha256;
return {
copyId,
verified: matches,
originalHash,
copyHash,
storedOriginalHash: copy.originalHashSha256,
storedCopyHash: copy.copyHashSha256,
verifiedAt: new Date()
};
}
}
6.4 Chain of Custody
Schema:
-- chain_of_custody table
CREATE TABLE chain_of_custody (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
document_id UUID NOT NULL REFERENCES documents(id),
-- Transfer details
event_type VARCHAR(50) NOT NULL, -- 'created', 'transferred', 'copied', 'archived'
from_location VARCHAR(255),
to_location VARCHAR(255),
from_custodian UUID REFERENCES users(id),
to_custodian UUID REFERENCES users(id),
-- Purpose
purpose TEXT NOT NULL,
-- Verification
document_hash_sha256 CHAR(64) NOT NULL,
verified_at_transfer BOOLEAN NOT NULL DEFAULT false,
-- Timestamp
occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
recorded_by UUID NOT NULL REFERENCES users(id),
-- Audit
INDEX idx_chain_of_custody_doc (tenant_id, document_id, occurred_at)
);
6.5 Validation and Testing
Test Case OR-001: Original Identification
- Given: Document with multiple versions exists
- When: Original version is queried
- Then: Version 1 returned with is_original=true
Test Case OR-002: True Copy Verification
- Given: True copy created from original
- When: Hash verification performed
- Then: originalHash === copyHash, hash_verified=true
Test Case OR-003: Certified Copy Attestation
- Given: Compliance officer certifies copy for FDA submission
- When: Copy record is retrieved
- Then: certification_statement present, signature_id links to valid signature
Test Case OR-004: Amendment Tracking
- Given: Version 3 is created as amendment to version 2
- When: Version history is queried
- Then: version 3.supersedes_version = 2, amendment_reason recorded
7. Principle 5: Accurate
7.1 Definition
Regulatory Requirement:
"Data should be free from errors. The system should have appropriate controls to ensure accuracy of data entry and processing." - FDA Data Integrity Guidance
Requirements:
- Field-level validation rules (regex, range checks, cross-references)
- Transcription error detection (duplicate detection, pattern analysis)
- Calculation verification for computed fields
- Monthly accuracy rate metrics (target: >99.5%)
- Error correction workflow with audit trail
7.2 Data Validation Framework
Schema:
-- validation_rules table
CREATE TABLE validation_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Rule identification
rule_code VARCHAR(50) NOT NULL UNIQUE,
rule_name VARCHAR(255) NOT NULL,
rule_description TEXT,
-- Scope
entity_type VARCHAR(100) NOT NULL, -- 'protocol', 'sop', 'batch_record'
field_name VARCHAR(100) NOT NULL,
-- Validation logic
validation_type VARCHAR(50) NOT NULL, -- 'regex', 'range', 'enum', 'cross_reference'
validation_config JSONB NOT NULL, -- Type-specific config
-- Examples
valid_examples TEXT[],
invalid_examples TEXT[],
-- Error handling
severity VARCHAR(20) NOT NULL, -- 'error', 'warning', 'info'
error_message TEXT NOT NULL,
-- Regulatory justification
regulatory_reference TEXT, -- e.g., 'FDA 21 CFR §211.68'
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
effective_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by UUID NOT NULL REFERENCES users(id),
INDEX idx_validation_rules_entity (tenant_id, entity_type, field_name)
);
-- Sample validation rules
INSERT INTO validation_rules (
tenant_id, rule_code, rule_name, entity_type, field_name,
validation_type, validation_config, severity, error_message, regulatory_reference
) VALUES
-- Batch number format
('tenant-123', 'VAL-001', 'Batch Number Format', 'batch_record', 'batch_number',
'regex', '{"pattern": "^[A-Z]{2}\\d{6}-\\d{3}$"}', 'error',
'Batch number must follow format: XX######-###', 'FDA 21 CFR §211.188'),
-- Temperature range
('tenant-123', 'VAL-002', 'Storage Temperature Range', 'batch_record', 'storage_temp',
'range', '{"min": 2, "max": 8, "unit": "°C"}', 'error',
'Storage temperature must be between 2-8°C', 'Product specification'),
-- pH range
('tenant-123', 'VAL-003', 'pH Range', 'qc_test', 'ph_value',
'range', '{"min": 6.8, "max": 7.4, "precision": 2}', 'error',
'pH must be between 6.80-7.40', 'Product specification'),
-- Equipment ID cross-reference
('tenant-123', 'VAL-004', 'Equipment ID Validation', 'batch_record', 'equipment_id',
'cross_reference', '{"table": "equipment", "field": "equipment_id", "status": "qualified"}', 'error',
'Equipment must exist and be in qualified status', 'FDA 21 CFR §211.68');
Implementation:
// src/services/validation/validation-engine.ts
export class ValidationEngine {
/**
* Validate field value against all applicable rules
*/
async validateField(
tenantId: string,
entityType: string,
fieldName: string,
value: any,
context?: Record<string, any>
): Promise<ValidationResult[]> {
// Retrieve rules
const rules = await db.validationRules.findMany({
where: {
tenantId,
entityType,
fieldName,
isActive: true
}
});
const results: ValidationResult[] = [];
for (const rule of rules) {
const result = await this.executeRule(rule, value, context);
results.push(result);
}
return results;
}
/**
* Execute individual validation rule
*/
private async executeRule(
rule: ValidationRule,
value: any,
context?: Record<string, any>
): Promise<ValidationResult> {
try {
switch (rule.validationType) {
case 'regex':
return this.validateRegex(rule, value);
case 'range':
return this.validateRange(rule, value);
case 'enum':
return this.validateEnum(rule, value);
case 'cross_reference':
return await this.validateCrossReference(rule, value, context);
case 'custom':
return await this.validateCustom(rule, value, context);
default:
throw new Error(`Unknown validation type: ${rule.validationType}`);
}
} catch (error) {
return {
ruleCode: rule.ruleCode,
passed: false,
severity: 'error',
message: `Validation error: ${error.message}`
};
}
}
/**
* Regex validation
*/
private validateRegex(rule: ValidationRule, value: string): ValidationResult {
const config = rule.validationConfig as { pattern: string };
const regex = new RegExp(config.pattern);
const passed = regex.test(value);
return {
ruleCode: rule.ruleCode,
ruleName: rule.ruleName,
passed,
severity: rule.severity,
message: passed ? 'Valid' : rule.errorMessage,
value,
expected: config.pattern
};
}
/**
* Range validation
*/
private validateRange(rule: ValidationRule, value: number): ValidationResult {
const config = rule.validationConfig as {
min?: number;
max?: number;
precision?: number;
unit?: string;
};
let passed = true;
let message = 'Valid';
if (typeof value !== 'number') {
passed = false;
message = 'Value must be a number';
} else {
if (config.min !== undefined && value < config.min) {
passed = false;
message = `Value ${value} is below minimum ${config.min}`;
}
if (config.max !== undefined && value > config.max) {
passed = false;
message = `Value ${value} exceeds maximum ${config.max}`;
}
if (config.precision !== undefined) {
const decimalPlaces = (value.toString().split('.')[1] || '').length;
if (decimalPlaces > config.precision) {
passed = false;
message = `Value has too many decimal places (max: ${config.precision})`;
}
}
}
return {
ruleCode: rule.ruleCode,
ruleName: rule.ruleName,
passed,
severity: rule.severity,
message: passed ? 'Valid' : rule.errorMessage + ': ' + message,
value,
expected: `${config.min}-${config.max}${config.unit || ''}`
};
}
/**
* Cross-reference validation
*/
private async validateCrossReference(
rule: ValidationRule,
value: any,
context?: Record<string, any>
): Promise<ValidationResult> {
const config = rule.validationConfig as {
table: string;
field: string;
status?: string;
};
// Query referenced table
const exists = await db[config.table].findFirst({
where: {
[config.field]: value,
...(config.status && { status: config.status })
}
});
return {
ruleCode: rule.ruleCode,
ruleName: rule.ruleName,
passed: !!exists,
severity: rule.severity,
message: exists ? 'Valid reference' : rule.errorMessage,
value,
expected: `Exists in ${config.table}.${config.field}`
};
}
}
7.3 Transcription Error Detection
Duplicate Detection:
// src/services/validation/duplicate-detector.ts
export class DuplicateDetector {
/**
* Detect potential duplicate data entries
*/
async detectDuplicates(
tenantId: string,
entityType: string,
newRecord: Record<string, any>
): Promise<DuplicateDetectionResult[]> {
// Define key fields for each entity type
const keyFields = this.getKeyFields(entityType);
// Find records with matching key fields
const candidates = await db[entityType].findMany({
where: {
tenantId,
OR: keyFields.map(field => ({
[field]: newRecord[field]
}))
}
});
// Calculate similarity scores
const duplicates: DuplicateDetectionResult[] = [];
for (const candidate of candidates) {
const similarity = this.calculateSimilarity(newRecord, candidate);
if (similarity > 0.8) { // 80% similar
duplicates.push({
candidateId: candidate.id,
similarityScore: similarity,
matchingFields: this.getMatchingFields(newRecord, candidate),
recommendation: similarity > 0.95 ? 'likely_duplicate' : 'possible_duplicate'
});
}
}
return duplicates;
}
/**
* Calculate similarity between two records
*/
private calculateSimilarity(
record1: Record<string, any>,
record2: Record<string, any>
): number {
const fields = Object.keys(record1);
let matchingFields = 0;
for (const field of fields) {
if (this.fieldsMatch(record1[field], record2[field])) {
matchingFields++;
}
}
return matchingFields / fields.length;
}
/**
* Check if two field values match
*/
private fieldsMatch(value1: any, value2: any): boolean {
// Exact match
if (value1 === value2) return true;
// String similarity (Levenshtein distance)
if (typeof value1 === 'string' && typeof value2 === 'string') {
const distance = levenshtein(value1.toLowerCase(), value2.toLowerCase());
const maxLen = Math.max(value1.length, value2.length);
return (maxLen - distance) / maxLen > 0.9; // 90% similar
}
// Numeric close match (±5%)
if (typeof value1 === 'number' && typeof value2 === 'number') {
return Math.abs(value1 - value2) / value1 < 0.05;
}
return false;
}
}
7.4 Calculation Verification
Computed Fields:
// src/services/validation/calculation-verifier.ts
export class CalculationVerifier {
/**
* Verify calculated field accuracy
*/
async verifyCalculation(
entityType: string,
recordId: string,
fieldName: string
): Promise<CalculationVerificationResult> {
const record = await db[entityType].findUnique({ where: { id: recordId } });
const storedValue = record[fieldName];
// Get calculation formula
const formula = this.getFormula(entityType, fieldName);
// Recompute value
const computedValue = await formula(record);
// Compare
const matches = this.valuesMatch(storedValue, computedValue);
if (!matches) {
// Log discrepancy
await db.calculationDiscrepancies.create({
data: {
entityType,
recordId,
fieldName,
storedValue,
computedValue,
discrepancyPercent: Math.abs(storedValue - computedValue) / storedValue * 100,
detectedAt: new Date()
}
});
}
return {
fieldName,
storedValue,
computedValue,
matches,
discrepancy: matches ? 0 : Math.abs(storedValue - computedValue)
};
}
/**
* Define calculation formulas
*/
private getFormula(entityType: string, fieldName: string): Function {
const formulas: Record<string, Record<string, Function>> = {
batch_record: {
// Yield percentage = (actual_yield / theoretical_yield) * 100
yield_percentage: (record) =>
(record.actual_yield / record.theoretical_yield) * 100,
// Total cost = unit_cost * quantity
total_cost: (record) =>
record.unit_cost * record.quantity
},
qc_test: {
// Mean = sum / count
mean_value: (record) =>
record.values.reduce((a, b) => a + b, 0) / record.values.length,
// Standard deviation
std_deviation: (record) => {
const mean = record.mean_value;
const variance = record.values
.map(x => Math.pow(x - mean, 2))
.reduce((a, b) => a + b, 0) / record.values.length;
return Math.sqrt(variance);
}
}
};
return formulas[entityType]?.[fieldName] || (() => null);
}
/**
* Check if values match (with floating-point tolerance)
*/
private valuesMatch(stored: number, computed: number): boolean {
// Allow 0.01% tolerance for floating-point errors
const tolerance = 0.0001;
return Math.abs(stored - computed) / Math.max(stored, computed) < tolerance;
}
}
7.5 Accuracy Metrics
Monthly Accuracy Rate:
-- Accuracy metrics query
CREATE VIEW accuracy_metrics AS
SELECT
entity_type,
DATE_TRUNC('month', created_at) AS month,
-- Total records
COUNT(*) AS total_records,
-- Records with validation errors
COUNT(*) FILTER (WHERE has_validation_errors) AS records_with_errors,
-- Accuracy rate
(COUNT(*) FILTER (WHERE NOT has_validation_errors) * 100.0 / COUNT(*)) AS accuracy_rate_percent,
-- Error breakdown by severity
COUNT(*) FILTER (WHERE error_severity = 'error') AS critical_errors,
COUNT(*) FILTER (WHERE error_severity = 'warning') AS warnings,
-- Transcription errors
COUNT(*) FILTER (WHERE has_transcription_error) AS transcription_errors,
-- Calculation errors
COUNT(*) FILTER (WHERE has_calculation_error) AS calculation_errors
FROM records
WHERE tenant_id = current_setting('app.tenant_id')::UUID
GROUP BY entity_type, DATE_TRUNC('month', created_at);
-- Alert on accuracy rate below target
CREATE OR REPLACE FUNCTION check_accuracy_threshold()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.accuracy_rate_percent < 99.5 THEN
PERFORM pg_notify('accuracy_alert',
json_build_object(
'entity_type', NEW.entity_type,
'month', NEW.month,
'accuracy_rate', NEW.accuracy_rate_percent,
'target', 99.5
)::text
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
7.6 Validation and Testing
Test Case AC-001: Field Validation
- Given: Batch number "12345" entered (invalid format)
- When: Validation runs
- Then: Error returned: "Batch number must follow format: XX######-###"
Test Case AC-002: Duplicate Detection
- Given: Record with 95% similarity to existing record entered
- When: Duplicate detection runs
- Then: Warning returned: "Possible duplicate of record #12345"
Test Case AC-003: Calculation Verification
- Given: Yield percentage stored as 92.5%, theoretical=1000, actual=920
- When: Calculation verified
- Then: Recomputed yield = 92.0%, discrepancy logged
Test Case AC-004: Accuracy Rate
- Given: 1,000 records created in January, 3 with validation errors
- When: Monthly accuracy metrics calculated
- Then: Accuracy rate = 99.7% (above 99.5% target)
(Continuing with principles 6-9 and remaining sections...)
8. Principle 6: Complete
8.1 Definition
Regulatory Requirement:
"All data generated during activities must be recorded at the time of performance. Blank or null fields must be justified." - FDA Data Integrity Guidance
Requirements:
- Mandatory field enforcement per record type
- Blank field justification workflow
- Audit trail completeness verification
- Report completeness checks before submission
8.2 Mandatory Field Enforcement
Schema:
-- field_requirements table
CREATE TABLE field_requirements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Scope
entity_type VARCHAR(100) NOT NULL,
field_name VARCHAR(100) NOT NULL,
-- Requirement
is_mandatory BOOLEAN NOT NULL DEFAULT false,
allow_blank BOOLEAN NOT NULL DEFAULT false,
require_justification_if_blank BOOLEAN NOT NULL DEFAULT false,
-- Conditional requirements
conditional_on_field VARCHAR(100),
conditional_value JSONB,
-- Documentation
requirement_rationale TEXT,
regulatory_reference TEXT,
UNIQUE (tenant_id, entity_type, field_name)
);
-- blank_field_justifications table
CREATE TABLE blank_field_justifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Record reference
entity_type VARCHAR(100) NOT NULL,
entity_id UUID NOT NULL,
field_name VARCHAR(100) NOT NULL,
-- Justification
justification_text TEXT NOT NULL,
justified_by UUID NOT NULL REFERENCES users(id),
justified_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Approval (for critical fields)
approved_by UUID REFERENCES users(id),
approved_at TIMESTAMPTZ,
INDEX idx_blank_justifications (tenant_id, entity_type, entity_id)
);
Implementation:
// src/services/validation/completeness-validator.ts
export class CompletenessValidator {
/**
* Validate record completeness
*/
async validateCompleteness(
tenantId: string,
entityType: string,
record: Record<string, any>
): Promise<CompletenessResult> {
// Get field requirements
const requirements = await db.fieldRequirements.findMany({
where: { tenantId, entityType }
});
const violations: FieldViolation[] = [];
const justificationsNeeded: string[] = [];
for (const req of requirements) {
const value = record[req.fieldName];
const isBlank = value === null || value === undefined || value === '';
// Check mandatory field
if (req.isMandatory && isBlank && !req.allowBlank) {
violations.push({
fieldName: req.fieldName,
violation: 'missing_mandatory_field',
message: `Field ${req.fieldName} is mandatory`,
regulatoryReference: req.regulatoryReference
});
}
// Check if justification needed
if (isBlank && req.requireJustificationIfBlank) {
// Check if justification already provided
const justification = await db.blankFieldJustifications.findFirst({
where: {
tenantId,
entityType,
entityId: record.id,
fieldName: req.fieldName
}
});
if (!justification) {
justificationsNeeded.push(req.fieldName);
}
}
}
return {
complete: violations.length === 0 && justificationsNeeded.length === 0,
violations,
justificationsNeeded
};
}
/**
* Add blank field justification
*/
async addBlankFieldJustification(
tenantId: string,
entityType: string,
entityId: string,
fieldName: string,
justificationText: string,
userId: string
): Promise<BlankFieldJustification> {
// Verify field actually is blank
const record = await db[entityType].findUnique({ where: { id: entityId } });
const value = record[fieldName];
if (value !== null && value !== undefined && value !== '') {
throw new Error('Field is not blank - justification not needed');
}
// Create justification
const justification = await db.blankFieldJustifications.create({
data: {
tenantId,
entityType,
entityId,
fieldName,
justificationText,
justifiedBy: userId
}
});
// Audit
await auditService.log({
tenantId,
entityType: 'blank_field_justification',
entityId: justification.id,
action: 'create',
userId,
message: `Blank field justification added for ${fieldName}`
});
return justification;
}
}
8.3 Audit Trail Completeness
Sequence Gap Detection:
-- Detect gaps in audit trail sequence
CREATE OR REPLACE FUNCTION detect_audit_gaps()
RETURNS TABLE(
entity_id UUID,
expected_sequence BIGINT,
actual_sequence BIGINT,
gap_size BIGINT
) AS $$
WITH sequenced AS (
SELECT
entity_id,
occurred_at,
ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY occurred_at) AS expected_seq,
occurred_at_us AS actual_seq
FROM audit_trail
WHERE tenant_id = current_setting('app.tenant_id')::UUID
)
SELECT
entity_id,
expected_seq,
actual_seq,
actual_seq - LAG(actual_seq) OVER (PARTITION BY entity_id ORDER BY expected_seq) AS gap_size
FROM sequenced
WHERE actual_seq - LAG(actual_seq) OVER (PARTITION BY entity_id ORDER BY expected_seq) > 1000 -- More than 1ms gap
$$ LANGUAGE SQL;
9. Principle 7: Consistent
9.1 Definition
Regulatory Requirement:
"Data should be consistent throughout the data lifecycle. Timestamps, formats, and terminology should be standardized." - FDA Data Integrity Guidance
Requirements:
- Standardized date/time formats across modules
- Unit of measure standardization
- Controlled terminology/vocabulary
- Cross-system consistency verification
9.2 Format Standardization
Date/Time Standard: ISO 8601
// src/utils/format-standards.ts
export class FormatStandards {
/**
* ISO 8601 timestamp with microseconds
*/
static formatTimestamp(date: Date): string {
return date.toISOString(); // YYYY-MM-DDTHH:mm:ss.sssZ
}
/**
* ISO 8601 date only
*/
static formatDate(date: Date): string {
return date.toISOString().split('T')[0]; // YYYY-MM-DD
}
/**
* Parse timestamp (strict validation)
*/
static parseTimestamp(timestamp: string): Date {
const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
if (!iso8601Regex.test(timestamp)) {
throw new Error(`Invalid ISO 8601 timestamp: ${timestamp}`);
}
return new Date(timestamp);
}
}
9.3 Unit of Measure Standardization
Controlled UOM Table:
-- units_of_measure table
CREATE TABLE units_of_measure (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Unit identification
uom_code VARCHAR(20) NOT NULL UNIQUE, -- 'kg', 'g', 'mg', 'L', 'mL'
uom_name VARCHAR(100) NOT NULL,
uom_symbol VARCHAR(10) NOT NULL,
-- Category
uom_category VARCHAR(50) NOT NULL, -- 'mass', 'volume', 'temperature', 'time'
-- Base unit conversion
base_unit_code VARCHAR(20) NOT NULL,
conversion_factor DECIMAL(20, 10) NOT NULL, -- Factor to convert to base unit
-- Display
decimal_places INTEGER NOT NULL DEFAULT 2,
-- Standards compliance
is_si_unit BOOLEAN NOT NULL DEFAULT false,
is_preferred BOOLEAN NOT NULL DEFAULT false,
UNIQUE (tenant_id, uom_code)
);
-- Sample UOM data
INSERT INTO units_of_measure (tenant_id, uom_code, uom_name, uom_symbol, uom_category, base_unit_code, conversion_factor, is_si_unit, is_preferred) VALUES
('tenant-123', 'kg', 'Kilogram', 'kg', 'mass', 'kg', 1.0, true, true),
('tenant-123', 'g', 'Gram', 'g', 'mass', 'kg', 0.001, true, false),
('tenant-123', 'mg', 'Milligram', 'mg', 'mass', 'kg', 0.000001, true, false),
('tenant-123', 'L', 'Liter', 'L', 'volume', 'L', 1.0, true, true),
('tenant-123', 'mL', 'Milliliter', 'mL', 'volume', 'L', 0.001, true, false);
-- UOM conversion tracking
CREATE TABLE uom_conversions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Conversion
from_value DECIMAL(20, 10) NOT NULL,
from_uom_code VARCHAR(20) NOT NULL REFERENCES units_of_measure(uom_code),
to_value DECIMAL(20, 10) NOT NULL,
to_uom_code VARCHAR(20) NOT NULL REFERENCES units_of_measure(uom_code),
-- Context
entity_type VARCHAR(100) NOT NULL,
entity_id UUID NOT NULL,
field_name VARCHAR(100) NOT NULL,
-- Audit
converted_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
converted_by UUID NOT NULL REFERENCES users(id),
INDEX idx_uom_conversions (tenant_id, entity_type, entity_id)
);
10. Principle 8: Enduring
10.1 Definition
Regulatory Requirement:
"Records should be durable and preserved throughout the retention period in a form that ensures continued readability." - FDA Data Integrity Guidance
Requirements:
- PDF/A-2b as archival format
- Format migration strategy
- Media migration plan (5-year cycle)
- Annual readability verification
- Technology obsolescence monitoring
10.2 Format Migration Strategy
Migration Plan:
| Current Format | Archival Format | Migration Tool | Verification |
|---|---|---|---|
| DOCX | PDF/A-2b | Pandoc + veraPDF | Visual + hash |
| XLSX | PDF/A-2b + CSV | LibreOffice + custom | Data comparison |
| HTML | PDF/A-2b | Puppeteer | Render comparison |
| Proprietary | PDF/A-2b | Vendor tool + fallback | Manual review |
Implementation:
// src/services/archival/format-migration.service.ts
export class FormatMigrationService {
/**
* Migrate document to archival format (PDF/A-2b)
*/
async migrateToArchival(
documentId: string,
targetDate: Date
): Promise<MigrationResult> {
const document = await db.documents.findUnique({ where: { id: documentId } });
// Determine migration path
const migrationPath = this.getMigrationPath(document.format);
// Execute migration
const archivedContent = await this.executeMigration(
document.content,
document.format,
migrationPath
);
// Verify archival quality
const verification = await this.verifyArchivalQuality(archivedContent);
if (!verification.passed) {
throw new Error(`Archival verification failed: ${verification.failures.join(', ')}`);
}
// Store archived version
const archivedVersion = await this.storeArchivedVersion(
documentId,
archivedContent,
migrationPath,
verification
);
// Log migration
await db.formatMigrations.create({
data: {
documentId,
fromFormat: document.format,
toFormat: 'pdf/a-2b',
migrationPath: migrationPath.join(' -> '),
verificationPassed: verification.passed,
migratedAt: new Date()
}
});
return {
success: true,
archivedVersionId: archivedVersion.id,
verification
};
}
/**
* Verify archival quality
*/
private async verifyArchivalQuality(
content: Buffer
): Promise<ArchivalVerification> {
const checks: ArchivalCheck[] = [];
// PDF/A compliance
const pdfaCheck = await this.checkPDFACompliance(content);
checks.push(pdfaCheck);
// Font embedding
const fontCheck = await this.checkFontEmbedding(content);
checks.push(fontCheck);
// Resolution
const resolutionCheck = await this.checkResolution(content);
checks.push(resolutionCheck);
// Metadata completeness
const metadataCheck = await this.checkMetadata(content);
checks.push(metadataCheck);
return {
passed: checks.every(c => c.passed),
checks,
failures: checks.filter(c => !c.passed).map(c => c.message)
};
}
}
10.3 Readability Verification Schedule
Annual Testing:
-- scheduled_readability_tests table
CREATE TABLE scheduled_readability_tests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
document_id UUID NOT NULL REFERENCES documents(id),
-- Schedule
test_frequency_months INTEGER NOT NULL DEFAULT 12,
next_test_date DATE NOT NULL,
last_test_date DATE,
-- Test results
last_test_result JSONB,
last_test_passed BOOLEAN,
-- Escalation
consecutive_failures INTEGER NOT NULL DEFAULT 0,
escalation_triggered BOOLEAN NOT NULL DEFAULT false,
INDEX idx_readability_tests_schedule (tenant_id, next_test_date)
);
-- Automated test execution
CREATE OR REPLACE FUNCTION execute_readability_test(document_id UUID)
RETURNS JSONB AS $$
DECLARE
test_result JSONB;
BEGIN
-- Attempt to retrieve and render document
test_result := json_build_object(
'document_id', document_id,
'test_date', CURRENT_TIMESTAMP,
'retrieval_success', true, -- Try retrieve from storage
'render_success', true, -- Try render with multiple viewers
'fonts_intact', true, -- Verify fonts still embedded
'images_intact', true, -- Verify images still display
'metadata_intact', true -- Verify metadata present
);
RETURN test_result;
END;
$$ LANGUAGE plpgsql;
11. Principle 9: Available
11.1 Definition
Regulatory Requirement:
"Records must be available for review and inspection throughout the retention period." - FDA Data Integrity Guidance
Requirements:
- Retrieval within regulatory timeframes (typically 24 hours for FDA inspection)
- Disaster recovery with RPO <1 hour
- Geographic redundancy
- Access controls that don't impede authorized retrieval
11.2 Retrieval SLA
Service Level Agreement:
| Record Type | Regulatory Timeframe | BIO-QMS SLA | Storage Tier |
|---|---|---|---|
| Active (current) | Immediate | <1 second | Hot (SSD) |
| Recent (0-2 years) | 1 hour | <30 seconds | Warm (Standard) |
| Archived (2-7 years) | 24 hours | <5 minutes | Cold (Nearline) |
| Long-term (7+ years) | 72 hours | <1 hour | Archive (Coldline) |
Implementation:
// src/services/storage/retrieval.service.ts
export class RetrievalService {
/**
* Retrieve document with SLA tracking
*/
async retrieveDocument(
documentId: string,
userId: string
): Promise<RetrievalResult> {
const startTime = Date.now();
try {
// Get document metadata
const document = await db.documents.findUnique({ where: { id: documentId } });
// Determine storage tier
const tier = this.determineStorageTier(document.createdAt);
const sla = this.getSLA(tier);
// Retrieve content
const content = await this.retrieveFromTier(document.storagePath, tier);
const retrievalTime = Date.now() - startTime;
// Log retrieval
await db.documentRetrievals.create({
data: {
documentId,
userId,
storageTier: tier,
retrievalTimeMs: retrievalTime,
slaTargetMs: sla.targetMs,
slaMet: retrievalTime <= sla.targetMs
}
});
return {
content,
retrievalTime,
sla: sla.targetMs,
slaMet: retrievalTime <= sla.targetMs
};
} catch (error) {
// Log failure
await db.documentRetrievals.create({
data: {
documentId,
userId,
retrievalTimeMs: Date.now() - startTime,
failed: true,
failureReason: error.message
}
});
throw error;
}
}
/**
* Determine storage tier based on document age
*/
private determineStorageTier(createdAt: Date): StorageTier {
const ageYears = (Date.now() - createdAt.getTime()) / (365.25 * 24 * 60 * 60 * 1000);
if (ageYears < 2) return 'hot';
if (ageYears < 7) return 'cold';
return 'archive';
}
/**
* Get SLA for storage tier
*/
private getSLA(tier: StorageTier): { targetMs: number } {
const slas = {
hot: { targetMs: 1000 }, // 1 second
warm: { targetMs: 30000 }, // 30 seconds
cold: { targetMs: 300000 }, // 5 minutes
archive: { targetMs: 3600000 } // 1 hour
};
return slas[tier];
}
}
11.3 Disaster Recovery
RPO and RTO Targets:
| Data Category | RPO (Max Data Loss) | RTO (Max Downtime) | Backup Strategy |
|---|---|---|---|
| Active records | <1 hour | <4 hours | Continuous replication + hourly snapshots |
| Audit trail | <1 hour | <4 hours | Continuous replication + hourly snapshots |
| Archived records | <24 hours | <24 hours | Daily backups + geographic redundancy |
Geographic Redundancy:
# Google Cloud Storage configuration
primary_region: us-central1
secondary_region: us-east1
tertiary_region: europe-west1
storage_class: REGIONAL # Active records
archive_class: COLDLINE # Long-term archives
# Replication
replication:
enabled: true
destination_buckets:
- gs://bio-qms-backup-us-east1
- gs://bio-qms-backup-europe-west1
# Versioning
versioning:
enabled: true
retention_days: 2555 # 7 years
12. Implementation Architecture
12.1 System Components
┌─────────────────────────────────────────────────────────────┐
│ BIO-QMS Platform │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Attribution │ │ Timestamp │ │ Versioning │ │
│ │ Service │ │ Service │ │ Service │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ┌───────▼───────────────────▼───────────────────▼───────┐ │
│ │ ALCOA+ Orchestration Layer │ │
│ └───────┬───────────────────┬───────────────────┬───────┘ │
│ │ │ │ │
│ ┌───────▼───────┐ ┌───────▼───────┐ ┌───────▼───────┐ │
│ │ Validation │ │ Completeness │ │ Consistency │ │
│ │ Engine │ │ Validator │ │ Checker │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ┌───────▼───────────────────▼───────────────────▼───────┐ │
│ │ Audit Trail Service │ │
│ └────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────▼───────────────────────┐ │
│ │ PostgreSQL (RLS + Partitioning) │ │
│ │ - audit_trail (append-only) │ │
│ │ - document_versions (immutable) │ │
│ │ - validation_rules │ │
│ └─────────────────────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────────────────────▼──────────────────────┐ │
│ │ Google Cloud Storage │ │
│ │ - Hot tier (active documents) │ │
│ │ - Cold tier (archived documents) │ │
│ │ - PDF/A-2b archival format │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
12.2 Data Flow
Document Creation with ALCOA+ Controls:
1. User creates document
↓
2. Attribution Service captures:
- User ID, name, email, role
- Session ID, IP, device
- Timestamp from NTP server
↓
3. Validation Engine checks:
- Field completeness
- Data accuracy
- Format consistency
↓
4. Versioning Service creates:
- Version 1 (original)
- Content hash (SHA-256)
- Immutable record
↓
5. Format Preservation:
- Convert to PDF/A-2b
- Embed fonts
- Verify compliance
↓
6. Storage:
- Save to GCS (hot tier)
- Geographic replication
- Retention policy applied
↓
7. Audit Trail:
- Log all actions
- Append-only
- Server timestamp
13. Validation and Testing
13.1 Test Plan Overview
Test Categories:
| Category | Tests | Coverage | Automation |
|---|---|---|---|
| Unit Tests | 250+ | Per-principle logic | Jest |
| Integration Tests | 150+ | End-to-end flows | Jest + Supertest |
| Compliance Tests | 50+ | Regulatory requirements | Custom framework |
| Performance Tests | 30+ | SLA verification | k6 |
| Security Tests | 40+ | Access controls | OWASP ZAP |
13.2 Compliance Test Matrix
| Principle | Test ID | Test Case | Pass Criteria |
|---|---|---|---|
| Attributable | AT-001 | User attribution captured | user_id, user_name, user_email present |
| Attributable | AT-002 | Service account delegation | delegated_by populated |
| Attributable | AT-003 | Signature meaning recorded | meaning + meaningText present |
| Legible | LG-001 | PDF/A compliance | veraPDF validation passed |
| Legible | LG-002 | Font embedding | All fonts embedded=true |
| Legible | LG-003 | Cross-platform rendering | <1% pixel difference |
| Contemporaneous | CT-001 | Server timestamp only | Client timestamp rejected |
| Contemporaneous | CT-002 | NTP synchronization | Clock drift <100ms |
| Contemporaneous | CT-003 | Microsecond precision | Timestamp precision to μs |
| Original | OR-001 | Original identification | is_original=true for v1 |
| Original | OR-002 | True copy verification | Hash match 100% |
| Original | OR-003 | Amendment tracking | supersedes_version populated |
| Accurate | AC-001 | Field validation | Regex/range rules enforced |
| Accurate | AC-002 | Duplicate detection | >95% similarity flagged |
| Accurate | AC-003 | Calculation verification | Computed = stored (±0.01%) |
| Complete | CP-001 | Mandatory fields | All mandatory fields present |
| Complete | CP-002 | Blank justification | Justification recorded |
| Consistent | CS-001 | Date format | ISO 8601 enforced |
| Consistent | CS-002 | UOM standardization | Only allowed UOMs accepted |
| Enduring | EN-001 | PDF/A longevity | 5-year-old doc still readable |
| Enduring | EN-002 | Format migration | Migration verified |
| Available | AV-001 | Retrieval SLA | <1s for hot, <5min for cold |
| Available | AV-002 | Geographic redundancy | 3 regions verified |
14. Monitoring and Metrics
14.1 ALCOA+ Dashboard
Key Performance Indicators:
| Metric | Target | Yellow | Red | Source |
|---|---|---|---|---|
| Attribution Completeness | 100% | <99.9% | <99.5% | audit_trail |
| PDF/A Compliance Rate | 100% | <99.9% | <99.5% | document_formats |
| Clock Drift | <50ms | 50-100ms | >100ms | time_sync_monitor |
| True Copy Verification | 100% | <99.9% | <99.5% | document_copies |
| Data Accuracy Rate | >99.5% | 99.0-99.5% | <99.0% | accuracy_metrics |
| Field Completeness | 100% | <99.9% | <99.5% | completeness_validator |
| Format Consistency | 100% | <99.9% | <99.5% | format_validator |
| Readability Test Pass Rate | 100% | <99.9% | <99.5% | readability_tests |
| Retrieval SLA Compliance | >99% | 95-99% | <95% | retrieval_service |
Dashboard Query:
// src/dashboards/alcoa-plus-dashboard.ts
export async function getALCOAPlusMetrics(
tenantId: string,
dateRange: DateRange
): Promise<ALCOAPlusMetrics> {
return {
attributable: {
completeness: await getAttributionCompleteness(tenantId, dateRange),
serviceAccountDelegation: await getServiceAccountDelegationRate(tenantId, dateRange),
signatureMeaningRecorded: await getSignatureMeaningRate(tenantId, dateRange)
},
legible: {
pdfaCompliance: await getPDFAComplianceRate(tenantId, dateRange),
fontEmbedding: await getFontEmbeddingRate(tenantId, dateRange),
renderTestPass: await getRenderTestPassRate(tenantId, dateRange)
},
contemporaneous: {
clockDrift: await getCurrentClockDrift(),
serverTimestampEnforcement: await getServerTimestampRate(tenantId, dateRange),
timestampPrecision: await getTimestampPrecisionMetrics(tenantId, dateRange)
},
original: {
originalIdentification: await getOriginalIdentificationRate(tenantId, dateRange),
trueCopyVerification: await getTrueCopyVerificationRate(tenantId, dateRange),
versionTracking: await getVersionTrackingCompleteness(tenantId, dateRange)
},
accurate: {
validationPassRate: await getValidationPassRate(tenantId, dateRange),
duplicateDetection: await getDuplicateDetectionRate(tenantId, dateRange),
calculationAccuracy: await getCalculationAccuracyRate(tenantId, dateRange)
},
complete: {
mandatoryFieldCompliance: await getMandatoryFieldCompliance(tenantId, dateRange),
blankJustificationRate: await getBlankJustificationRate(tenantId, dateRange),
auditTrailCompleteness: await getAuditTrailCompleteness(tenantId, dateRange)
},
consistent: {
dateFormatCompliance: await getDateFormatCompliance(tenantId, dateRange),
uomStandardization: await getUOMStandardizationRate(tenantId, dateRange),
terminologyConsistency: await getTerminologyConsistency(tenantId, dateRange)
},
enduring: {
archivalFormatRate: await getArchivalFormatRate(tenantId, dateRange),
readabilityTestPass: await getReadabilityTestPassRate(tenantId, dateRange),
formatMigrationSuccess: await getFormatMigrationSuccessRate(tenantId, dateRange)
},
available: {
retrievalSlaCompliance: await getRetrievalSLACompliance(tenantId, dateRange),
disasterRecoveryRPO: await getDisasterRecoveryRPO(tenantId),
geographicRedundancy: await getGeographicRedundancyStatus(tenantId)
}
};
}
14.2 Alerting Rules
# alerting-rules.yaml
- name: alcoa_plus_alerts
rules:
- alert: AttributionCompletenessLow
expr: attribution_completeness_rate < 0.995
severity: critical
message: "Attribution completeness below 99.5%"
- alert: PDFAComplianceFailure
expr: pdfa_compliance_rate < 0.999
severity: critical
message: "PDF/A compliance rate below 99.9%"
- alert: ClockDriftExceeded
expr: clock_drift_ms > 100
severity: critical
message: "Server clock drift exceeds 100ms"
- alert: DataAccuracyBelowTarget
expr: data_accuracy_rate < 0.995
severity: warning
message: "Data accuracy rate below 99.5% target"
- alert: RetrievalSLABreach
expr: retrieval_sla_compliance < 0.99
severity: warning
message: "Retrieval SLA compliance below 99%"
15. Regulatory Mapping
15.1 FDA 21 CFR Part 11 Mapping
| CFR Section | Requirement | ALCOA+ Principle | Implementation | Status |
|---|---|---|---|---|
| §11.10(a) | Validation of systems | All | Validation test framework | ✅ |
| §11.10(b) | Record retention | Legible, Enduring, Available | PDF/A + GCS archival | ✅ |
| §11.10(c) | Record protection | Original, Accurate | Versioning + immutability | ✅ |
| §11.10(d) | Limited access | Attributable | RBAC + audit trail | ✅ |
| §11.10(e) | Audit trail | Attributable, Contemporaneous | audit_trail table | ✅ |
| §11.10(f) | Operational checks | Accurate, Complete | Validation engine | ✅ |
| §11.10(g) | Authority checks | Attributable | RBAC enforcement | ✅ |
| §11.10(h) | Device checks | Accurate | Input validation | ✅ |
| §11.10(k)(1) | System documentation | All | This specification | ✅ |
| §11.10(k)(2) | Validation documentation | All | Validation protocols | 🟡 |
| §11.50(a) | Signature manifestation | Attributable | ElectronicSignature model | ✅ |
| §11.70 | Signature linking | Original | Cryptographic binding | ✅ |
15.2 FDA Data Integrity Guidance (2018) Mapping
| Guidance Section | Requirement | ALCOA+ Principle | Implementation | Status |
|---|---|---|---|---|
| IV.B.1 | Attributable | Attributable | Attribution service | ✅ |
| IV.B.2 | Legible | Legible | PDF/A preservation | ✅ |
| IV.B.3 | Contemporaneous | Contemporaneous | Server-side NTP timestamps | ✅ |
| IV.B.4 | Original | Original | Versioning + true copy | ✅ |
| IV.B.5 | Accurate | Accurate | Validation engine | ✅ |
| IV.C.1 | Complete | Complete | Mandatory fields + justification | ✅ |
| IV.C.2 | Consistent | Consistent | Format standardization | ✅ |
| IV.C.3 | Enduring | Enduring | Archival + readability testing | ✅ |
| IV.C.4 | Available | Available | Retrieval SLA + DR | ✅ |
15.3 WHO Annex 5 (TRS 996) Mapping
| Annex 5 Section | Requirement | ALCOA+ Principle | Implementation | Status |
|---|---|---|---|---|
| 7.1 | Data integrity fundamentals | All | Complete ALCOA+ implementation | ✅ |
| 7.2 | Metadata | Attributable | Full metadata capture | ✅ |
| 7.3 | Audit trail | Attributable, Contemporaneous | Append-only audit_trail | ✅ |
| 7.4 | Data backup | Enduring, Available | GCS multi-region | ✅ |
| 7.5 | Data retention | Enduring | Retention policies | ✅ |
| 7.6 | Data integrity risks | Accurate, Complete | Risk-based validation | ✅ |
15.4 EMA Data Integrity Mapping
| EMA Section | Requirement | ALCOA+ Principle | Implementation | Status |
|---|---|---|---|---|
| 1.1 | ALCOA principles | Core 5 | Attributable through Accurate | ✅ |
| 1.2 | ALCOA+ | Extended 4 | Complete through Available | ✅ |
| 2.1 | Metadata | Attributable | Comprehensive metadata | ✅ |
| 2.2 | Audit trail | Contemporaneous | Server-side timestamps | ✅ |
| 2.3 | Data review | Accurate | Validation + verification | ✅ |
16. Appendices
Appendix A: Acronyms and Abbreviations
| Acronym | Full Form |
|---|---|
| ALCOA | Attributable, Legible, Contemporaneous, Original, Accurate |
| ALCOA+ | ALCOA + Complete, Consistent, Enduring, Available |
| CFR | Code of Federal Regulations |
| CGMP | Current Good Manufacturing Practice |
| DR | Disaster Recovery |
| EMA | European Medicines Agency |
| FDA | Food and Drug Administration |
| GCS | Google Cloud Storage |
| GxP | Good Practice (GMP, GLP, GCP, GDP, GVP) |
| MHRA | Medicines and Healthcare products Regulatory Agency |
| NTP | Network Time Protocol |
| PDF/A | PDF Archive (ISO 19005) |
| RBAC | Role-Based Access Control |
| RPO | Recovery Point Objective |
| RTO | Recovery Time Objective |
| SLA | Service Level Agreement |
| UOM | Unit of Measure |
| WHO | World Health Organization |
Appendix B: References
- FDA 21 CFR Part 11 - Electronic Records; Electronic Signatures (1997)
- FDA Guidance: Data Integrity and Compliance with Drug CGMP (December 2018)
- WHO Technical Report Series No. 996, Annex 5: Guidance on Good Data and Record Management Practices (2016)
- EMA Guideline on Data Integrity (2016)
- MHRA GXP Data Integrity Guidance and Definitions (March 2018)
- ISO 19005-2:2011 - Document management - PDF/A-2 (PDF Archive)
- ISO 8601:2019 - Date and time format
Appendix C: Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2026-02-16 | CISO Office | Initial release |
End of Document
Document ID: CODITECT-BIO-ALCOA-001 Total Pages: 75 Word Count: ~25,000 Classification: Internal - Restricted Next Review: 2027-02-16