Skip to main content
CCDigital integrates Hyperledger Fabric to provide an immutable, decentralized audit trail for document registration and access events.

Architecture Overview

The Fabric integration layer consists of:

Ledger Registry

Records document metadata and state changes on the Fabric ledgerScript: sync-db-to-ledger.js

Audit Log

Captures access events with actor, action, and result detailsScript: record-access-event.js
All blockchain operations are invoked through Java services that execute Node.js scripts via ExternalToolsService.

Document Registration

Documents are registered on Fabric when citizens approve access requests or during manual synchronization.

Synchronization Workflows

1

Approval Trigger

When a citizen approves an access request, the system automatically syncs their documents to Fabric:
// AccessRequestService.java:497
private void syncApprovedPersonDocumentsToFabric(AccessRequest request) {
    Person person = request.getPerson();
    String idType = person.getIdType() != null ? person.getIdType().name() : null;
    String idNumber = person.getIdNumber();
    
    ExternalToolsService.ExecResult result = 
        externalToolsService.runFabricSyncPerson(idType, idNumber);
    
    if (!result.isOk()) {
        throw new IllegalArgumentException(
            "No se pudo registrar la aprobación en Fabric. Intente nuevamente. " +
            "Detalle: " + firstNonBlankLine(result.getStderr(), "Error de sincronización")
        );
    }
}
If sync fails, the approval is rolled back.
2

Admin Sync

Administrators can manually synchronize documents from the admin dashboard at /admin/sync:
  • Sync All - Registers all approved documents in the database
  • Sync Person - Registers documents for a specific person by id_type and id_number
// ExternalToolsService.java
public ExecResult runFabricSyncAll() {
    List<String> cmd = List.of(nodeBin, syncAllScript);
    return exec(cmd, fabricWorkdir, Map.of());
}

public ExecResult runFabricSyncPerson(String idType, String idNumber) {
    List<String> cmd = List.of(nodeBin, syncPersonScript, idType, idNumber);
    return exec(cmd, fabricWorkdir, Map.of());
}
3

Chaincode Invocation

The Node.js script invokes the Fabric chaincode to store document metadata:Script: sync-db-to-ledger.jsMetadata includes:
  • Document ID (composite key: idType:idNumber:docId)
  • Title
  • Issue date
  • Expiry date
  • File path
  • File size
  • SHA256 hash
  • Issuing entity
  • Status (VIGENTE, VENCIDO, etc.)

Script Configuration

Fabric sync scripts:
Environment VariablePurposeDefault
FABRIC_WORKDIRDirectory containing Fabric client scripts-
FABRIC_NODE_BINNode.js executablenode
FABRIC_SYNC_ALL_SCRIPTGlobal sync scriptsync-db-to-ledger.js
FABRIC_SYNC_PERSON_SCRIPTPerson-specific sync scriptsync-person-to-ledger.js
EXTERNAL_TOOLS_TIMEOUT_SECONDSScript execution timeout300

Querying Fabric Ledger

The system queries Fabric to list documents for verification and display purposes.

Document Listing

// FabricLedgerCliService.java:82
public String listDocsRaw(String idType, String idNumber) {
    List<String> cmd = List.of(nodeBin, listDocsScript, idType, idNumber);
    
    ExternalToolsService.ExecResult res = externalToolsService.exec(cmd, fabricWorkdir, Map.of());
    
    if (res.getExitCode() != 0) {
        throw new RuntimeException("Fabric listDocs falló (exit=" + res.getExitCode() + "): " 
                                  + res.getStderr());
    }
    return res.getStdout();
}
Script: list-docs.js Command format:
node list-docs.js <idType> <idNumber>

Parsed Document View

The raw JSON output is parsed into FabricDocView records:
// FabricLedgerCliService.java:115
public List<FabricDocView> listDocsView(String idType, String idNumber) {
    try {
        String stdout = listDocsRaw(idType, idNumber);
        String json = extractJsonArray(stdout); // Extracts first [...] from output
        
        List<Map<String, Object>> items = mapper.readValue(
            json, new TypeReference<List<Map<String, Object>>>() {}
        );
        
        return items.stream().map(m -> {
            String docId = asString(m.get("docId"));
            String title = asString(m.get("title"));
            String issuingEntity = asString(m.get("issuingEntity"));
            String filePath = asString(m.get("filePath"));
            Long sizeBytes = asLong(m.get("sizeBytes"));
            String createdAt = asString(m.get("createdAt"));
            
            return new FabricDocView(
                docId, title, issuingEntity, null, createdAt, sizeBytes, filePath
            );
        }).toList();
        
    } catch (Exception e) {
        throw new RuntimeException("No se pudo parsear listDocs desde Fabric", e);
    }
}
DTO: FabricDocView.java
public record FabricDocView(
    String docId,
    String title,
    String issuingEntity,
    String status,
    String createdAtHuman,
    Long sizeBytes,
    String filePath
) {
    public String sizeHuman() {
        if (sizeBytes == null) return "N/A";
        return formatBytes(sizeBytes);
    }
}

