Skip to main content

System Architecture

CCDigital is designed as a Spring Boot monolith that integrates multiple storage backends and blockchain networks to provide secure, auditable document management with verifiable digital identity.

High-Level Architecture

Architecture Principles

Monolithic Design

Single deployable Spring Boot JAR simplifies operations and maintains transactional consistency across modules.

Layered Architecture

Clear separation: Presentation (Controllers) → Business Logic (Services) → Data Access (Repositories)

Blockchain as Trust Layer

MySQL remains the primary source of truth. Blockchain provides auditable evidence and verification.

External Integration

Node.js and Python scripts encapsulate blockchain complexity, called via ExternalToolsService.

Component Breakdown

Spring Boot Application

The core application is built on Spring Boot 3.5.11 with Java 17, providing:

Configuration Layer

Package: co.edu.unbosque.ccdigital.config
  • SecurityConfig: Multi-chain security configuration with separate authentication flows for Admin, Issuer, and User
  • FileStorageProperties: Maps ccdigital.fs.base-path environment variable
  • IndyProperties: Maps all ccdigital.indy.* configuration for ACA-Py integration
  • SensitiveEndpointRateLimitFilter: Rate limiting filter with configurable windows and thresholds

Controller Layer

Package: co.edu.unbosque.ccdigital.controller Key controllers and their responsibilities:
ControllerEndpointsPurpose
AdminController/admin/dashboard
/admin/persons
/admin/sync
/admin/reports
Government operations: person management, document review, blockchain sync, analytics
IssuerController/issuer
/issuer/upload
/issuer/search
Institution operations: search citizens, upload PDFs, create access requests
UserAuthController/user/auth/start
/user/auth/poll
/user/auth/otp/verify
End-user authentication with Indy proof + MFA
UserPagesController/user/dashboard
/user/requests
Citizen dashboard and access request management
UserDocsController/user/docs/view/{id}
/user/docs/download/{id}
Document viewing and downloading with signed URLs
UserRegistrationController/register/user
/register/user/verify-email
Self-service registration with email OTP verification
Controllers are thin layers that delegate business logic to services. They handle HTTP concerns (request binding, redirects, model population) while services handle transactions and validation.

Service Layer

Package: co.edu.unbosque.ccdigital.service Critical services: Core Business Services:
  • PersonService: Person lifecycle management, including createPersonAndFolder(Person) for atomic person + filesystem setup
  • PersonDocumentService: Document upload, review workflow (PENDING → APPROVED/REJECTED), PDF validation
  • AccessRequestService: Access request creation, approval/rejection, expiry validation
  • UserAccountService: User account management, password hashing, normalization
  • FileStorageService: Physical file storage with SHA-256 hashing, size tracking, folder creation
Security Services:
  • UserAuthFlowService: Orchestrates the complete user login flow (Indy proof + MFA)
  • IndyProofLoginService: Present-proof 2.0 protocol implementation, attribute extraction
  • UserLoginOtpService: Email OTP generation and verification for 2FA
  • UserTotpService: TOTP (Time-based OTP) management for authenticator apps
  • SignedUrlService: Generates HMAC-signed time-limited URLs for document access
  • ForgotPasswordService: Password recovery with OTP codes
Blockchain Integration Services:
  • ExternalToolsService: Executes Node.js and Python scripts with timeout control
  • FabricLedgerCliService: Wraps Fabric Node.js scripts for document listing and sync
  • FabricAuditCliService: Records and queries access events on Fabric
  • IndyAdminClient: REST client for ACA-Py admin API
  • UserAccessGovernanceService: Synchronizes user access state to Indy connection metadata
Reporting Services:
  • AdminReportService: Aggregates analytics data with configurable date ranges and periods
  • AdminReportPdfService: Generates PDF reports with embedded charts
  • BlockchainTraceDetailService: Resolves blockchain references to full block/transaction details

Entity Layer (Data Model)

