Signed Documents Storage with Audit Log on Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

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.