3. Architecture
KMP is a modular CakePHP application deployed as a database-per-tenant managed platform. Server-rendered HTML is the default interface, with Stimulus and Turbo Frames adding focused interactivity. Domain plugins and a shared workflow engine extend the core without bypassing its authorization or tenant boundaries.
System map
┌────────────────────────────┐
request host ────────▶│ KMP web application │
│ routing → tenant binding │
│ → authn → authz → domain │
└──────────┬─────────────────┘
│
┌───────────────┴────────────────┐
│ │
┌─────────▼─────────┐ ┌─────────▼─────────┐
│ platform database │ │ selected tenant DB │
│ registry, secrets,│ │ core + active │
│ jobs, audit, ops │ │ plugin data │
└───────────────────┘ └────────────────────┘
The platform database is not a shared application database. Tenant business records never move into it merely to make cross-tenant queries easier. Each request or background operation may have at most one selected tenant database.
Repository boundaries
| Area | Responsibility |
|---|---|
app/config |
CakePHP configuration, routes, migrations, seeds, plugin loading |
app/src/Controller |
HTTP orchestration and authorization boundaries |
app/src/Model |
ORM persistence, entities, table rules, reusable behaviors |
app/src/Policy |
resource authorization and query scoping |
app/src/Services |
domain workflows, tenancy, platform operations, integrations |
app/src/KMP |
small application primitives such as TenantContext |
app/templates |
accessible server-rendered views and elements |
app/assets |
Vite-bundled Stimulus controllers and CSS |
app/plugins |
isolated first-party and infrastructure plugins |
app/tests |
PHPUnit, Jest, and Playwright tests |
deploy, docker, installer |
managed deployment, containers, legacy packaging |
Controllers stay thin: parse input, load a resource, authorize it, call a domain service/table operation, and choose a response. Multi-step business workflows belong in services; templates only render already-authorized data.
HTTP request flow
The important middleware ordering is:
- error handling, request correlation/performance context, and response security headers;
- static assets and CakePHP routing;
TenantResolutionMiddleware;- restore-maintenance enforcement and request-body parsing;
- security-token and CSRF handling;
- tenant member authentication and authorization; and
- footprint/audit attribution.
Routing occurs before tenant resolution so the middleware can recognize the health and platform-admin paths, but tenant binding occurs before any normal application authentication, authorization, controller, table, or template work. The platform-admin host follows its own central identity/session path and does not receive a tenant datasource binding.
For a tenant request, the host resolver reads the central registry and
TenantConnectionManager temporarily makes the selected physical connection
CakePHP’s default datasource. It installs a fresh table locator, applies
scoped mail settings, enters TenantContext, executes the request, and restores
all state in finally. An open transaction at scope exit is rolled back and
reported as an error.
See Multi-tenant architecture for failure responses, lifecycle states, cache/storage rules, and background context.
Application foundation
KMP builds on a small set of project abstractions:
- web controllers extend
AppController; API controllers extendApiController; - tables extend
BaseTable, entities extendBaseEntity, and policies extendBasePolicy; AuthorizationComponentmethods such asauthorize(),authorizeModel(), andapplyScope()enforce access consistently;- registries such as
ViewCellRegistryand navigation registries let plugins contribute UI without hard-coded core conditionals; DataverseGridTraitplusBaseGridColumnsdefine searchable and filterable grid screens;TimezoneHelperand frontend timezone utilities handle the UTC/display boundary.
Use these established abstractions before introducing a parallel framework. More detail is in Application foundation.
Domain and plugin model
Core owns members, branches, RBAC, warrants, gatherings, documents, settings, and the workflow engine. Active first-party domain plugins are loaded in this migration order:
- Activities
- Officers
- Awards
- Waivers
The Queue plugin is active infrastructure. Template remains an unloaded
skeleton. Plugin controllers, policies, services, cells, assets, migrations, and
tests remain under the plugin namespace. Cross-plugin collaboration uses public
services, events, registries, or workflow providers rather than querying another
plugin’s tables from templates.
Authorization model
Authentication establishes an identity; authorization decides what it may do. A request normally combines:
- an explicit resource or model authorization check;
- a policy scope applied to collection queries;
- branch hierarchy rules where the permission grants branch or descendant reach; and
- restore-lock and impersonation restrictions where applicable.
Do not replace these layers with controller name checks, raw role IDs, UI-only hiding, or ad hoc branch predicates. Platform operators are also not implicit tenant superusers: platform administration and tenant membership are separate identity domains.
Workflow engine
The shared workflow engine owns reusable definitions, instances, states, transitions, approvals, schedules, actions, and conditions. Core and plugins register domain-specific handlers through the existing provider/registry patterns. A domain record may trigger or reference a workflow, but the workflow engine remains the lifecycle authority once that flow begins.
Keep state changes transactional and idempotent. Scheduled reconciliation must enter tenant context and tolerate retries. Use the current approval endpoints and services rather than recreating historical feature-specific approval entities. See Workflow approval nodes.
Background processing
There are two persistence lanes:
queued_jobsin each tenant database for tenant application work; andplatform_jobsplusplatform_schedulesin the platform database for fleet operations.
PlatformWorkerService performs a bounded pass: dispatch due platform schedules,
drain enabled active-tenant queues with explicit context switching, then run
platform jobs. No job may retain a table instance, connection alias, mail
profile, cache namespace, or tenant metadata for use after the scope ends.
Data and migration tracks
KMP has independent platform, core tenant, and plugin tenant migrations. The platform schema is migrated first. A tenant release then inspects the complete core/plugin catalog, takes a PostgreSQL advisory lock per tenant, creates an encrypted pre-migration backup marker when needed, applies pending migrations, and verifies every history scope before recording the tenant schema version. Suspended tenants remain migration-current so they can be safely reactivated.
The current database contract lives in migrations and table classes, not copied SQL in this guide. See Data architecture and Migration lifecycle.
Frontend architecture
CakePHP templates render semantic HTML. Bootstrap 5 provides the visual
component layer, Stimulus owns local behavior, and Turbo Frames update selected
regions. Turbo Drive is disabled. Controllers register through
window.Controllers and must remove global listeners in disconnect().
Vite builds JavaScript and CSS and writes the manifest consumed by
ViteHelper. UI changes must preserve WCAG 2.2 Level AA, including keyboard
operation, visible focus, useful labels, live announcements, contrast, and
non-color-only cues.
Cross-cutting rules
- Store timestamps in UTC and convert only at input/output boundaries.
- Use public IDs where routes or external references require non-sequential identifiers; public IDs are not an authorization mechanism.
- Route tenant caches, mail, documents, jobs, and settings through their scoped services.
- Do not log secrets, customer data, raw request bodies, or high-cardinality identifiers in platform telemetry.
- Preserve unrelated plugin and tenant behavior with targeted tests before a full verification run.