Package: co.edu.unbosque.ccdigital.entity Key JPA entities:
@Entity
@Table(name = "persons")
public class Person {
    @Id @GeneratedValue
    private Long id;
    
    @Enumerated(EnumType.STRING)
    private IdType idType;  // CC, CE, TI, etc.
    
    private String idNumber;
    private String firstName;
    private String lastName;
    private String email;
    private String phone;
    private LocalDate birthdate;
    // ... relationships
}
@Entity
@Table(name = "person_documents")
public class PersonDocument {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    private Person person;
    
    @ManyToOne
    private Document document;  // Document definition
    
    @Enumerated(EnumType.STRING)
    private ReviewStatus reviewStatus;  // PENDING, APPROVED, REJECTED
    
    private LocalDate issueDate;
    private LocalDate expiryDate;
    // ... file relationship
}
Complete Entity List:
  • Person, User, EntityUser (issuer accounts)
  • IssuingEntity, Company, CompanyDocumentDefinition
  • Document (document types), Category (document categories)
  • PersonDocument (documents owned by persons)
  • File (file metadata: path, sha256, size, version)
  • AccessRequest, AccessRequestItem (request workflow)
  • Consent (approval/rejection records)
  • AuditEvent (system audit log)

MySQL Database

The database schema includes: Tables: 22+ tables for core data Views: Simplified queries for common operations
  • v_documents: Complete document information
  • v_person_full_documents: Person documents with all metadata
Stored Procedures: Complex operations
  • sp_add_person_document: Atomically add document with validation
  • sp_create_user_with_person: Create user account linked to person
  • sp_upload_file_path: Record file metadata
  • sp_upload_pdf_blob: Store PDF as BLOB (optional)
Triggers: Automatic behaviors
  • trg_files_autoversion: Automatically increments version on file updates
The database remains the primary source of truth. All business logic operates against MySQL, and blockchain networks receive synchronized snapshots for audit and verification purposes.

File System Storage

Managed by FileStorageService: Directory Structure:
/var/ccdigital/documents/
├── Juan_Perez_CC_12345678/
│   ├── cedula.pdf
│   ├── diploma.pdf
│   └── certificado_laboral.pdf
├── Maria_Garcia_CE_87654321/
│   └── pasaporte.pdf
└── ...
Features:
  • Normalized folder names: FirstName_LastName_IdType_IdNumber
  • SHA-256 hash calculation for integrity verification
  • File size tracking
  • Automatic directory creation
  • Path validation for security (prevents directory traversal)
Security Validation (from UserDocsController.java):
// Ensures files served to users are within allowed directories
if (!Files.exists(fullPath) || !fullPath.startsWith(allowedBase)) {
    return ResponseEntity.status(HttpStatus.FORBIDDEN)
        .body("Ruta no permitida");
}

Module Architecture

Admin Module

Primary Controller: AdminController.java Key Flows:
1

Person Registration

  1. Admin fills PersonCreateForm with citizen data
  2. PersonService.createPersonAndFolder(Person) atomically:
    • Saves person to MySQL
    • Creates filesystem folder
    • Returns saved entity
  3. Admin is redirected to person detail page
2

Document Upload & Review

  1. Admin uploads file via /admin/persons/{id}/upload
  2. PersonDocumentService.uploadForPerson() validates and stores file
  3. Document enters PENDING review status
  4. Admin reviews via /admin/person-documents/{id}/review
  5. Status changes to APPROVED or REJECTED
3

Access Governance

  1. Admin modifies user access state via /admin/persons/{id}/access-state
  2. UserAccessGovernanceService.updateState() updates MySQL
  3. If INDY_USER_ACCESS_SYNC_ENABLED=true, syncs to Indy connection metadata
  4. User’s access is immediately affected system-wide
4

Blockchain Synchronization

  1. Admin triggers sync from /admin/sync page
  2. ExternalToolsService executes configured Node.js scripts
  3. Scripts read MySQL and write to Fabric ledger
  4. Results displayed with stdout/stderr/exit code
