Architecture
Repository Structure
openclm/
├── src/ # React frontend (Vite + TypeScript)
│ ├── components/ # UI components (per feature)
│ ├── contexts/ # React contexts (auth, org, theme)
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utilities and API client
│ └── App.tsx # Root component and routing
│
├── server/ # Node.js API (TypeScript + Hono)
│ ├── src/
│ │ ├── routes/ # API route handlers
│ │ ├── services/ # Business logic layer
│ │ ├── middleware/ # Auth, RBAC, rate limiting
│ │ └── lib/ # Shared utilities
│ └── prisma/
│ ├── schema.prisma # Database schema
│ ├── migrations/ # Migration files
│ └── seed.ts # Default roles and data
│
├── help/ # This documentation (Docusaurus)
├── docs/ # Marketing website (static HTML)
└── deploy/ # Deployment configs (Nginx, Caddy, Helm)
Frontend
| Concern | Technology |
|---|---|
| Framework | React 18 |
| Build tool | Vite 5 |
| Language | TypeScript |
| Styling | Tailwind CSS |
| UI components | shadcn/ui (Radix primitives) |
| Rich text editor | Quill (contract authoring) |
| State management | React Context + local component state |
| Data fetching | TanStack Query (React Query) |
| Routing | React Router v6 |
| Auth | Keycloak JS adapter |
Backend API
| Concern | Technology |
|---|---|
| Runtime | Node.js 20+ |
| Framework | Hono (lightweight, fast, edge-compatible) |
| Language | TypeScript |
| ORM | Prisma |
| Database | PostgreSQL 15+ |
| Auth validation | Keycloak token introspection |
| File storage | Local disk (Docker volume) or S3-compatible |
| Nodemailer (SMTP) |
Database Schema (Key Tables)
| Table | Purpose |
|---|---|
organisations | Tenant isolation root |
users | User accounts |
roles | 12 built-in + custom roles |
user_roles | Many-to-many user ↔ role |
contracts | Core contract record |
contract_versions | Full document body snapshots |
contract_documents | File attachments |
templates | Contract templates |
clauses | Clause library entries |
workflows | Workflow template definitions |
workflow_steps | Individual steps in a workflow |
approval_requests | Per-contract workflow instances |
approval_responses | Approver decisions |
obligations | Post-signature obligations |
signatures | E-signature requests and status |
letters | Generated contract letters |
audit_events | Immutable event log |
RBAC Implementation
Permissions are checked at the route level using middleware:
// Example: only users with contracts:create can POST /contracts
router.post('/contracts',
requirePermission('contracts', 'create'),
createContractHandler
);
The requirePermission(resource, action) middleware:
- Extracts the user ID from the validated JWT.
- Queries the user's roles from the database.
- Checks whether any of those roles grant the requested
actionon theresource. - Returns
403 Forbiddenif the check fails.
Key Design Decisions
Multi-tenancy via Row-Level Scoping
All queries include an organisation_id filter. There is no cross-tenant data access possible at the query layer — even if a user somehow obtained a JWT for another organisation, all queries would return empty results for the wrong org.
Immutable Audit Log
audit_events rows are inserted but never updated or deleted. The table uses a PostgreSQL trigger to prevent UPDATE and DELETE statements, ensuring tamper evidence.
Version History via Snapshots
Rather than storing diffs, each contract save stores a complete snapshot of the document body. This makes version restore simple (copy the snapshot) and ensures version history is readable even if the diff algorithm changes.
Stateless API
The API server is fully stateless — all state lives in PostgreSQL. This makes horizontal scaling straightforward: run multiple API container replicas behind a load balancer with no session affinity required.