Access Event Recording

Every document access operation is logged to Fabric with full context.

Event Types

CCDigital records the following event types:
Event TypeTriggered ByPurpose
REQUEST_CREATEDAccess request creationTrack consent workflow initiation
DOC_VERIFY_ON_REQUESTRequest approvalVerify document presence in Fabric
DOC_VIEW_GRANTEDDocument viewLog successful document access
DOC_DOWNLOAD_GRANTEDDocument downloadLog file download event
DOC_BLOCK_TRACE_QUERYBlockchain trace queryTrack metadata access
DOC_ACCESS_CHECKAuthorization validationRecord access attempts
USER_ACCESS_STATE_CHANGEAdmin state updateLog account enable/suspend/disable

Recording Audit Events

// FabricAuditCliService.java
@Service
public class FabricAuditCliService {
    
    public record AuditCommand(
        String idType,              // Person's ID type
        String idNumber,            // Person's ID number
        String eventType,           // Event classification
        Long accessRequestId,       // Related access request (if any)
        Long personDocumentId,      // Related document (if any)
        String fabricDocId,         // Fabric ledger doc reference
        String documentTitle,       // Human-readable doc title
        Long entityId,              // Actor entity ID
        String entityName,          // Actor entity name
        String action,              // Technical action name
        String result,              // OK or FAIL
        String reason,              // Explanation or error detail
        String actorType,           // ISSUER, USER, ADMIN, SYSTEM
        String actorId,             // Actor identifier
        String source               // CCDIGITAL_SPRING, etc.
    ) {}
    
    public void recordEvent(AuditCommand cmd) {
        List<String> command = List.of(
            nodeBin,
            recordAccessScript,
            cmd.idType(),
            cmd.idNumber(),
            cmd.eventType(),
            String.valueOf(cmd.accessRequestId()),
            // ... all other parameters
        );
        
        ExternalToolsService.ExecResult result = 
            externalToolsService.exec(command, fabricWorkdir, Map.of());
        
        if (result.getExitCode() != 0) {
            throw new RuntimeException(
                "Fabric audit recording failed: " + result.getStderr()
            );
        }
    }
}
Script: record-access-event.js

Audit Recording Modes

CCDigital supports two audit recording strategies:
Audit failure causes the entire operation to fail.
// AccessRequestService.java:736
private void recordAuditStrict(AccessRequest request, PersonDocument pd,
                              FabricDocView fabricDoc, String eventType,
                              String result, String reason, String action,
                              String actorType, String actorId) {
    try {
        fabricAuditCliService.recordEvent(buildAuditCommand(
            request, pd, fabricDoc, eventType, result, reason, action, actorType, actorId
        ));
    } catch (RuntimeException ex) {
        throw new IllegalArgumentException(
            "No fue posible registrar la auditoría en Fabric para el evento " + eventType
        );
    }
}
Used for critical operations like document access where audit trail is mandatory.
Audit failure is logged but doesn’t block the operation.
// AccessRequestService.java:760
private void recordAuditBestEffort(AccessRequest request, PersonDocument pd,
                                  FabricDocView fabricDoc, String eventType,
                                  String result, String reason, String action,
                                  String actorType, String actorId) {
    try {
        fabricAuditCliService.recordEvent(buildAuditCommand(
            request, pd, fabricDoc, eventType, result, reason, action, actorType, actorId
        ));
    } catch (RuntimeException ex) {
        log.warn("No se pudo registrar auditoría Fabric (best-effort). " +
                "requestId={}, eventType={}, detail={}",
                request != null ? request.getId() : null, eventType, ex.getMessage());
    }
}
Used for auxiliary operations where operational continuity is prioritized.

Block Trace Details

Administrators and authorized entities can query blockchain trace details for registered documents.

Block Reader Service

// BlockchainTraceDetailService.java
@Service
public class BlockchainTraceDetailService {
    
