Skip to main content

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

ConcernTechnology
FrameworkReact 18
Build toolVite 5
LanguageTypeScript
StylingTailwind CSS
UI componentsshadcn/ui (Radix primitives)
Rich text editorQuill (contract authoring)
State managementReact Context + local component state
Data fetchingTanStack Query (React Query)
RoutingReact Router v6
AuthKeycloak JS adapter

Backend API

ConcernTechnology
RuntimeNode.js 20+
FrameworkHono (lightweight, fast, edge-compatible)
LanguageTypeScript
ORMPrisma
DatabasePostgreSQL 15+
Auth validationKeycloak token introspection
File storageLocal disk (Docker volume) or S3-compatible
EmailNodemailer (SMTP)

Database Schema (Key Tables)

TablePurpose
organisationsTenant isolation root
usersUser accounts
roles12 built-in + custom roles
user_rolesMany-to-many user ↔ role
contractsCore contract record
contract_versionsFull document body snapshots
contract_documentsFile attachments
templatesContract templates
clausesClause library entries
workflowsWorkflow template definitions
workflow_stepsIndividual steps in a workflow
approval_requestsPer-contract workflow instances
approval_responsesApprover decisions
obligationsPost-signature obligations
signaturesE-signature requests and status
lettersGenerated contract letters
audit_eventsImmutable 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:

  1. Extracts the user ID from the validated JWT.
  2. Queries the user's roles from the database.
  3. Checks whether any of those roles grant the requested action on the resource.
  4. Returns 403 Forbidden if 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.