Blockchain Document Management System Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Blockchain Document Management System Development
Complex
from 1 week to 3 months
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Development of Document Management System on Blockchain

Classic task: notarized contract, work acceptance certificate or financial document must be verifiable by any party without contacting central organization. Existing solutions — centralized registries or PKI-infrastructure with CA — work while organizations trust each other and central regulator. In cross-border scenarios or disputes this condition breaks.

Blockchain here doesn't replace document storage system — it replaces notary. Document lives in secure storage (IPFS, S3), and fact of its existence at given time, immutability and list of authorized signers fixed on-chain. This fundamental separation must be understood from start.

Cryptographic Foundations of Document Flow

Document Commitment

Any proof-of-existence system built on one principle: hash(document) published on-chain. But implementation matters in details.

Simple hash (anti-pattern):

mapping(bytes32 => uint256) public timestamps;

function notarize(bytes32 docHash) external {
    timestamps[docHash] = block.timestamp;
}

Problem: if two documents differ only in timestamp — they give different hashes, both will be "notarized". Problem deeper: if document revoked — no mechanism to reflect this. No link between hash and signatory — anyone can notarize someone else's document.

Correct structure:

struct DocumentRecord {
    bytes32 contentHash;      // keccak256 from content
    bytes32 metadataHash;     // hash from metadata (without content)
    address registrant;       // who registered
    uint256 registeredAt;
    DocumentStatus status;
    bytes32[] signatories;    // DID hashes of signatories
    uint256 expiresAt;        // 0 = indefinite
}

enum DocumentStatus { PENDING, ACTIVE, REVOKED, EXPIRED }

mapping(bytes32 => DocumentRecord) public documents;
// docId = keccak256(contentHash + registrant + registeredAt)

event DocumentRegistered(
    bytes32 indexed docId,
    bytes32 indexed contentHash,
    address indexed registrant
);

event DocumentSigned(
    bytes32 indexed docId,
    bytes32 indexed signatoryDid,
    bytes signature
);

event DocumentRevoked(
    bytes32 indexed docId,
    address revokedBy,
    string reason
);

Cryptographic Timestamping (RFC 3161 On-Chain)

For legally significant documents important not just fact of hash publication but proof document existed at time T. Problem: block.timestamp in EVM manipulable by miner/validator within ~15 seconds. For most use cases insignificant, but for legal documents better use:

  • Block hash commitment: publish keccak256(docHash || blockHash(N-1)) — tie to specific block, not just timestamp
  • Chainlink or other VRF oracle for additional entropy source
  • Anchor to Bitcoin via OP_RETURN — Bitcoin timestamping via services like OpenTimestamps
function registerWithBlockCommitment(
    bytes32 contentHash,
    bytes32 metadataHash
) external returns (bytes32 docId) {
    // Tie to hash of previous block
    bytes32 blockCommitment = keccak256(abi.encodePacked(
        contentHash,
        blockhash(block.number - 1),
        block.timestamp,
        msg.sender
    ));
    
    docId = keccak256(abi.encodePacked(contentHash, msg.sender, block.timestamp));
    
    documents[docId] = DocumentRecord({
        contentHash: contentHash,
        metadataHash: metadataHash,
        registrant: msg.sender,
        registeredAt: block.timestamp,
        status: DocumentStatus.ACTIVE,
        signatories: new bytes32[](0),
        expiresAt: 0
    });
    
    emit DocumentRegistered(docId, contentHash, msg.sender);
    return docId;
}

Electronic Signature and Multi-Party Signing Workflow

EIP-712 Structured Signatures

For document signing use EIP-712 instead of simple ecrecover — typed structured data. This gives readable prompts in MetaMask and protection from cross-chain replay:

bytes32 private constant DOCUMENT_SIGNING_TYPEHASH = keccak256(
    "DocumentSigning(bytes32 docId,bytes32 contentHash,uint256 signedAt,string signatoryRole)"
);

