01 β Architecture Overview
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:
- The registry: tenants, regions, licenses, deployments, domains, branding.
- Provisioning decisions: given a new tenant's characteristics (plan, compliance, scale, region), decide how it is deployed β see 02.
- Orchestration: for dedicated tenants, create the compute (a Kubernetes Deployment/Service/Ingress) via
KubernetesOrchestrationService. - Authentication & domain resolution: the portal authenticates against core-api and resolves which tenant/branding a domain maps to.
- License authoring: encode each tenant's enabled modules into an encrypted license β see 04.
- Platform metering/billing: meter tenant usage and compute what the tenant owes the platform β see 06.
Source: src/core-api/src.
Data plane β tenant-api
The authority over tenant business data. Each running tenant-api instance:
- Serves all functional modules (members, financials, properties, communications, β¦) over a REST API β see 07.
- Owns its schema: creates and migrates its own tenant databases on instruction from core-api (it runs the
CREATE DATABASE+ migrations itself). - Runs the async workers (BullMQ over Redis) for billing, statements, settlement, communications, etc.
- Enforces the license and RBAC on every request.
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:
- core-api (
environment.coreApiUrl) for authentication and domain resolution. - tenant-api (the regional API,
environment.apiBaseUrl) for all feature data.
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
- PostgreSQL is the system of record. In production the tenant DBs live on Azure Cosmos DB for PostgreSQL (Citus) β a distributed Postgres. Topology and the shared-vs-dedicated model are in 03.
- Redis backs BullMQ queues (billing, statements, settlement, usage snapshots, communications) and distributed locks (
RedisLockService) and caches. Workers are at-least-once β a job can run more than once on crash/retry, so money-touching handlers are made idempotent (see 05). - Object storage (S3-compatible,
STORAGE_*) holds generated PDFs (statements, certificates), documents, gallery images.
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.