Endpoints Reference (from AdminController.java):
@GetMapping({"", "/", "/dashboard"})
public String dashboard()

@GetMapping("/reports")
public String reports(@RequestParam LocalDate from, 
                      @RequestParam LocalDate to,
                      @RequestParam String period, 
                      Model model)

@GetMapping("/persons")
public String persons(Model model)

@GetMapping("/persons/{id}")
public String personDetail(@PathVariable Long id, Model model)

@PostMapping("/persons/{id}/upload")
public String uploadDoc(@PathVariable Long id, 
                        @ModelAttribute DocumentUploadForm uploadForm,
                        @RequestParam MultipartFile file)

@PostMapping("/persons/{id}/access-state")
public String updatePersonAccessState(@PathVariable Long id,
                                      @RequestParam String state,
                                      @RequestParam String reason)

@PostMapping("/sync/fabric/all")
public String fabricAll(Model model)

@PostMapping("/sync/fabric/person")
public String fabricPerson(@ModelAttribute SyncPersonForm form, Model model)

@PostMapping("/sync/indy/issue")
public String indyIssue(Model model)

Issuer Module

Primary Controller: IssuerController.java Authentication: Uses IssuerPrincipal security context Key Flows:
1

Search Citizen

  1. Issuer submits IssuerSearchForm with ID type and number
  2. PersonService.findByIdTypeAndNumber() queries MySQL
  3. If found, redirects to /issuer?personId={id}
  4. Person’s existing documents are displayed
2

Upload PDF Document

  1. Issuer selects allowed document type (based on entity_document_definitions)
  2. Uploads PDF file via /issuer/upload
  3. PDF Validation (from IssuerController.java):
    private boolean isPdfUpload(MultipartFile file) {
        String name = originalFilename.toLowerCase();
        String mime = contentType.toLowerCase();
        return name.endsWith(".pdf") || mime.contains("pdf");
    }
    
  4. PersonDocumentService.uploadFromIssuer() performs:
    • Authorization check (issuer allowed to issue this document type?)
    • Deep PDF validation (extension, MIME, file signature %PDF)
    • File storage with SHA-256 hash
    • Creates PersonDocument with PENDING review status
3

Create Access Request

  1. Issuer navigates to /issuer/access-requests/new
  2. Selects person and approved documents
  3. AccessRequestService.create() creates request
  4. User receives notification of pending request
4

View Authorized Documents

  1. Issuer views approved access requests
  2. Can view/download documents via signed URLs
  3. All access events logged to Fabric via FabricAuditCliService
Security Constraints:
  • Issuers can only upload document types assigned to their entity
  • Only PDF files accepted
  • Documents require admin approval before appearing in access requests
  • Access requests must be approved by end user
  • Signed URLs expire after configured TTL

User Module

Primary Controllers: UserAuthController.java, UserPagesController.java, UserDocsController.java, UserRegistrationController.java Authentication Flow (from UserAuthFlowService.java and IndyProofLoginService.java):
1

Initiate Login

Endpoint: POST /user/auth/start
{
  "email": "[email protected]",
  "password": "securePassword123"
}
Process:
  1. Validates email and password against MySQL
  2. Checks user access state (must be ENABLED)
  3. Initiates Indy present-proof 2.0 via IndyProofLoginService.requestProof()
  4. Returns presExId (presentation exchange ID) and QR code data
2

Poll Proof Status

Endpoint: GET /user/auth/poll?presExId={id}Process:
  1. Queries ACA-Py for presentation exchange state
  2. Returns one of: pending, verified, failed
  3. When verified, extracts attributes:
    // From IndyProofLoginService
    String idType = getAttribute("id_type");
    String idNumber = getAttribute("id_number");
    String firstName = getAttribute("first_name");
    String lastName = getAttribute("last_name");
    String email = getAttribute("email");
    
  4. Validates attributes match user record in MySQL
3

Second Factor Authentication