struct DocumentSigning {
    bytes32 docId;
    bytes32 contentHash;
    uint256 signedAt;
    string signatoryRole;
}

function signDocument(
    bytes32 docId,
    string calldata signatoryRole,
    uint256 signedAt,
    bytes calldata signature
) external {
    DocumentRecord storage doc = documents[docId];
    require(doc.status == DocumentStatus.ACTIVE, "Document not active");
    require(signedAt <= block.timestamp, "Future timestamp");
    require(block.timestamp - signedAt < 3600, "Signature too old");
    
    // Recover signer from EIP-712 signature
    bytes32 structHash = keccak256(abi.encode(
        DOCUMENT_SIGNING_TYPEHASH,
        docId,
        doc.contentHash,
        signedAt,
        keccak256(bytes(signatoryRole))
    ));
    
    bytes32 digest = _hashTypedDataV4(structHash);
    address signer = ECDSA.recover(digest, signature);
    
    require(isAuthorizedSignatory(signer, docId), "Not authorized signatory");
    require(!hasAlreadySigned(signer, docId), "Already signed");
    
    doc.signatories.push(keccak256(abi.encode(signer)));
    
    emit DocumentSigned(docId, keccak256(abi.encode(signer)), signature);
    
    // Check completion
    if (_checkSigningComplete(docId)) {
        doc.status = DocumentStatus.ACTIVE;
        emit DocumentFullySigned(docId);
    }
}

Multi-Party Approval Workflow

For complex documents (sale contract, work acceptance) need workflow with signing order:

struct SigningWorkflow {
    bytes32 docId;
    SigningStep[] steps;
    uint256 currentStep;
    bool isSequential;  // sequential or parallel signature
}

struct SigningStep {
    address[] requiredSigners;
    uint256 threshold;      // how many of requiredSigners must sign
    uint256 deadline;
    bool completed;
}

Example: work acceptance certificate

  1. Contractor signs (step 1, mandatory)
  2. Customer's technical supervisor signs (step 2, mandatory)
  3. Customer's financial director signs OR any of two authorized representatives (step 3, threshold 1 of 2)

Document Storage: Hybrid Architecture

IPFS + Encryption

Documents stored encrypted in IPFS. Encryption key managed separately — via on-chain access control:

import { create } from 'ipfs-http-client'
import { encrypt, decrypt } from '@metamask/eth-sig-util'

async function uploadEncryptedDocument(
  file: Buffer,
  docId: string,
  authorizedAddresses: string[]
): Promise<string> {
  // Generate symmetric key
  const aesKey = crypto.getRandomValues(new Uint8Array(32))
  
  // Encrypt document
  const iv = crypto.getRandomValues(new Uint8Array(12))
  const cipher = await crypto.subtle.importKey('raw', aesKey, 'AES-GCM', false, ['encrypt'])
  const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cipher, file)
  
  // Encrypt AES key with public keys of each authorized address
  const encryptedKeys: Record<string, string> = {}
  for (const address of authorizedAddresses) {
    const pubKey = await getPublicEncryptionKey(address)  // from MetaMask
    encryptedKeys[address] = encryptForPublicKey(pubKey, Buffer.from(aesKey))
  }
  
  // Publish to IPFS: encrypted document + encrypted keys bundle
  const ipfs = create({ url: process.env.IPFS_API })
  const { cid } = await ipfs.add(JSON.stringify({
    encrypted: Buffer.from(encrypted).toString('base64'),
    iv: Buffer.from(iv).toString('base64'),
    encryptedKeys,
  }))
  
  return cid.toString()
}

Document Versioning

Documents in business workflow are edited. Need chain of versions with ability to prove version V2 is update of V1:

struct DocumentVersion {
    bytes32 contentHash;
    bytes32 previousVersionId;  // 0x0 for first version
    address updatedBy;
    uint256 updatedAt;
    string changeDescription;
}

