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 powering 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 — parties rarely need to type simultaneously
- 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 — a net positive 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 — each service validating JWTs independently — 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 — no centralized 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 — 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 — 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.