Endpoint: POST /user/auth/otp/verify
{
  "presExId": "abc123...",
  "code": "123456"
}
Two Options:A) TOTP (Time-based OTP) via UserTotpService:
  • User has registered authenticator app (Google Authenticator, Authy)
  • Verifies 6-digit code with configurable time window
  • Algorithm: HMAC-SHA1 with 30-second period
B) Email OTP via UserLoginOtpService:
  • System generates random code
  • Sends via SMTP to user’s registered email
  • Code expires after configured TTL (default: 5 minutes)
  • Max attempts limit prevents brute force
On Success:
  • Spring Security session established
  • User redirected to /user/dashboard
Registration Flow (from UserRegistrationFlowService.java):
1

Submit Registration Form

Endpoint: POST /register/userUser provides:
  • ID type and number (must match existing Person record)
  • Email (verified via OTP)
  • Password (strong validation)
  • Optional: TOTP setup
Validation:
  • Password must meet complexity requirements
  • Email must be unique
  • Person must exist in system (pre-registered by admin)
  • Person must not already have a user account
2

Email Verification

Endpoint: POST /register/user/verify-emailProcess (from UserRegisterEmailOtpService.java):
  1. Generates random code (default: 6 digits)
  2. Stores in temporary cache with TTL
  3. Sends via SMTP
  4. User submits code
  5. If valid, account activated
3

Automatic State Sync

After successful registration:
  1. User access state set to ENABLED
  2. If INDY_USER_ACCESS_SYNC_ENABLED=true:
    • UserAccessGovernanceService finds Indy connection
    • Updates connection metadata with access state
    • Credential holder can be verified in future proofs
Document Access (from UserDocsController.java):
@GetMapping("/docs/view/{docId}")
public ResponseEntity<byte[]> viewDocument(@PathVariable Long docId,
                                           @RequestParam String signature,
                                           @RequestParam Long expires)
Signed URL Flow:
  1. User requests document from dashboard
  2. SignedUrlService.generateSignedUrl() creates URL with:
    • Document ID
    • Expiration timestamp
    • HMAC-SHA256 signature
  3. URL valid for configured TTL (default: 5 minutes)
  4. Controller validates signature and expiry
  5. Performs additional checks:
    • User owns the document
    • File exists on filesystem
    • Path is within allowed directory
  6. Records access event to Fabric
  7. Returns file with appropriate Content-Type

Security Architecture

Spring Security Configuration

From SecurityConfig.java, the application uses multiple security filter chains:
// Admin chain
@Bean
@Order(1)
SecurityFilterChain adminChain(HttpSecurity http) {
    http.securityMatcher("/admin/**", "/login/admin")
        .authorizeHttpRequests(auth -> 
            auth.requestMatchers("/admin/**").hasRole("GOBIERNO")
        )
        .formLogin(form -> 
            form.loginPage("/login/admin")
                .defaultSuccessUrl("/admin/dashboard")
        );
}

// Issuer chain
@Bean
@Order(2)
SecurityFilterChain issuerChain(HttpSecurity http) {
    http.securityMatcher("/issuer/**", "/login/issuer")
        .authorizeHttpRequests(auth -> 
            auth.requestMatchers("/issuer/**").hasRole("ISSUER")
        )
        .formLogin(form -> 
            form.loginPage("/login/issuer")
                .defaultSuccessUrl("/issuer")
        );
}

// User chain (custom authentication)
@Bean
@Order(3)
SecurityFilterChain userChain(HttpSecurity http) {
    http.securityMatcher("/user/**", "/login/user")
        .authorizeHttpRequests(auth -> 
            auth.requestMatchers("/user/**").hasRole("USER")
                .requestMatchers("/login/user").permitAll()
        );
}

Security Controls

Three Roles:
  • ROLE_GOBIERNO: Full administrative access
  • ROLE_ISSUER: Document issuance and access requests
  • ROLE_USER: Personal document access
