Implementation of Signed Documents Storage with Audit Log
Signed document storage is not simply "put file in folder". Requirements: immutability, integrity, availability, audit of every action and compliance with archival storage legislation. Violation of any of these requirements questions the legal validity of documents.
Storage Principles
Immutability — signed document cannot be changed. S3 versioning with MFA Delete or Write-Once-Read-Many (WORM) storage.
Integrity — each access checks content matches stored hash.
Separation — signed documents stored separately from working drafts. Different S3 buckets with different access policies.
Backup — cross-region replication. Loss of signed contract is legal and reputational risk.
Document Storage
// Service for uploading to immutable storage
class DocumentStorageService {
async storeSignedDocument(
documentBytes: Buffer,
metadata: DocumentMetadata
): Promise<StoredDocument> {
// Document hash — immutable content identifier
const contentHash = crypto.createHash('sha256').update(documentBytes).digest('hex');
// Key includes hash for deduplication
const s3Key = `signed/${metadata.documentId}/${contentHash}.pdf`;
await this.s3.putObject({
Bucket: process.env.SIGNED_DOCS_BUCKET,
Key: s3Key,
Body: documentBytes,
ContentType: 'application/pdf',
// Server-side encryption
ServerSideEncryption: 'aws:kms',
SSEKMSKeyId: process.env.KMS_KEY_ID,
// Object Lock prevents deletion/modification
ObjectLockMode: 'COMPLIANCE',
ObjectLockRetainUntilDate: addYears(new Date(), 10),
Metadata: {
'document-id': metadata.documentId,
'signer-id': metadata.signerId,
'signed-at': metadata.signedAt.toISOString(),
'content-hash': contentHash,
},
}).promise();
return {
s3Key,
contentHash,
storageUrl: `s3://${process.env.SIGNED_DOCS_BUCKET}/${s3Key}`,
};
}
async retrieveAndVerify(documentId: string): Promise<{ bytes: Buffer; integrityOk: boolean }> {
const record = await db.signedDocuments.findByDocumentId(documentId);
const object = await this.s3.getObject({
Bucket: process.env.SIGNED_DOCS_BUCKET,
Key: record.s3Key,
}).promise();
const bytes = object.Body as Buffer;
const currentHash = crypto.createHash('sha256').update(bytes).digest('hex');
const integrityOk = currentHash === record.contentHash;
if (!integrityOk) {
await this.alertIntegrityViolation(documentId, record.contentHash, currentHash);
}
return { bytes, integrityOk };
}
}
Database Schema for Documents
CREATE TABLE signed_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID REFERENCES documents(id),
version INT NOT NULL DEFAULT 1,
s3_key VARCHAR(1000) NOT NULL UNIQUE,
content_hash CHAR(64) NOT NULL, -- SHA-256
file_size_bytes BIGINT,
stored_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ, -- For documents with limited period
deleted_at TIMESTAMPTZ, -- Soft delete
delete_reason TEXT,
delete_by UUID REFERENCES users(id)
);
-- Signatures on document
CREATE TABLE document_signatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
signed_doc_id UUID REFERENCES signed_documents(id),
signer_id UUID REFERENCES users(id),
signer_role VARCHAR(100), -- 'initiator', 'approver', 'witness'
signature_type VARCHAR(50), -- 'drawn', 'text', 'sms', 'kep'
signature_data JSONB, -- Depends on type
document_hash_at_signing CHAR(64), -- Hash at signing moment
signed_at TIMESTAMPTZ DEFAULT NOW(),
ip_address INET,
user_agent TEXT
);
Audit Log
Every action with document is recorded in immutable journal:
CREATE TABLE document_audit_log (
id BIGSERIAL PRIMARY KEY, -- Auto-increment for order
document_id UUID NOT NULL,
actor_id UUID REFERENCES users(id),
actor_type VARCHAR(50) DEFAULT 'user', -- 'user', 'system', 'api'
action VARCHAR(200) NOT NULL,
-- Examples: 'document.created', 'document.viewed', 'document.signed',
-- 'document.downloaded', 'document.shared', 'document.revoked'
details JSONB DEFAULT '{}',
ip_address INET,
user_agent TEXT,
session_id UUID,
occurred_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for fast document search
CREATE INDEX ON document_audit_log (document_id, occurred_at DESC);
CREATE INDEX ON document_audit_log (actor_id, occurred_at DESC);
-- Trigger prevents deletion of audit records
CREATE RULE no_delete_audit AS ON DELETE TO document_audit_log DO INSTEAD NOTHING;
// Log every action
async function auditLog(documentId, actorId, action, details = {}) {
await db.documentAuditLog.create({
documentId,
actorId,
action,
details,
ipAddress: request?.ip,
userAgent: request?.headers?.['user-agent'],
sessionId: request?.session?.id,
occurredAt: new Date(),
});
}
// Middleware: automatic log on download
app.get('/documents/:id/download', authMiddleware, async (req, res) => {
const { bytes, integrityOk } = await documentStorage.retrieveAndVerify(req.params.id);
await auditLog(req.params.id, req.user.id, 'document.downloaded', { integrityOk });
res.setHeader('Content-Disposition', `attachment; filename="document-${req.params.id}.pdf"`);
res.send(bytes);
});
Document Access
Signed documents should not be accessible via direct S3 URLs. Only through temporary presigned URLs, generated by server after permission check and audit log recording:
async function getDocumentDownloadUrl(documentId, userId) {
await checkDocumentAccess(documentId, userId); // Throws 403 if no access
const record = await db.signedDocuments.findByDocumentId(documentId);
const url = await s3.getSignedUrlPromise('getObject', {
Bucket: process.env.SIGNED_DOCS_BUCKET,
Key: record.s3Key,
Expires: 300, // 5 minutes
ResponseContentDisposition: `attachment; filename="document.pdf"`,
});
await auditLog(documentId, userId, 'document.viewed');
return url;
}
Storage Periods
| Document Type | Storage Period | Basis |
|---|---|---|
| Sales contracts | 10 years | Civil Code |
| Employment contracts | 50 years | Federal Law No. 125 |
| HR documents | 75 years | Archival legislation |
| Data processing consents | 3 years after revocation | Federal Law No. 152 |
Automatic setting of expires_at when document created based on its type.
Implementation Timeline
Storage with S3 Object Lock, hash verification and audit log — 5–7 days. Access control with presigned URLs and automatic logging — 2–3 days. Document action history interface — 2–3 days.







