Files
Ekklesia/docs/architecture/platform-architecture.md
T
ekklesiaandClaude Sonnet 5 d3c6bd73d3 fix: enforce API request limits
Add a single platform-owned request-body policy (hono/body-limit) with
small, named limits for Better Auth and Core's grant-mutation endpoint, and
a new required maxBodyBytes field on the executable module route contract so
every module route makes a conscious no-body-or-bounded-body choice, capped
at a platform ceiling. Every oversized request gets one sanitized 413
contract and never reaches business logic, authorization, or a handler.
Also configure finite Node header/request/keep-alive timeouts, including the
non-obvious connectionsCheckingInterval needed for them to take effect
promptly, so a slow-header or slow-body connection cannot hold a socket
indefinitely.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-26 12:29:24 -05:00

137 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Platform architecture
Ekklesia is a long-lived platform from which churches can compose many applications and user
experiences. It begins as a modular monolith so contracts can stabilize before distribution is
introduced.
## Dependency model
The arrows below mean "depends on." Presentation and composition depend on stable lower-level
contracts; infrastructure never learns individual feature names.
```text
Surface Builder configuration -> Surfaces -> public capability contracts
^
|
HTTP/API and client contracts
|
Feature Modules -> Module SDK / platform contracts <- Core implementations
```
The four ownership layers are:
1. **Core** orchestrates generic platform infrastructure: startup, identity, permissions,
configuration, database connectivity, storage, events, jobs, module discovery, and lifecycle.
2. **Modules** own backend domain behavior, data schemas, resources, actions, routes, events,
permissions, and jobs.
3. **Surfaces** own every presentation concern: components, renderers, layout, navigation,
accessibility, interaction, branding, and client state.
4. **Surface Builder** stores validated declarative composition. It selects capabilities and
surface-owned renderers but contains neither executable code nor module business logic.
Modules do not own UI. For example, Calendar data, actions, and permissions will live in
`modules/calendar`; Calendar renderers for a dashboard or public website will live in those
surfaces.
## Stable seams
- Modules use public SDK ports and injected Core services, never undocumented Core internals.
- Modules do not import another module's schema or private code. Integrations use public service
contracts, capabilities, events, or APIs.
- Surfaces consume API/client and capability contracts, never feature-module packages.
- Core discovers feature modules generically. It contains no feature-specific imports or
conditional branches.
- Deployable apps are thin composition roots and do not become domain-logic owners.
- Generated bootstrap artifacts may connect discovered entry points at the application edge;
they do not become manually maintained Core registries.
## Security invariants
- Authorization is enforced server-side.
- Secrets are isolated from public configuration and logs.
- Authentication tokens, private form content, prayer requests, and pastoral information are not
logged.
- Sensitive actions are designed for auditability without copying sensitive content into audit
records.
- Private objects use signed access and namespaced storage paths.
- Every network service starts from least-privilege and local-only development defaults.
## Current implementation status
Implemented now: the Phase 1 repository and infrastructure foundation plus public module contracts,
static versioned manifests, independent capability versions and dependency ranges, automatic
filesystem discovery, compiled server loading, runtime-definition verification, staged capability
registration, dependency-ordered startup and reverse shutdown, rollback, and a backend-only module
template. Optional compiled schema entrypoints now contribute validated, namespaced module-owned
Drizzle tables to a deterministic aggregate without connecting to a database. Optional module-owned
Drizzle migration directories now produce an atomic dependency-ordered offline inventory with
validated metadata, explicit destructive-change declarations, and exact SHA-256 checksums. Core can
now compare that inventory with its fixed PostgreSQL history table, produce a read-only plan, and
apply the complete pending batch under a session advisory lock and one transaction. Destructive
migrations require an exact externally supplied approval bound to module, identity, and checksum.
Hermetic integration coverage now exercises those contracts through the production adapter against
the pinned PostgreSQL container, including real DDL rollback and advisory-lock concurrency. The
Core-owned Doctor composes those existing services into a deterministic terminal readiness report:
static module, schema, and migration validation always run when their prerequisites succeed, while
PostgreSQL history planning runs only when configured. Schema inspection evaluates compiled schema
definition entrypoints, which the module contract requires to be side-effect-free; Doctor never
imports or starts module server entrypoints. It does not apply migrations or modify PostgreSQL, and
it honestly skips object storage because no production Core storage service exists yet.
Phase 4A added the Core identity foundation. Core owns a Better Auth-backed identity schema
(`ekklesia_core_user`, `ekklesia_core_session`, `ekklesia_core_account`,
`ekklesia_core_verification`) reachable only through Ekklesia-owned contracts and an explicit
Core-owned Better Auth construction boundary; modules never import Better Auth or receive a raw
database client. That schema ships as a first-class Core-owned migration source that reuses the
exact migration format, checksum rule, planner, and transactional runner Phase 3 already
established, ordered before module migrations in the same aggregate. See
[ADR 0010](decisions/0010-core-identity-and-better-auth-adoption.md).
Phase 4B adds identity HTTP transport: a minimal Hono-based `apps/api` composition root mounts
Better Auth at `/api/auth/*` and exposes one Ekklesia-owned route, `GET /api/v1/principal`, behind
Core's own Fetch-compatible identity runtime (`@ekklesia/core/identity`) — Core still does not
depend on Hono. Public email/password registration is the intended eventual product policy — every
account receives identical lowest-privilege baseline status, and signup order never grants
elevation — but is disabled by default as of Phase 5D pass 1, below. See
[ADR 0011](decisions/0011-identity-http-transport-and-public-enrollment.md) and
[the API guide](../development/api.md), and
[the identity development guide](../development/identity.md) for the underlying schema and
contracts. It is now possible to publicly register, sign in, resolve a principal, and sign out.
Phase 4C is complete: Core's authorization foundation — a schema, evaluator, and store for
platform owners, explicit grants, and relationship-derived grants, plus an immutable permission
registry — and an explicit, single-use, auditable platform-owner bootstrap command, plus reusable
Hono enforcement and an `/api/v1/grants` HTTP API in `apps/api`. See
[ADR 0013](decisions/0013-core-authorization-and-owner-bootstrap.md) and
[the authorization guide](../development/authorization.md).
Phase 5A wired module-declared permissions into that same authorization system — a version-3
manifest, one composed platform permission registry spanning Core's own declarations and every
installed module's, enforced through the identical middleware Core's own routes use. Phase 5B let a
module mount its own framework-neutral HTTP routes in production, under
`/api/v1/modules/<module-id>`, enforced and response-sanitized exactly like Core's own routes. This
branch completes Phase 5C: typed module database access (`context.services.database`) for a
module's lifecycle hooks and authorized HTTP handlers, over the platform's one existing PostgreSQL
pool. See [ADR 0014](decisions/0014-module-permission-declarations-and-platform-registry.md),
[ADR 0015](decisions/0015-static-module-http-routing-and-runtime-lifecycle.md),
[ADR 0016](decisions/0016-module-database-access.md), and the repository root
[README](../../README.md#current-status) for the complete, current status of every phase.
Phase 5D pass 1 of 5 hardens the public request perimeter against an independent security audit of
the Phase 5C checkpoint: a server-owned client-address resolution boundary (direct mode by default,
explicit trusted-proxy mode otherwise) replaces trusting a caller-controlled forwarded header for
the authentication rate limiter; public sign-up is disabled by default behind an explicit typed
opt-in; every public route — Better Auth, Core's grant mutation, and every module route via a new
required `maxBodyBytes` field on the executable route contract — has an explicit, enforced
request-body ceiling with one sanitized `413` contract; and the Node HTTP server has finite
header/request timeouts. See
[ADR 0017](decisions/0017-public-request-perimeter-hardening.md). This pass does not add
distributed rate limiting or durable signup-abuse control, and Phase 5D itself is not complete —
passes 2–5 and a final independent re-audit remain before Phase 6 may begin.
The project continues finishing the module platform's backend foundations before Phase 6 Surface
work begins. Target architecture not yet implemented includes production approval-provider
composition, physical storage-bucket provisioning, jobs/workflow automation, richer public resource
and action contracts, a Dashboard, a Builder, and reference feature modules; none of these are
scheduled by this document.