Enforcement:
  • Controller level: @PreAuthorize("hasRole('GOBIERNO')")
  • Method level: SecurityContextHolder.getContext().getAuthentication()
  • Custom principals: IssuerPrincipal, UserPrincipal
Configuration:
server.servlet.session.timeout=${SERVER_SESSION_TIMEOUT:5m}
Features:
  • Automatic expiry after inactivity
  • Client-side expiry tracking via JavaScript
  • Keepalive endpoint: /api/session/keepalive
  • Manual expiry: /api/session/expire
  • Same-origin frame policy
Filter: SensitiveEndpointRateLimitFilterConfiguration:
export APP_SECURITY_RATE_LIMIT_ENABLED='true'
export APP_SECURITY_RATE_LIMIT_WINDOW_SECONDS='60'
export APP_SECURITY_RATE_LIMIT_MAX_REQUESTS_PER_WINDOW='10'
Implementation:
  • In-memory request counter per IP
  • Sliding window algorithm
  • Returns 429 Too Many Requests when exceeded
Headers (configured in SecurityConfig):
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Service: SignedUrlServiceAlgorithm:
String data = docId + ":" + expires;
String signature = HMAC_SHA256(secret, data);
String url = "/user/docs/view/" + docId + 
             "?expires=" + expires + 
             "&signature=" + signature;
Validation:
  1. Check expiration timestamp
  2. Recompute HMAC with secret
  3. Compare signatures (constant-time comparison)
  4. Reject if mismatch or expired

Data Flow and Integration

Document Upload Flow

User Authentication Flow

Blockchain Synchronization Flow

Technology Stack Details

Backend Framework

Spring Boot 3.5.11 (from pom.xml):
<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.5.11</spring-boot.version>
</properties>

<dependencies>
    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
    <!-- JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!-- Mail -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
</dependencies>
Key Libraries:
  • MySQL Connector/J: JDBC driver for MySQL 8
  • Thymeleaf Spring Security 6: Security dialect for templates
  • OpenHTMLToPDF 1.0.10: HTML to PDF rendering with PDFBox
  • XChart 3.8.8: Server-side chart generation

Blockchain Technologies

Hyperledger Fabric

Purpose: Document registry and audit trail Integration Method: Node.js CLI scripts Scripts (configured via environment variables):
ScriptPurposeInvoked By
list-docs.jsList documents for a person from ledgerFabricLedgerCliService.listPersonDocs()
read-block-by-ref.jsResolve functional reference to actual blockBlockchainTraceDetailService.readDetail()
record-access-event.jsLog document access/verification eventFabricAuditCliService.recordAccess()
list-access-events.jsQuery audit events for person or globalFabricAuditCliService.listEvents()
sync-db-to-ledger.jsFull database → ledger syncExternalToolsService.runFabricSyncAll()
sync-person.jsSingle person syncExternalToolsService.runFabricSyncPerson()
Data Flow:
  1. Admin clicks “Sync to Fabric” in UI
  2. ExternalToolsService constructs command:
    /usr/bin/node sync-db-to-ledger.js
    
  3. Script connects to Fabric peer via SDK
  4. Queries MySQL for current state
  5. Invokes chaincode functions to register documents
  6. Returns transaction IDs and status
Chaincode Expected Functions:
  • registerDocument(docId, personId, metadata): Register new document
  • recordAccessEvent(docId, actorId, eventType, timestamp): Audit log
  • queryDocumentsByPerson(personId): List person’s documents
  • queryAccessEvents(filters): Query audit trail

Hyperledger Indy / ACA-Py