    public Map<String, Object> getBlockDetailByRef(String blockRef) {
        List<String> cmd = List.of(nodeBin, blockReaderScript, blockRef);
        
        ExternalToolsService.ExecResult res = 
            externalToolsService.exec(cmd, fabricWorkdir, Map.of());
        
        if (res.getExitCode() != 0) {
            throw new RuntimeException("Failed to read block: " + res.getStderr());
        }
        
        String json = extractJsonObject(res.getStdout());
        return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
    }
}
Script: read-block-by-ref.js Returns:
  • Block number
  • Transaction ID
  • Timestamp
  • Chaincode name
  • Endorser signatures
  • Read/write sets

Trace Query in Access Workflow

// AccessRequestService.java:426
@Transactional(readOnly = true)
public DocumentBlockchainTrace loadApprovedDocumentBlockchainTrace(
        Long entityId, Long requestId, Long personDocumentId) {
    
    // ... authorization checks ...
    
    PersonDocument pd = personDocumentRepository.findByIdWithFiles(personDocumentId)
        .orElseThrow(() -> new IllegalArgumentException("Documento no encontrado"));
    
    FileRecord latest = findLatestFile(pd);
    List<FabricDocView> fabricDocs = loadFabricDocsForPerson(request.getPerson());
    
    FabricDocView fabricDoc = findMatchingFabricDoc(fabricDocs, pd, latest)
        .orElseThrow(() -> new IllegalArgumentException(
            "No se encontró la referencia del documento en Fabric para esta solicitud."
        ));
    
    // Record trace query audit event
    recordAuditStrict(request, pd, fabricDoc, "DOC_BLOCK_TRACE_QUERY", "OK",
                     "Consulta de detalle blockchain de documento autorizado",
                     "READ_BLOCK_TRACE", "ISSUER", String.valueOf(entityId));
    
    return new DocumentBlockchainTrace(
        "Hyperledger Fabric",
        fabricDoc.docId(),
        fabricDoc.title(),
        fabricDoc.issuingEntity(),
        fabricDoc.status(),
        fabricDoc.createdAtHuman(),
        fabricDoc.sizeHuman(),
        "documento",
        fabricDoc.filePath()
    );
}

Audit Event Listing

Query recorded access events from the Fabric ledger:
// FabricAuditCliService.java
public List<Map<String, Object>> listAccessEvents(String idType, String idNumber, Integer limit) {
    List<String> cmd = new ArrayList<>();
    cmd.add(nodeBin);
    cmd.add(listAccessScript);
    cmd.add(idType);
    cmd.add(idNumber);
    if (limit != null) {
        cmd.add(String.valueOf(limit));
    }
    
    ExternalToolsService.ExecResult res = externalToolsService.exec(cmd, fabricWorkdir, Map.of());
    
    if (res.getExitCode() != 0) {
        throw new RuntimeException("Failed to list access events: " + res.getStderr());
    }
    
    String json = extractJsonArray(res.getStdout());
    return objectMapper.readValue(json, new TypeReference<List<Map<String, Object>>>() {});
}
Script: list-access-events.js Usage in reports:
// AdminReportService.java
public AuditTrailReport generateAuditTrailReport(String idType, String idNumber) {
    List<Map<String, Object>> events = fabricAuditCliService.listAccessEvents(idType, idNumber, 100);
    
    // Aggregate by event type, actor, time period, etc.
    return buildAuditReport(events);
}

Synchronization Best Practices

Approval-Triggered Sync: Documents are automatically synced to Fabric when citizens approve access requests, ensuring the ledger reflects the exact documents authorized for access.
Sync Failures Block Approvals: If document sync to Fabric fails during approval, the entire approval transaction is rolled back. This ensures no access is granted without blockchain traceability.
Manual Sync for Pre-Existing Data: Administrators should use the sync tools at /admin/sync to register historical documents that existed before blockchain integration.

Configuration Reference

Fabric Scripts

VariablePurposeDefault
FABRIC_WORKDIRWorking directory for Fabric clientRequired
FABRIC_NODE_BINNode.js executable pathnode
FABRIC_LIST_DOCS_SCRIPTList documents scriptlist-docs.js
FABRIC_BLOCK_READER_SCRIPTBlock detail readerread-block-by-ref.js
FABRIC_RECORD_ACCESS_SCRIPTAudit event recorderrecord-access-event.js
FABRIC_LIST_ACCESS_SCRIPTAudit event listerlist-access-events.js
FABRIC_SYNC_ALL_SCRIPTGlobal sync scriptsync-db-to-ledger.js
FABRIC_SYNC_PERSON_SCRIPTPerson sync scriptsync-person-to-ledger.js

Network Configuration

