A multi-tenant workspace management API built with Express.js, MongoDB, and TypeScript. It provides comprehensive functionality for managing entities (customers, employees, vendors), documents with expiry tracking, work items, and full audit logging — with real-time updates via Socket.io.
- Documentation
- Features
- Tech Stack
- Architecture
- Getting Started
- Environment Variables
- API Reference
- Authentication & RBAC
- Real-Time Events
- Business Rules
- Project Structure
| Document | Description |
|---|---|
| High Level Design (HLD) | System context, container architecture, deployment topology, design decisions |
| LLD — State Machines | Work item lifecycle, expiry status, role transitions |
| LLD — Sequence Diagrams | Login, upload document, update work item status flows |
| Use Case Diagram | Who can do what — actors, roles, and system capabilities |
| Architecture Decision Records | Why each major technical decision was made |
| Error Catalogue | Every API error with status code, message, and source |
| Non-Functional Requirements | Performance, availability, security, scalability targets |
| API Documentation | Complete REST API reference |
- Multi-tenant workspaces — isolated data per workspace with role-based member access
- Entity management — track customers, employees, vendors, and other entities with custom roles
- Document management — upload files, attach metadata, track expiry (VALID / EXPIRING / EXPIRED)
- Work item tracking — tasks/activities with a status state machine and document linking
- Flexible templates — define reusable document types and work item types with custom fields
- Audit logging — automatic compliance trail for all write operations (queryable)
- Dashboard overview — aggregated counts by status/role across all resource types
- Real-time updates — Socket.io WebSocket events on the same port as the REST API
- Interactive API docs — Swagger UI at
/api-docs
| Category | Technology | Version |
|---|---|---|
| Runtime | Node.js + Express.js | Express 5.2.1 |
| Language | TypeScript | 5.9.3 |
| Database | MongoDB + Mongoose | Mongoose 9.1.5 |
| Authentication | JWT (jsonwebtoken) | 9.0.3 |
| Password hashing | bcrypt | 6.0.0 |
| Real-time | Socket.io | 4.8.3 |
| File uploads | Multer | 2.0.2 |
| API docs | Swagger UI Express | 5.0.1 |
| Dev server | ts-node-dev | 2.0.0 |
| Linting | ESLint + Prettier | 9.39.2 / 3.8.1 |
The project follows Clean Architecture with four layers per module:
Domain → Application → Interfaces → Infrastructure
- Domain — entities, value objects, pure business logic
- Application — use cases that orchestrate domain logic
- Interfaces — HTTP controllers and response presenters
- Infrastructure — Mongoose repositories, external services
Dependency injection is done manually — each route file instantiates repositories, injects them into use cases, and injects use cases into controllers. No DI container is used.
src/
├── app.ts # Express setup, route registration, Swagger
├── server.ts # HTTP server + Socket.io initialization
├── config/
│ ├── database.ts # MongoDB connection
│ └── env.ts # Environment variable loading
├── common/
│ └── middleware/ # Auth, RBAC, error handling
├── infrastructure/
│ ├── socket/ # Socket.io server
│ └── idempotency/ # Duplicate-request prevention
├── shared/
│ └── domain/ # Shared error classes, utilities
└── modules/
├── auth/
├── workspace/
├── entity/
├── document-type/
├── document/
├── work-item/
├── audit-log/
└── overview/
Each module contains its own domain/, application/use-cases/, interfaces/, and infrastructure/ subdirectories.
- Node.js 18+
- MongoDB (local or Atlas)
git clone <repo-url>
cd workspaceops-backend
npm installcp .env.example .env
# Edit .env with your values (see Environment Variables section)npm run dev
# Server starts on http://localhost:4000 with hot reloadnpm run build # Compile TypeScript → dist/
npm start # Run dist/server.js# Health check (no auth required)
curl http://localhost:4000/health
# Interactive API docs
open http://localhost:4000/api-docsnpx tsc --noEmitCopy .env.example to .env and set the following:
# Server
PORT=4000
# Database — choose one
MONGO_URI=mongodb://localhost:27017/workspaceops
# MONGO_URI=mongodb+srv://<user>:<pass>@cluster0.xxxxx.mongodb.net/workspaceops?retryWrites=true&w=majority
# Authentication
JWT_SECRET=change_this_to_a_long_random_string_in_production
# File storage
UPLOAD_DIR=./uploads
MAX_FILE_SIZE_MB=10
# Business logic
EXPIRY_WARNING_DAYS=30 # Documents expiring within N days are flagged as EXPIRING
# CORS
FRONTEND_URL=http://localhost:3000All endpoints (except auth and /health) require a valid JWT token:
Authorization: Bearer <token>
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/auth/signup |
None | Register a new user (creates a default workspace) |
POST |
/auth/login |
None | Login and receive a JWT token |
Signup request:
{
"name": "Jane Doe",
"email": "jane@example.com",
"password": "securepassword"
}Signup response:
{
"success": true,
"data": {
"userId": "...",
"workspaceId": "...",
"token": "eyJ..."
},
"message": "User registered successfully"
}Login request:
{
"email": "jane@example.com",
"password": "securepassword"
}Login response:
{
"success": true,
"data": {
"userId": "...",
"token": "eyJ..."
}
}| Method | Path | Role Required | Description |
|---|---|---|---|
POST |
/workspaces |
Authenticated | Create a new workspace |
GET |
/workspaces |
Authenticated | List all workspaces the user belongs to |
GET |
/workspaces/:id/members |
ADMIN+ | List workspace members |
POST |
/workspaces/:id/members |
ADMIN+ | Invite a user by email |
PUT |
/workspaces/:id/members/:memberId |
ADMIN+ | Update a member's role |
DELETE |
/workspaces/:id/members/:memberId |
ADMIN+ | Remove a member |
Invite member request body:
{
"invitedEmail": "user@example.com",
"role": "MEMBER"
}| Method | Path | Role Required | Description |
|---|---|---|---|
POST |
/workspaces/:workspaceId/entities |
MEMBER+ | Create a new entity |
GET |
/workspaces/:workspaceId/entities |
MEMBER+ | List entities (filterable by role) |
GET |
/workspaces/:workspaceId/entities/:id |
MEMBER+ | Get a single entity |
PUT |
/workspaces/:workspaceId/entities/:id |
MEMBER+ | Update an entity |
DELETE |
/workspaces/:workspaceId/entities/:id |
ADMIN+ | Delete an entity |
Create entity request:
{
"name": "Acme Corp",
"role": "CUSTOMER"
}Entity response:
{
"id": "...",
"workspaceId": "...",
"name": "Acme Corp",
"role": "CUSTOMER",
"createdAt": "2026-01-01T00:00:00.000Z"
}List response:
{
"entities": [...],
"count": 5
}Define reusable templates for documents, with optional custom metadata fields and expiry tracking.
| Method | Path | Role Required | Description |
|---|---|---|---|
POST |
/workspaces/:workspaceId/document-types |
ADMIN+ | Create a document type |
GET |
/workspaces/:workspaceId/document-types |
MEMBER+ | List all document types |
GET |
/workspaces/:workspaceId/document-types/:id |
MEMBER+ | Get document type with fields |
PUT |
/workspaces/:workspaceId/document-types/:id |
ADMIN+ | Update a document type |
POST |
/workspaces/:workspaceId/document-types/:id/fields |
ADMIN+ | Add a metadata field |
DELETE |
/workspaces/:workspaceId/document-types/:id |
ADMIN+ | Delete a document type |
Create document type request:
{
"name": "Insurance Certificate",
"hasMetadata": true,
"hasExpiry": true,
"fields": [
{ "fieldKey": "Policy Number", "fieldType": "text", "isRequired": true },
{ "fieldKey": "Expiry Date", "fieldType": "date", "isRequired": true, "isExpiryField": true }
]
}Note:
fieldTypemust be lowercase —"text"or"date". IfhasMetadata: true, at least one field is required. IfhasExpiry: true, at least one field must haveisExpiryField: trueandfieldType: "date".
| Method | Path | Role Required | Description |
|---|---|---|---|
POST |
/workspaces/:workspaceId/documents |
MEMBER+ | Upload a document (multipart/form-data) |
GET |
/workspaces/:workspaceId/documents |
MEMBER+ | List documents (filterable by expiryStatus, entityId) |
GET |
/workspaces/:workspaceId/documents/expiring |
MEMBER+ | List only expiring or expired documents |
GET |
/workspaces/:workspaceId/documents/:id |
MEMBER+ | Get a single document |
GET |
/workspaces/:workspaceId/documents/:id/download |
MEMBER+ | Download the file (binary stream) |
GET |
/workspaces/:workspaceId/entities/:entityId/documents |
MEMBER+ | List an entity's documents |
PUT |
/workspaces/:workspaceId/documents/:id |
MEMBER+ | Update document metadata |
DELETE |
/workspaces/:workspaceId/documents/:id |
ADMIN+ | Delete a document |
Upload request (multipart/form-data):
file (binary)
documentTypeId
entityId
metadata (JSON string of field values)
Document response:
{
"id": "...",
"workspaceId": "...",
"entityId": "...",
"documentTypeId": "...",
"fileName": "certificate.pdf",
"expiryStatus": "EXPIRING",
"downloadUrl": "/workspaces/.../documents/.../download",
"metadata": { "Policy Number": "POL-1234", "Expiry Date": "2026-06-01" },
"createdAt": "2026-01-01T00:00:00.000Z"
}Expiry statuses: VALID | EXPIRING | EXPIRED
| Method | Path | Role Required | Description |
|---|---|---|---|
POST |
/workspaces/:workspaceId/work-item-types |
ADMIN+ | Create a work item type |
GET |
/workspaces/:workspaceId/work-item-types |
MEMBER+ | List all work item types |
DELETE |
/workspaces/:workspaceId/work-item-types/:id |
ADMIN+ | Delete a work item type |
| Method | Path | Role Required | Description |
|---|---|---|---|
POST |
/workspaces/:workspaceId/work-items |
MEMBER+ | Create a work item |
GET |
/workspaces/:workspaceId/work-items |
MEMBER+ | List work items (filterable by status, entityId, typeId) |
GET |
/workspaces/:workspaceId/work-items/:id |
MEMBER+ | Get a single work item |
PUT |
/workspaces/:workspaceId/work-items/:id |
MEMBER+ | Update work item fields |
PATCH |
/workspaces/:workspaceId/work-items/:id/status |
MEMBER+ | Transition status |
POST |
/workspaces/:workspaceId/work-items/:id/documents |
MEMBER+ | Link a document to a work item |
GET |
/workspaces/:workspaceId/work-items/:id/documents |
MEMBER+ | Get linked documents |
DELETE |
/workspaces/:workspaceId/work-items/:id/documents/:docId |
MEMBER+ | Unlink a document |
DELETE |
/workspaces/:workspaceId/work-items/:id |
ADMIN+ | Delete a work item |
GET |
/workspaces/:workspaceId/entities/:entityId/work-items |
MEMBER+ | List an entity's work items |
Status state machine:
DRAFT ↔ ACTIVE ↔ COMPLETED
Valid transitions: DRAFT → ACTIVE, ACTIVE → DRAFT, ACTIVE → COMPLETED, COMPLETED → ACTIVE
Invalid: DRAFT ↔ COMPLETED (no direct transition)
Work item response:
{
"id": "...",
"workspaceId": "...",
"entityId": "...",
"workItemTypeId": "...",
"title": "Annual review",
"description": "...",
"status": "ACTIVE",
"linkedDocumentIds": ["...", "..."],
"linkedDocumentCount": 2,
"createdAt": "2026-01-01T00:00:00.000Z"
}All write operations are automatically recorded. Logs are query-only.
| Method | Path | Role Required | Description |
|---|---|---|---|
GET |
/workspaces/:workspaceId/audit-logs |
ADMIN+ | Query audit logs |
Query parameters:
| Param | Type | Description |
|---|---|---|
userId |
string | Filter by actor |
action |
string | Filter by action type (e.g. CREATE, UPDATE, DELETE) |
targetType |
string | Filter by resource type (e.g. document, workItem) |
targetId |
string | Filter by specific resource ID |
fromDate |
ISO string | Start of date range |
toDate |
ISO string | End of date range |
limit |
number | Page size (default: 50, max 200) |
offset |
number | Pagination offset (default: 0) |
Response:
{
"total": 150,
"limit": 50,
"offset": 0,
"logs": [
{
"id": "...",
"workspaceId": "...",
"userId": "...",
"action": "CREATE",
"targetType": "document",
"targetId": "...",
"metadata": {},
"createdAt": "2026-01-01T00:00:00.000Z"
}
]
}| Method | Path | Role Required | Description |
|---|---|---|---|
GET |
/workspaces/:workspaceId/overview |
MEMBER+ | Dashboard aggregation |
Response:
{
"workspaceId": "...",
"entities": {
"total": 10,
"byRole": { "CUSTOMER": 5, "EMPLOYEE": 3, "VENDOR": 2 }
},
"documents": {
"total": 42,
"byStatus": { "VALID": 30, "EXPIRING": 8, "EXPIRED": 4 }
},
"workItems": {
"total": 18,
"byStatus": { "DRAFT": 5, "ACTIVE": 9, "COMPLETED": 4 }
},
"documentTypes": [...],
"workItemTypes": [...]
}All workspace-scoped endpoints enforce role-based access control. Roles are per workspace (a user can be OWNER in one workspace and MEMBER in another).
| Role | Capabilities |
|---|---|
| OWNER | Full control including workspace settings and member management |
| ADMIN | Manage members, create/delete types and resources |
| MEMBER | Create and read most resources; update own work |
| VIEWER | Read-only (reserved) |
The workspaceId is read from the route parameter (req.params.workspaceId or req.params.id).
Socket.io runs on the same port as the REST API (4000). Clients must authenticate with a valid JWT token on connect.
Connect with auth:
const socket = io("http://localhost:4000", {
auth: { token: "Bearer eyJ..." }
});Join a workspace room:
socket.emit("join-workspace", { workspaceId: "..." });Once joined, the client receives events when workspace resources change. Events follow REST semantics (create, update, delete) and include the updated resource payload.
hasMetadata: truerequires at least one field defined infields[]hasExpiry: truerequires at least one field with bothisExpiryField: trueandfieldType: "date"- Field types are lowercase strings:
"text"or"date"(not"TEXT"/"DATE")
- Expiry status is computed on the fly (not stored): documents within
EXPIRY_WARNING_DAYSareEXPIRING; past-expiry areEXPIRED
- Status transitions must follow the state machine — no skipping states
- Documents can be linked to multiple work items
- Invite by
invitedEmail(email address of an existing registered user) - Each user can hold only one role per workspace
- Implemented as a fire-and-forget injectable service
- Failures are silently ignored and never propagate to the request/response cycle
workspaceops-backend/
├── src/
│ ├── app.ts
│ ├── server.ts
│ ├── config/
│ │ ├── database.ts
│ │ └── env.ts
│ ├── common/
│ │ └── middleware/
│ │ ├── auth.middleware.ts
│ │ └── rbac.middleware.ts
│ ├── infrastructure/
│ │ ├── socket/
│ │ └── idempotency/
│ ├── shared/
│ │ └── domain/
│ └── modules/
│ ├── auth/
│ ├── workspace/
│ ├── entity/
│ ├── document-type/
│ ├── document/
│ ├── work-item/
│ ├── audit-log/
│ └── overview/
├── swagger.yaml # OpenAPI specification
├── .env.example # Environment variable template
├── tsconfig.json
├── package.json
└── PLAN/
├── implementation_plan.md
├── audit_log_implementation_plan.md
└── test_report.md