Purpose: Verifiable credential-based authentication Integration Method: REST API via IndyAdminClient and IndyProofLoginService Architecture:
┌──────────────────┐         ┌──────────────────┐
│   CCDigital      │◄───────►│  ACA-Py Issuer   │
│   Application    │  REST   │  (Admin API)     │
│                  │         └──────────────────┘
│                  │                  │
│                  │         ┌────────▼──────────┐
│                  │         │   Indy Ledger     │
│                  │         │   (von-network)   │
│                  │         └────────┬──────────┘
│                  │                  │
│  Login Flow      │         ┌────────▼──────────┐
│  /user/auth/*    │◄───────►│  ACA-Py Holder    │
└──────────────────┘  REST   │  (User's Wallet)  │
                              └───────────────────┘
Credential Schema Attributes (from README.md):
  • id_type: Citizen ID type (CC, CE, TI, etc.)
  • id_number: Citizen ID number
  • first_name: Given name
  • last_name: Family name
  • email: Email address
Present-Proof 2.0 Flow (implemented in IndyProofLoginService.java):
  1. Request Proof:
    POST /present-proof-2.0/send-request
    {
      "connection_id": "<holder-connection-id>",
      "proof_request": {
        "name": "CCDigital Login",
        "version": "1.0",
        "requested_attributes": {
          "id_type": {"name": "id_type", "restrictions": [{"cred_def_id": "..."}]},
          "id_number": {"name": "id_number", ...},
          "first_name": {"name": "first_name", ...},
          "last_name": {"name": "last_name", ...},
          "email": {"name": "email", ...}
        }
      }
    }
    
  2. Poll for Verification:
    GET /present-proof-2.0/records/{pres_ex_id}
    
    States: request-sentpresentation-receiveddone (verified)
  3. Extract Attributes:
    Map<String, Object> presentation = response.get("presentation");
    Map<String, Object> revealedAttrs = 
        presentation.get("requested_proof").get("revealed_attrs");
    
    String idType = revealedAttrs.get("id_type").get("raw");
    String idNumber = revealedAttrs.get("id_number").get("raw");
    // ... etc.
    
  4. Validate Against Database:
    User user = userRepository.findByEmail(email);
    Person person = user.getPerson();
    
    if (!person.getIdType().name().equals(idType) ||
        !person.getIdNumber().equals(idNumber)) {
        throw new AuthenticationException("Credential mismatch");
    }
    
User Access State Synchronization (from UserAccessGovernanceService.java): When admin changes user access state:
public void syncStateToIndy(Long personId, UserAccessState state) {
    // Find Indy connection for this person
    String connectionId = findConnectionByPerson(personId);
    
    // Update connection metadata
    PUT /connections/{connectionId}/metadata
    {
      "access_state": "ENABLED",
      "updated_at": "2026-03-07T10:30:00Z"
    }
}
This enables future proof requests to check if the user is still authorized.

Deployment Architecture

Development Environment

┌─────────────────────────────────────────────┐
│          Developer Machine                  │
│                                             │
│  ┌──────────────────────────────────────┐  │
│  │   Spring Boot (./mvnw spring-boot:run)│  │
│  │   Port 8080                            │  │
│  └──────────────┬───────────────────────┘  │
│                 │                           │
└─────────────────┼───────────────────────────┘

     ┌────────────┼────────────┐
     │            │            │
     ▼            ▼            ▼
┌─────────┐  ┌──────────┐  ┌──────────────┐
│  MySQL  │  │  Local   │  │  Docker      │
│localhost│  │  Files   │  │  (Fabric +   │
│  :3306  │  │  /var/   │  │   Indy)      │
└─────────┘  └──────────┘  └──────────────┘
                    Internet


              ┌────────────────┐
              │  Reverse Proxy │
              │  (Caddy/nginx) │
              │  Port 443 TLS  │
              └────────┬───────┘


         ┌─────────────────────────┐
         │   Spring Boot App       │
         │   Port 8080 (internal)  │
         └──────┬──────────────────┘

    ┌───────────┼───────────┐
    │           │           │
    ▼           ▼           ▼
┌────────┐  ┌────────┐  ┌──────────┐
│MySQL   │  │ NFS/   │  │Blockchain│
│Cluster │  │ S3     │  │Nodes     │
└────────┘  └────────┘  └──────────┘
Production Considerations:
  • Use a managed MySQL cluster with replication and backups
  • Store files on network storage (NFS, S3) for horizontal scaling
  • Run blockchain nodes in dedicated infrastructure
  • Use container orchestration (Kubernetes) for multi-instance deployments
  • Implement proper secrets management (HashiCorp Vault, AWS Secrets Manager)
  • Set up comprehensive monitoring (Prometheus, Grafana)
  • Configure centralized logging (ELK stack, Splunk)

Performance Considerations

Database Optimization

Indexes (defined in entity classes and schema):
  • Composite index on persons(id_type, id_number) for fast lookups
  • Index on person_documents(person_id, review_status)
  • Index on access_requests(status, expiry_date)
  • Full-text index on document metadata fields
Query Optimization:
  • Views (v_documents, v_person_full_documents) pre-join frequently accessed data
  • Stored procedures for complex multi-table operations
  • JPA @BatchSize for lazy loading optimization

Caching Strategy

Current implementation does not include extensive caching. For production, consider:
  • Spring Cache abstraction with Redis
  • HTTP caching headers for static resources
  • ACA-Py response caching (proof states are ephemeral)
  • Signed URL caching (short TTL)

Blockchain Performance

Fabric:
  • Batch synchronization during off-peak hours
  • Async audit event recording (fire-and-forget)
  • Connection pooling in Node.js scripts
Indy:
  • Connection reuse across proof requests
  • Proof request caching (same structure per request)
  • Parallel polling with configurable intervals

Monitoring and Observability

Application Logging

Spring Boot default logging configuration:
logging.level.root=INFO
logging.level.co.edu.unbosque.ccdigital=DEBUG
logging.level.org.springframework.security=DEBUG
logging.file.name=logs/ccdigital.log
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
Key Log Points (from service layer):
  • Authentication attempts and failures
  • Document upload and review events
  • Blockchain sync results (stdout/stderr from scripts)
  • Access request approvals/rejections
  • User access state changes

Audit Events

Database Table: audit_events Recorded Events:
  • User login (success/failure)
  • Document access (view/download)
  • Access request creation/approval/rejection
  • User access state changes by admin
  • Blockchain synchronization triggers
Blockchain Audit (Fabric):
  • All events recorded on-chain for immutability
  • Queryable via list-access-events.js
  • Block-level verification via read-block-by-ref.js

Health Checks

Recommended endpoints for monitoring:
// Add Spring Boot Actuator dependency
GET /actuator/health
GET /actuator/metrics
GET /actuator/info
Custom Health Indicators:
  • MySQL connectivity
  • File system writability
  • ACA-Py admin API reachability
  • Fabric peer connectivity

Scalability Considerations

Current Architecture Limitations

As a monolith with local file storage:
  • Vertical scaling only: Single JVM, single file system
  • Session affinity required: In-memory sessions don’t distribute
  • File system bottleneck: Local I/O limits throughput

Future Scaling Options

Requirements:
  • Externalize sessions (Spring Session + Redis)
  • Move to network file storage (NFS, S3)
  • Use load balancer with session affinity
  • Consider database connection pooling limits
Potential Services:
  • Auth Service: User authentication, MFA, Indy integration
  • Document Service: Upload, storage, retrieval
  • Access Service: Access requests, approvals, consent
  • Blockchain Service: Fabric and Indy integrations
  • Admin Service: Person management, reporting
Benefits: Independent scaling, technology diversity, fault isolationChallenges: Distributed transactions, network latency, operational complexity
Candidates for Asynchronization:
  • Blockchain synchronization (use message queue)
  • PDF generation for reports (background jobs)
  • Email OTP sending (don’t block request)
  • Audit event recording to Fabric
Technologies: RabbitMQ, Apache Kafka, Spring @Async

Next Steps

Quickstart Guide

Set up CCDigital locally and run your first instance

Admin Module Guide

Learn how to manage citizens and documents

Issuer Module Guide

Understand document issuance and access requests

User Module Guide

Explore citizen registration and document access

Build docs developers (and LLMs) love