M2 — Maintenance Management System
M2 is a Computerised Maintenance Management System (CMMS) built for the Inspiring Living Solutions (ILS) property management platform. It handles work orders, asset tracking, preventive maintenance scheduling, vendor management, inventory, purchase orders, and emergency response for villa properties.
- Production URL:
https://m2.inspiringlivingsolutions.com - Company: Inspiring Living Solutions / Inspiring Group (UAE)
- Codebase: Next.js full-stack monolith (API + frontend in one app)



System Architecture
Section titled “System Architecture”graph TD Browser["Browser / PWA"] NextApp["M2 Next.js App\nm2.inspiringlivingsolutions.com"] AuthJS["NextAuth v5\nJWT Session"] Authentik["Authentik SSO\nsso.inspiringroup.com"] AzureAD["Microsoft Azure AD\nIdentity Provider"] Bitrix["Bitrix24\nAlternate Auth"] Postgres["PostgreSQL\nPrisma ORM"] GMaps["Google Maps API\nGeocoding / Places"] Sentry["Sentry\nsentry.dabz.me\nError Monitoring"]
Browser -->|HTTPS| NextApp NextApp -->|JWT verify| AuthJS AuthJS -->|OIDC| Authentik Authentik -->|OAuth| AzureAD AuthJS -->|OAuth| Bitrix NextApp -->|Prisma| Postgres NextApp -->|JS API Loader| GMaps NextApp -->|SDK| SentryHow M2 Fits the ILS Ecosystem
Section titled “How M2 Fits the ILS Ecosystem”graph LR OBV["OBV (M1)\nOnboarding Wizard\nobv.inspiringlivingsolutions.com"] M2["M2\nMaintenance CMMS\nm2.inspiringlivingsolutions.com"] M3["M3\nInventory\nm3.inspiringlivingsolutions.com"] M4["M4\nReporting"] M7["M7\nHardware Assets\nm7.inspiringlivingsolutions.com"] SSO["Authentik SSO\nShared Identity Provider"] AzureAD["Microsoft Azure AD"]
SSO -->|OIDC| OBV SSO -->|OIDC| M2 SSO -->|OIDC| M3 SSO -->|OIDC| M4 SSO -->|OIDC| M7 AzureAD -->|IdP| SSOAll ILS microservices share the same Authentik SSO instance. M2 does not share its database with any other service — each module maintains its own PostgreSQL instance.
Tech Stack
Section titled “Tech Stack”Runtime & Language
Section titled “Runtime & Language”| Technology | Version | Purpose |
|---|---|---|
| Node.js | 22+ | Runtime |
| TypeScript | 5.9.3 | Language |
| Next.js | 16.1.6 | Full-stack framework (App Router) |
| React | 19 | UI library |
Package Management & Build
Section titled “Package Management & Build”| Tool | Version | Purpose |
|---|---|---|
| Yarn | 1.22.22 | Package manager |
| Vite | 7.3.0 | Build tooling / Vitest runner |
| PostCSS | 8.5.6 | CSS transformation |
Database & ORM
Section titled “Database & ORM”| Technology | Version | Purpose |
|---|---|---|
| PostgreSQL | 16 | Primary database |
| Prisma | 7.3.0 | ORM + migrations |
| pg | 8.16.3 | PostgreSQL client adapter |
Frontend Libraries
Section titled “Frontend Libraries”| Library | Version | Purpose |
|---|---|---|
| Tailwind CSS | 4 | Utility-first styling |
| shadcn/ui | Latest | Component library (Sky/Zinc theme) |
| Radix UI | Various | Headless UI primitives |
| React Hook Form | 7.69.0 | Form state management |
| Zod | 4.3.4 | Schema validation |
| Zustand | 5.0.9 | Global state management |
| TanStack React Table | 8.21.3 | Data tables |
| React Big Calendar | 1.19.4 | PMP schedule calendar |
| Sonner | 2.0.7 | Toast notifications |
| next-intl | 4.6.1 | i18n (English + Thai) |
| next-themes | 0.4.6 | Dark/light mode |
| Lucide React | 0.562.0 | Icons |
Authentication
Section titled “Authentication”| Library | Version | Purpose |
|---|---|---|
| NextAuth | v5 beta 30 | Auth framework |
| Authentik (OIDC) | — | Primary SSO provider |
| Bitrix24 (OAuth) | — | Secondary login provider |
Observability & Testing
Section titled “Observability & Testing”| Tool | Version | Purpose |
|---|---|---|
Sentry (@sentry/nextjs) | 10.32.1 | Error monitoring |
| Vitest | 4.0.16 | Unit + integration tests |
@testing-library/react | 16.3.1 | Component tests |
@vitest/coverage-v8 | 4.0.18 | Test coverage |
Folder Structure
Section titled “Folder Structure”m2-main/├── src/│ ├── app/ # Next.js App Router│ │ ├── api/ # All REST API route handlers│ │ │ ├── assets/│ │ │ ├── auth/ # NextAuth + Bitrix OAuth routes│ │ │ ├── emergency-contacts/│ │ │ ├── incidents/│ │ │ ├── inventory/│ │ │ ├── parts/│ │ │ ├── pmp-schedules/│ │ │ ├── pmp-templates/│ │ │ ├── purchase-orders/│ │ │ ├── requests/│ │ │ ├── response-teams/│ │ │ ├── staff/│ │ │ ├── teams/│ │ │ ├── vendors/│ │ │ ├── villa-locations/│ │ │ ├── villas/│ │ │ ├── work-orders/│ │ │ └── zones/│ │ ├── assets/ # Asset management pages│ │ ├── emergency/ # Emergency management pages│ │ ├── inventory/ # Inventory pages│ │ ├── locations/ # Villa location pages│ │ ├── login/ # Login page│ │ ├── parts/ # Parts management pages│ │ ├── pmp/ # PMP calendar + templates│ │ ├── purchase-orders/ # Purchase order pages│ │ ├── reports/ # Analytics & reporting│ │ ├── requests/ # Maintenance request pages│ │ ├── settings/ # Settings page│ │ ├── staff/ # Staff & team management│ │ ├── vendors/ # Vendor management│ │ ├── villas/ # Villa management + map view│ │ ├── work-orders/ # Work order pages│ │ ├── layout.tsx # Root layout (Sentry, theme, i18n)│ │ └── page.tsx # Dashboard│ ├── components/│ │ ├── ui/ # shadcn/ui primitives│ │ ├── analytics/ # Dashboard charts│ │ ├── assets/ # Asset form, list, detail components│ │ ├── emergency/ # Incident form, map, contacts│ │ ├── locations/ # Location management│ │ ├── maps/ # Google Maps wrappers│ │ ├── parts/ # Parts & inventory components│ │ ├── pmp/ # PMP calendar, templates│ │ ├── purchase-orders/ # PO form, detail components│ │ ├── requests/ # Request form, detail│ │ ├── staff/ # Staff list, profile, team│ │ ├── vendors/ # Vendor list, contracts, pricing│ │ ├── villas/ # Villa form, map, detail│ │ ├── work-orders/ # WO form, detail, tasks, checklist│ │ ├── app-header.tsx # Top navigation bar│ │ ├── app-sidebar.tsx # Side navigation│ │ ├── cmms-dashboard.tsx # Main dashboard component│ │ ├── data-table.tsx # Generic reusable table (TanStack)│ │ ├── sidebar-layout.tsx # Page layout wrapper│ │ └── status-badge.tsx # Status/priority badge│ ├── stores/ # Zustand state stores│ │ ├── work-order-store.ts│ │ ├── asset-store.ts│ │ ├── emergency-store.ts│ │ ├── inventory-store.ts│ │ ├── parts-store.ts│ │ ├── purchase-orders-store.ts│ │ ├── requests-store.ts│ │ ├── vendor-store.ts│ │ └── ui-store.ts│ ├── lib/│ │ ├── validations/ # Zod schemas per module│ │ ├── api/ # Client-side API fetch helpers│ │ ├── utils/ # Shared utilities (dates, labels)│ │ ├── pmp/ # PMP recurrence helpers│ │ ├── db.ts # Prisma client singleton│ │ ├── auth-server.ts # Server-side auth helpers│ │ ├── auth-client.ts # Client-side auth helpers│ │ ├── bitrix-user.ts # Bitrix24 user sync│ │ ├── password.ts # bcrypt helpers│ │ ├── mock-data.ts # Development/seed data (71 KB)│ │ └── maintenance-alert.ts # Alert dispatch logic│ ├── contexts/│ │ └── auth-context.tsx # React auth context│ ├── types/│ │ ├── villa.ts│ │ └── zone.ts│ ├── i18n/│ │ ├── config.ts # Supported locales: en, th│ │ └── request.ts│ ├── test/│ │ ├── setup.ts # Vitest global setup│ │ └── integration/ # Integration test helpers│ └── auth.ts # NextAuth config (providers, callbacks)├── prisma/│ └── schema.prisma # Full database schema (1327 lines)├── scripts/ # E2E API test scripts├── messages/ # i18n translation files (en, th)├── nginx/ # Nginx reverse proxy config├── Dockerfile # Production multi-stage build├── Dockerfile.migrate # Migration-only container├── docker-compose.yml # Local development├── docker-compose.prod.yml # Production deployment├── .env.example # Development env template├── .env.production.example # Production env template├── DEPLOYMENT.md # Deployment runbook└── README.md # Authentik SSO setup guideEnvironment Variables
Section titled “Environment Variables”Development (.env.example)
Section titled “Development (.env.example)”| Variable | Example | Description |
|---|---|---|
HOSTNAME | 0.0.0.0 | Bind address |
PORT | 3000 | HTTP port |
LOG_LEVEL | info | Log verbosity (debug, info, warn, error) |
NEXT_PUBLIC_SENTRY_DSN | (empty) | Sentry DSN for client-side error reporting |
SENTRY_AUTH_TOKEN | (empty) | Sentry token for source map upload |
AUTH_URL | http://localhost:3000 | Full app URL (used by NextAuth) |
AUTH_SECRET | (generated) | NextAuth secret — run openssl rand -base64 32 |
AUTH_TRUST_HOST | true | Trust X-Forwarded-Host header |
AUTH_AUTHENTIK_ID | (empty) | Authentik OAuth client ID |
AUTH_AUTHENTIK_SECRET | (empty) | Authentik OAuth client secret |
AUTH_AUTHENTIK_ISSUER | https://sso.inspiringroup.com/application/o/m2/ | Authentik OIDC issuer URL |
NEXT_PUBLIC_AUTH_AUTHENTIK_ISSUER | (same) | Issuer URL exposed to client |
DATABASE_URL | prisma+postgres://localhost:51213/?api_key=... | Prisma Data Proxy URL for local dev |
Production (.env.production.example)
Section titled “Production (.env.production.example)”| Variable | Example | Description |
|---|---|---|
NODE_ENV | production | Node environment |
POSTGRES_PASSWORD | change_me_strong_password | PostgreSQL root password |
DATABASE_URL | postgresql://postgres:PASSWORD@postgres:5432/m2_cmms | Direct PostgreSQL connection string |
AUTH_URL | https://your-domain.com | Public-facing app URL |
AUTH_SECRET | (generate) | NextAuth secret — never share |
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY | (optional) | Google Maps JS API key |
Authentication Flow
Section titled “Authentication Flow”sequenceDiagram participant User participant M2 as M2 App participant NextAuth participant Authentik participant AzureAD participant DB as PostgreSQL
User->>M2: GET /login M2->>NextAuth: Initiate sign-in NextAuth->>Authentik: OIDC Authorization Request Authentik->>AzureAD: Federated login AzureAD-->>Authentik: Identity token Authentik-->>NextAuth: ID token + user info NextAuth->>DB: Upsert user record NextAuth-->>M2: JWT session cookie M2-->>User: Redirect to dashboard
Note over M2,DB: Bitrix24 fallback: if no session,<br/>check cookie for Bitrix OAuth tokenAuth Helpers
Section titled “Auth Helpers”| Function | File | Purpose |
|---|---|---|
getAuthenticatedUser() | lib/auth-server.ts | Primary entry — checks session then Bitrix token |
getCurrentUserServer() | lib/auth-server.ts | Reads JWT from cookie |
verifyPassword() | lib/password.ts | bcrypt comparison for credential provider |
Note: Demo bypass credentials (
[email protected]:123,[email protected]:123) auto-create accounts in the database. Remove these for production.
Design Patterns
Section titled “Design Patterns”API Route Pattern
Section titled “API Route Pattern”Every API route follows this structure:
export async function GET(request: NextRequest) { const user = await getAuthenticatedUser(); if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// Parse query params → build Prisma where clause → paginate const data = await prisma.resource.findMany({ where, skip, take }); return NextResponse.json(data);}
export async function POST(request: NextRequest) { const user = await getAuthenticatedUser(); if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = await request.json(); const parsed = resourceSchema.safeParse(body); if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
const record = await prisma.resource.create({ data: parsed.data }); return NextResponse.json(record, { status: 201 });}Standard API Response Codes
Section titled “Standard API Response Codes”| Code | Meaning |
|---|---|
200 | Success (GET, PUT, PATCH) |
201 | Created (POST) |
400 | Validation error — body contains Zod issue array |
401 | Unauthenticated — no valid session |
404 | Resource not found |
500 | Internal server error |
State Management (Zustand)
Section titled “State Management (Zustand)”One store per major domain. Each store holds:
- Entity list + filter state
- Selected item
- Loading / error flags
- CRUD action methods
Stores use persist middleware to cache filters to localStorage and devtools for debug inspection.
Form Validation (Zod + React Hook Form)
Section titled “Form Validation (Zod + React Hook Form)”- Zod schemas live in
src/lib/validations/[module].ts - Types are derived with
z.infer<typeof schema> - The same schema is used for server-side validation in API routes and client-side validation in forms
Database Transactions
Section titled “Database Transactions”Complex multi-step writes use prisma.$transaction([...]) to ensure atomicity (e.g., creating a work order and its initial activity log entry together).
API Request Flow
Section titled “API Request Flow”sequenceDiagram participant Client participant Route as Next.js API Route participant Auth as getAuthenticatedUser() participant Zod as Zod Schema participant Prisma participant DB as PostgreSQL
Client->>Route: HTTP Request Route->>Auth: Validate session Auth-->>Route: User | null alt Unauthenticated Route-->>Client: 401 Unauthorized end Route->>Zod: Parse request body alt Invalid body Route-->>Client: 400 { error: issues[] } end Route->>Prisma: Query / Mutation Prisma->>DB: SQL DB-->>Prisma: Result Prisma-->>Route: Typed result Route-->>Client: 200/201 JSONThird-Party Integrations
Section titled “Third-Party Integrations”graph TD M2["M2 App"] Authentik["Authentik OIDC\nsso.inspiringroup.com"] AzureAD["Microsoft Azure AD"] Bitrix24["Bitrix24 OAuth"] GMaps["Google Maps API\nPlaces + Geocoding"] Sentry["Sentry (self-hosted)\nsentry.dabz.me"]
M2 -->|OIDC auth flow| Authentik Authentik -->|identity federation| AzureAD M2 -->|fallback login| Bitrix24 M2 -->|villa map + place search| GMaps M2 -->|errors + source maps| Sentry| Integration | Package | Usage |
|---|---|---|
| Authentik | next-auth/providers | Primary SSO — all users login via Authentik |
| Microsoft Azure AD | (via Authentik) | Identity source |
| Bitrix24 | lib/bitrix-user.ts | Alternative OAuth login, user profile sync |
| Google Maps | @googlemaps/js-api-loader, @react-google-maps/api | Villa map view, address autocomplete, geocoding |
| Sentry | @sentry/nextjs | Client + server error capture, source map upload |
Note: No email delivery service (SendGrid, SES, etc.) is currently wired up. Alert notifications use in-app toasts only. Email integration requires manual implementation.
Note: File/image uploads accept URL strings in the schema. No S3/CDN/blob storage is currently integrated — the upload endpoints are placeholders.
Key Workflows
Section titled “Key Workflows”Work Order Lifecycle
Section titled “Work Order Lifecycle”sequenceDiagram participant Reporter participant Manager participant Technician participant M2 as M2 API participant DB
Reporter->>M2: POST /api/work-orders M2->>DB: Create WorkOrder (status: PENDING) M2->>DB: Log WorkOrderActivity (created) Manager->>M2: PATCH /work-orders/:id/status (ASSIGNED) M2->>DB: Update assignedToId, status Technician->>M2: PATCH /work-orders/:id/status (IN_PROGRESS) Technician->>M2: POST /work-orders/:id/comments Technician->>M2: PATCH /work-orders/:id/tasks/:taskId (COMPLETED) Technician->>M2: PATCH /work-orders/:id/status (COMPLETED) M2->>DB: Set completedAt, log activity Manager->>M2: Verify and closeRequest → Work Order Conversion
Section titled “Request → Work Order Conversion”sequenceDiagram participant User participant M2 as M2 API participant DB
User->>M2: POST /api/requests (status: PENDING) DB-->>M2: Request created (REQ-XXXX) Manager->>M2: PATCH /api/requests/:id/convert M2->>DB: Create WorkOrder linked to requestId M2->>DB: Update Request status → IN_PROGRESS M2-->>Manager: { workOrderId, workOrderNumber }Preventive Maintenance (PMP) Scheduling
Section titled “Preventive Maintenance (PMP) Scheduling”sequenceDiagram participant Admin participant M2 as M2 API participant DB participant Scheduler
Admin->>M2: POST /api/pmp-templates (checklist + frequency) Admin->>M2: POST /api/pmp-schedules (template + villa/asset) M2->>DB: Create PMPSchedule (nextDueDate calculated) Scheduler->>M2: POST /api/pmp-schedules/generate-recurrence M2->>DB: Insert future PMPSchedule instances M2-->>Technician: Schedules visible on PMP calendarUser Roles
Section titled “User Roles”| Role | Access Level |
|---|---|
ADMIN | Full access — configuration, user management, all modules |
MANAGER | All properties, work order assignment, reporting |
TECHNICIAN | Assigned work orders, field notes, timesheets |
VENDOR | Assigned external work orders only |
Local Development
Section titled “Local Development”Prerequisites
Section titled “Prerequisites”- Node.js 22+
- Yarn 1.22.x
- Docker (for PostgreSQL)
- Access to Authentik SSO credentials (or use demo bypass accounts)
# Install dependenciesyarn install
# Copy environment file and fill in valuescp .env.example .env
# Generate Prisma clientnpx prisma generate
# Run database migrationsnpx prisma migrate dev
# Seed with mock data (optional)npx prisma db seed
# Start development serveryarn devRunning Tests
Section titled “Running Tests”yarn test # Unit tests in watch modeyarn test:run # Unit tests onceyarn test:integration # Integration testsyarn test:coverage # Coverage reportDocker (Full Stack)
Section titled “Docker (Full Stack)”docker-compose up -d # Starts app + PostgreSQLDeployment
Section titled “Deployment”M2 is deployed as a Docker container on Digital Ocean via Coolify.
graph LR GitLab["GitLab CI\n.gitlab-ci.yml"] Docker["Docker Build\nmulti-stage"] Coolify["Coolify\nDeploy Host"] App["M2 Container\nNode 22 Alpine"] PG["PostgreSQL\nContainer"] Nginx["Nginx\nReverse Proxy"]
GitLab -->|build image| Docker Docker -->|push| Coolify Coolify -->|run| App App --- PG Nginx -->|proxy| App- Build: Multi-stage Dockerfile — deps → build (Prisma generate +
next build) → runner (standalone output) - Migrations: Separate
Dockerfile.migraterunsprisma migrate deploybefore app start - Config: Secrets injected via Coolify environment variables
- Output:
next buildwithoutput: 'standalone'for minimal container size
See DEPLOYMENT.md in the repo root for the full deployment runbook.
Known Limitations & TODOs
Section titled “Known Limitations & TODOs”| Location | Issue |
|---|---|
api/purchase-orders/route.ts:54 | TODO: Get actual user ID from session — currently falls back to first user in DB |
components/vendors/new-contract-modal.tsx:51 | TODO: Wire to API once service contracts endpoint is ready |
| File uploads | fileUrl fields accept strings only — no storage backend (S3/CDN) integrated |
| Email notifications | No email service configured — alerts are in-app toasts only |
| Real-time updates | No WebSocket/SSE — polling only |
| QR code scanning | qrCode field exists on Asset model but no scanning UI |
| Bitrix24 sync | Only OAuth login implemented — work order sync not built |
| Mobile app | FieldNote and Timesheet models exist but no dedicated mobile UI |
Multi-Language Support
Section titled “Multi-Language Support”M2 is internationalised using next-intl. Supported locales:
| Code | Language |
|---|---|
en | English (default) |
th | Thai (ภาษาไทย) |
Translation message files are in messages/. Switch locale via the user menu.