Connection Profile: Fabric client scripts must have access to:
  • Peer endpoint(s) with TLS certificates
  • Orderer endpoint(s) with TLS certificates
  • Channel name
  • Chaincode name
  • MSP identity with signing key
Environment variables expected by scripts:
  • FABRIC_CHANNEL_NAME
  • FABRIC_CHAINCODE_NAME
  • FABRIC_MSP_ID
  • FABRIC_PEER_ENDPOINT
  • FABRIC_ORDERER_ENDPOINT

Key Services

Location: src/main/java/co/edu/unbosque/ccdigital/service/FabricLedgerCliService.javaQueries documents from Fabric ledger via list-docs.js and parses results into FabricDocView records for application use.Key Methods:
  • listDocsRaw(String idType, String idNumber) - Raw stdout from script
  • listDocsView(String idType, String idNumber) - Parsed document list
  • findDocById(String idType, String idNumber, String docId) - Find specific document
Location: src/main/java/co/edu/unbosque/ccdigital/service/FabricAuditCliService.javaRecords access events to Fabric via record-access-event.js and retrieves audit trail via list-access-events.js.Key Methods:
  • recordEvent(AuditCommand cmd) - Write audit event to ledger
  • listAccessEvents(String idType, String idNumber, Integer limit) - Query audit log
Location: src/main/java/co/edu/unbosque/ccdigital/service/ExternalToolsService.javaGeneric command execution service for invoking external scripts (Fabric Node.js, Indy Python) with timeout and error handling.Key Methods:
  • exec(List<String> command, String workdir, Map<String, String> env) - Execute command
  • runFabricSyncAll() - Sync all documents
  • runFabricSyncPerson(String idType, String idNumber) - Sync person documents
Location: src/main/java/co/edu/unbosque/ccdigital/service/BlockchainTraceDetailService.javaRetrieves detailed block/transaction information from Fabric for administrative inspection and audit reports.Key Methods:
  • getBlockDetailByRef(String blockRef) - Get block metadata
  • Used by admin reporting and trace visualization

Document-to-Ledger Matching

When validating approved documents against Fabric ledger, the system uses flexible matching:
// AccessRequestService.java:643
private Optional<FabricDocView> findMatchingFabricDoc(List<FabricDocView> fabricDocs,
                                                      PersonDocument pd,
                                                      FileRecord latest) {
    String dbRelativePath = normalizePath(latest.getStoragePath());
    String title = pd.getDocumentDefinition() != null 
                   ? pd.getDocumentDefinition().getTitle() 
                   : null;
    
    return fabricDocs.stream()
        .filter(Objects::nonNull)
        .filter(doc -> matchesFilePath(doc.filePath(), dbRelativePath) 
                    || matchesTitle(doc.title(), title))
        .findFirst();
}

private boolean matchesFilePath(String fabricPath, String dbRelativePath) {
    if (fabricPath == null || dbRelativePath == null) return false;
    
    String fabricNorm = normalizePath(fabricPath);
    String dbNorm = normalizePath(dbRelativePath);
    
    // Exact match or suffix match (handles absolute vs relative paths)
    return fabricNorm.equals(dbNorm)
        || fabricNorm.endsWith("/" + dbNorm)
        || fabricNorm.endsWith(dbNorm);
}
Matching strategies:
  1. Path matching - Primary strategy, handles both absolute and relative paths
  2. Title matching - Fallback strategy using case-insensitive comparison
Why flexible matching? Fabric may store absolute paths while the database stores relative paths. The matching logic accommodates both representations.

Admin Dashboard Integration

The admin dashboard at /admin/sync provides:

Sync Status

View last sync timestamp and results for all documents and per-person syncs

Manual Triggers

Execute sync operations on-demand for testing, recovery, or initial data load

Audit Trail

View recent access events and blockchain trace summaries

Error Logs

Review sync failures and script execution errors for troubleshooting
Controller: AdminController.java (endpoints: /admin/sync, /admin/sync/run, /admin/sync/person)

Security Considerations

Script Execution Security: External scripts run in a controlled environment with timeout limits. Ensure script paths are not user-controllable to prevent command injection.Service: ExternalToolsService enforces timeout via EXTERNAL_TOOLS_TIMEOUT_SECONDS (default: 300 seconds).
Immutable Audit Log: Events written to Fabric cannot be modified or deleted, providing tamper-proof evidence for compliance and forensic investigation.
Chaincode Authorization: Ensure the MSP identity used by sync and audit scripts has appropriate chaincode endorsement policies to prevent unauthorized ledger writes.

Build docs developers (and LLMs) love