Contract Lifecycle Management Platform
Overview
An enterprise contract lifecycle management platform enabling organizations to draft, negotiate, approve, sign, and analyze legal documents in a secure, collaborative environment. Built on 9 independently deployable microservices with an NGINX API gateway, event-driven inter-service communication, eIDAS-compliant electronic signatures, and multi-party document workflows with version-controlled collaboration.
| Domain | Legal Tech: Contract Lifecycle Management |
|---|---|
| Role | Lead Software Engineer / System Architect |
| Timeline | Architecture Design & Technical Investigation |
| Stack | Node.js, React, MongoDB, NGINX, Docker, Kubernetes, RabbitMQ, MinIO |
| Scale | 9 microservices, NGINX API gateway, 3 storage layers, eIDAS + DocuSign signing |
Architecture
System Architecture
Service Responsibilities
| Service | Purpose | Key Technology |
|---|---|---|
| NGINX API Gateway | Single entry point; routing, SSL termination, rate limiting, auth delegation | NGINX auth_request, proxy_pass |
| IAM Service | Authentication (local + Google SSO), JWT issuance/validation, role management | Passport.js, jsonwebtoken, bcrypt |
| Documents Service | Document CRUD, S3-backed versioning, clause library, pessimistic locking | Mongoose, MinIO SDK |
| Workflow Engine | Document lifecycle state machine (Draft through Signed/Archived) | Event publishing, state validation |
| Conversion Service | Format conversion (DOCX, HTML, PDF) via headless LibreOffice | LibreOffice CLI, Docker |
| E-Signature Service | Signing orchestration with DocuSign, eIDAS compliance, digital certificates | DocuSign API, WebCrypto |
| Presence & Chat | Real-time user presence and document-scoped chat | Socket.io (WebSockets) |
| Comments Service | Threaded, version-linked comments with resolution tracking | Mongoose |
| Notifications Service | Event-driven email and in-app alert dispatch | Nodemailer, event consumer |
| Analytics Service | KPI aggregation, categorized reports, deadline tracking | Event consumer, aggregation pipelines |
Key Architectural Decisions
1. Asynchronous Collaboration Over Real-Time Editing
Context: The platform requires multi-party document review and editing. The initial design considered real-time collaborative editing using Operational Transformation (OT) or Conflict-free Replicated Data Types (CRDTs), the same algorithms that power Google Docs.
Decision: I chose an asynchronous, version-based editing model with pessimistic locking instead.
Rationale:
- OT/CRDT introduces significant implementation complexity (conflict resolution algorithms, stateful WebSocket servers, editor library constraints)
- The business workflow is inherently sequential: Author edits, then Reviewer reviews, then Approver approves, so parties rarely need to type at the same time
- A locking + explicit save model provides clear version history and simpler conflict prevention
- The “Revisions” feature (diff between versions) is fully achievable without real-time sync
Trade-off: Users cannot see each other’s keystrokes in real time. But the overall system is dramatically simpler to build, debug, scale, and maintain. That is a good trade for a platform where the review cycle spans days, not seconds.
2. NGINX auth_request Pattern for Distributed Authentication
Context: In a microservices architecture, every service needs to verify that incoming requests are authenticated and extract user identity. The naive approach, where each service validates JWTs on its own, spreads security logic across all services.
Decision: Centralize authentication at the API gateway using NGINX’s auth_request directive, which delegates token validation to the IAM Service via an internal sub-request before any traffic reaches downstream services.
How it works:
Result: Authentication happens once at the edge. Downstream services receive pre-validated X-User-ID and X-User-Roles headers, trusting them because they came through the gateway. Each service then applies its own fine-grained authorization (e.g., “can this editor modify this specific document?”) using a local permission configuration, so there is no central authorization bottleneck.
3. Macro-State vs. Micro-State Separation
Context: Both the Workflow Engine and the Documents Service manage “state” for a document. This initially appeared to create a conflict in ownership.
Decision: Separate concerns into two distinct state layers:
| Workflow Engine (Macro) | Documents Service (Micro) | |
|---|---|---|
| Manages | Business lifecycle | Operational edit lock |
| States | Draft, In Review, Signed, Archived | Locked, Unlocked |
| Duration | Days to weeks | Minutes to hours |
| Triggers | Business milestones (submit, approve, sign) | Edit/Save/Cancel actions |
| Question | “Where is this in the business process?” | “Is someone editing right now?” |
How they interact: The macro-state constrains the micro-state. A document in “Signed” state cannot be locked for editing. A document “In Review” can only be locked by designated reviewers. The Documents Service checks the workflow state before granting any lock.
Deep Dive: E-Signature Orchestration
This sequence diagram illustrates the most complex cross-service workflow in the platform. It shows how a document moves from “ready for signature” through third-party signing to final sealed storage:
Complexity managed:
- 6 services coordinated through events (no direct service-to-service coupling)
- Format conversion happens on-demand (HTML editor format to signing-ready PDF)
- DocuSign integration is fully isolated in one service, so it is swappable for any other provider
- Webhook-based completion avoids polling and keeps the flow async
Document Lifecycle
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | React, Redux Toolkit, Socket.io Client, Rich Text Editor | SPA with real-time chat and document editing |
| API Gateway | NGINX | Routing, SSL, rate limiting, auth delegation |
| Backend | Node.js 20, Express.js | All microservice runtimes |
| Database | MongoDB (per-service), Mongoose ODM | Document storage with schema validation |
| Object Storage | MinIO (S3-compatible) | Document files, versions, signed PDFs |
| Authentication | Passport.js (local + Google), jsonwebtoken, bcrypt | Multi-strategy auth, stateless JWT sessions |
| Messaging | RabbitMQ | Async event-driven inter-service communication |
| Real-time | Socket.io (WebSockets) | Presence tracking and document chat |
| Conversion | LibreOffice (headless) | DOCX/HTML/PDF format conversion |
| E-Signatures | DocuSign API | Third-party signing orchestration |
| Nodemailer | Notification dispatch | |
| Containers | Docker | Service packaging and isolation |
| Orchestration | Kubernetes | Auto-scaling, self-healing, rolling deployments |
| CI/CD | GitHub Actions | Automated test, build, and deploy pipelines |
What I Would Do Differently Today
Architectural decisions are context-dependent. With the benefit of hindsight and the evolution of the ecosystem since the original design, here are the changes I would consider:
| Area | Original Choice | What I’d Evaluate Today | Why |
|---|---|---|---|
| API Gateway | Raw NGINX config | Kong, AWS API Gateway, or Envoy | Built-in observability, plugin ecosystem, reduced configuration burden |
| IAM Service | Custom Node.js + Passport.js | Keycloak or Auth0 | Battle-tested identity platform with built-in SSO, MFA, user management UI; eliminates a full custom service |
| Inter-service comms | REST + RabbitMQ | gRPC for sync calls + Kafka for events | gRPC gives type-safe contracts with Protobuf and better performance; Kafka provides event replay and stream processing |
| Document versioning | New S3 object per version | Event Sourcing pattern | Store a stream of change events rather than full snapshots; enables richer audit trails and undo/redo |
| Authorization | Per-service config files | Open Policy Agent (OPA) | Centralized, declarative policy engine with Rego language; avoids duplicating permission maps across services |
| Observability | Not addressed in original | OpenTelemetry + Grafana stack | Distributed tracing across 9 services is essential for debugging production issues |
| Real-time editing | Removed entirely | Evaluate Yjs or Liveblocks | The CRDT ecosystem has matured significantly; if the business case demands it, it’s now more feasible |
These aren’t criticisms of the original architecture. They reflect how the ecosystem has evolved and how requirements might change at scale. The original decisions were sound for the constraints and timeline of the project.
Event-Driven Communication Map
All asynchronous inter-service communication flows through the message broker:
Architecture designed and documented as part of a technical investigation into a contract lifecycle management platform. All diagrams created with Mermaid.js.