01 β€” Architecture Overview

← Index

What IP4CMS is

IP4CMS is a vertical of the IP4X platform β€” a multi-tenant SaaS for community / estate / membership management. The platform is built once and deployed into many verticals; IP4CMS enables the membership, property, communications, financial, access-control and engagement modules. This document describes the runtime architecture that all verticals share, as configured for IP4CMS.

The two planes

The system is split into a control plane and a data plane. This split is the single most important architectural idea β€” almost everything else follows from it.

Control plane β€” core-api

The authority over the platform, not over any tenant's business data. Responsibilities:

Source: src/core-api/src.

Data plane β€” tenant-api

The authority over tenant business data. Each running tenant-api instance:

Source: src/tenant-api/src. Top-level layout: controllers/ (HTTP handlers, grouped by module), services/ (business logic), routes/ (middleware wiring), repositories/, database/ (schema + connection + migration), queue/ (BullMQ processors), middleware/, schemas/, jobs/, shared/.

Admin portal β€” tenant-frontend

An Angular single-page app; the staff/operator UI. It talks to two backends:

Every API call carries Authorization: Bearer <JWT> and x-tenant-id: <tenant> (the data plane requires the tenant header for scoping). Feature modules are lazy-loaded behind an authenticated shell; navigation is filtered by the tenant's enabled modules. See 08.

There are also member/resident portals and native apps (src/public-sites/..., native-apps/...) β€” the end-user-facing side β€” which consume the same tenant-api with member-scoped permissions. This reference focuses on the platform and the admin portal.

Request lifecycle (data plane)

A typical authenticated request to the tenant-api passes through a fixed middleware chain (wired per route in src/tenant-api/src/routes/index.ts):

HTTP request
  β†’ tenantMiddleware        resolve req.tenantId (header / subdomain / path / token)
  β†’ authenticateJWT         verify identity, resolve req.user.permissions
  β†’ requireModule(...)      LICENSE gate β€” is this module in the tenant's enabled_modules?
  β†’ requirePermission(...)  RBAC gate β€” does the principal hold the permission?
  β†’ controller β†’ service β†’ repository β†’ Postgres (tenant DB resolved by tenantId)

Two orthogonal gates therefore protect every feature: a tenant-level entitlement (the license) and a principal-level permission (RBAC). Both must pass. Public/unauthenticated endpoints use a separate functionAuthMiddleware allowlist keyed by domain. Full detail in 04.

Data & async infrastructure

How a tenant is "wired"

A single tenant-api instance can be shared (serves many tenants; resolves the tenant per request and looks up the right DB via getDatabase(tenantId)) or dedicated (pinned to one tenant via the TENANT_ID env, with TENANT_DB_NAME and REDIS_PREFIX). The same image supports both modes; the difference is purely configuration + how core-api orchestrates it. This is the basis of compute isolation β€” see 02 and 03.


Key source paths: src/core-api/src, src/tenant-api/src, src/tenant-frontend/src/app, src/tenant-api/src/routes/index.ts, src/tenant-api/src/server.ts.

Next: 02 β€” Tenant provisioning & orchestration β†’