mapping(bytes32 => bytes32[]) public documentVersions;  // docId → version hashes
mapping(bytes32 => DocumentVersion) public versionDetails;

function publishNewVersion(
    bytes32 docId,
    bytes32 newContentHash,
    string calldata changeDescription
) external onlyDocumentOwner(docId) {
    bytes32[] storage versions = documentVersions[docId];
    bytes32 previousHash = versions.length > 0 ? versions[versions.length - 1] : bytes32(0);
    
    bytes32 versionId = keccak256(abi.encodePacked(
        docId, newContentHash, block.timestamp, versions.length
    ));
    
    versionDetails[versionId] = DocumentVersion({
        contentHash: newContentHash,
        previousVersionId: previousHash,
        updatedBy: msg.sender,
        updatedAt: block.timestamp,
        changeDescription: changeDescription
    });
    
    versions.push(versionId);
    emit DocumentVersionPublished(docId, versionId, versions.length - 1);
}

Access Control and Confidentiality

Zero-Knowledge Disclosure

In some cases need prove fact about document without revealing content. Example: prove contract sum > X without revealing exact sum.

Solved via ZK-proof (e.g., Groth16 or PLONK):

// Circom circuit: proof that contract sum > threshold
pragma circom 2.0.0;

template ContractAmountProof() {
    signal input contractAmount;  // private input
    signal input threshold;       // public
    signal input documentHash;    // public (verifier knows document)
    
    signal output isAboveThreshold;
    
    // Check that sum > threshold
    component gt = GreaterThan(64);
    gt.in[0] <== contractAmount;
    gt.in[1] <== threshold;
    isAboveThreshold <== gt.out;
    
    // Binding to specific document (commitment)
    // Verifier convinced we know contractAmount for this document
}

Selective Disclosure via NFC/QR

For physical documents (vehicle passport, quality certificate): QR-code contains docId. Scanning QR, verifier requests on-chain record and gets only public metadata. For access to full document need authorized wallet.

Integration with Legacy Systems

Most enterprise clients have ERP/ECM systems (SAP, 1C, OpenText). Integration via:

Webhook approach: on document creation in ECM — webhook called → oracle service → on-chain registration. Transparent to ERP users.

OpenAPI adapter:

openapi: "3.0.0"
info:
  title: Document Notarization API
paths:
  /documents/register:
    post:
      summary: Register document hash on blockchain
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                documentId: { type: string }
                contentHash: { type: string, pattern: "^0x[0-9a-f]{64}$" }
                metadata: { type: object }
                signatories: 
                  type: array
                  items: { type: string }  # Ethereum addresses

Blockchain-agnostic layer: develop abstraction over specific blockchain. Client receives documentId — UUID in familiar format. UUID → on-chain docId mapping stored in PostgreSQL. This allows blockchain migration without contract API changes.

Legal Aspects

Blockchain notarization has different legal status in different jurisdictions:

  • EU (eIDAS 2.0) — recognizes electronic signatures, but blockchain timestamp ≠ qualified electronic signature automatically
  • USA — ESIGN Act recognizes electronic signatures, but for specific use cases (mortgage, notary) requires more
  • UAE — Dubai Blockchain Strategy, active blockchain document adoption

For legally binding documents develop hybrid approach: blockchain timestamp + qualified electronic signature (QES) via certified CA.

Project Stages

Phase Content Duration
Requirements Analysis of document flow, document types, participants, jurisdiction 2–3 weeks
Core contracts Document registry, signing workflow, access control 3–4 weeks
Storage layer IPFS integration, encryption, versioning 2–3 weeks
API & Integration REST API, webhooks, ERP-connectors 3–4 weeks
Frontend Signing interface, verification, management 3–4 weeks
Security audit Smart contracts + backend 2–3 weeks
Pilot Launch with real documents, feedback 2–3 weeks

Total: 17–24 weeks. Critical path — ERP system integration and legal expertise in specific jurisdiction.