Files
Ekklesia/README.md
T
ekklesiaandClaude Opus 5 44ba259f58
CI / verify (pull_request) Successful in 2m31s
CI / infrastructure-smoke (pull_request) Successful in 15s
CI / migration-smoke (pull_request) Successful in 49s
feat: establish the module product contract
Ekklesia's module model has been backend-only since ADR 0001: a module
owns domain behavior and data, and every presentation concern belongs to
a surface. That prevents a module author — the person who knows the
domain — from shipping the screen that makes their module useful, and it
guarantees a module and its own UI drift apart. It also pushes toward two
codebases for one domain the moment a module needs to run standalone.

ADR 0021 replaces that rule. A module is an independently versioned
product that owns its domain behavior, data, permissions, routes,
migrations, and its own UI, hostable inside Ekklesia and — from the same
implementation — by a later standalone host. Core keeps the containing
application shell and every platform policy: authentication,
authorization enforcement, platform context, top-level routing,
navigation, global theme, error boundaries, and trusted-origin/CORS. The
module graph stays flat: modules are peers, genuine module and capability
dependencies remain the only relationships, and Dashboard folders are
presentation configuration an administrator can reorganize, never module
architecture.

This pass implements the static contract only. `manifestVersion` moves to
4 and `entrypoints` gains one optional field, `ui`. Version 3's
`entrypoints` is strict and rejects `ui`, so this is an incompatible
shape change and version 3 is now rejected outright rather than
reinterpreted, exactly as version 1 was by version 2 and version 2 by
version 3. Every field version 4 adds is optional, so upgrading a
backend-only manifest is a one-character edit.

Discovery resolves and validates `ui` under the same rules as `server`
and `schema` — package-relative, inside the package before and after
symlink resolution, a real file, compiled ESM `.js`/`.mjs`, so source
TypeScript is rejected rather than assumed to be compiled later.
Discovery stays inert: nothing imports, evaluates, mounts, or renders a
UI entrypoint, and a test proves a throwing artifact still discovers
successfully. This follows `entrypoints.jobs`, declared and resolved
since ADR 0006 with no runtime consumer.

The name is `ui`, not `dashboard`, because a module's UI must be hostable
by Ekklesia's shell and by a standalone host, so the contract cannot be
named after one of them; and not `surface`, because `surfaces/*` is a
separate ownership layer. No widget, route, icon, folder, menu, layout,
or standalone-metadata field is added, and tests assert the manifest
still rejects every unknown entrypoint key and offers no hierarchy field.

Deliberately unchanged: `scripts/check-boundaries.ts` still enforces
`modules-do-not-own-ui`. Relaxing that guard depends entirely on the UI
authoring model a later pass chooses, and weakening a working protection
before that decision exists would trade real coverage for nothing. A
module may therefore declare `ui` today only for a framework-neutral
compiled artifact, and the docs say so rather than implying otherwise.
Restart-based discovery, explicit operator-invoked migrations, and the
trusted compiled-module model are untouched — a module that ships UI is
exactly as trusted as one that does not.

No Dashboard shell, UI runtime, renderer, framework choice, navigation
registry, grouping system, standalone host, installer, hot-loading, or
feature module is added by this pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-21 20:20:41 -05:00

379 lines
26 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.
# Ekklesia
Ekklesia is an open, modular church technology platform. Each module is an independently versioned
product that owns its domain behavior, data, and its own UI, composed into dashboards, websites,
mobile apps, kiosks, displays, and other surfaces.
## Current status
Phase 3 is complete, and Phase 4A (Core identity foundation), Phase 4B (identity HTTP transport),
Phase 4C (Core authorization), Phase 5A (module authorization integration), Phase 5B (module HTTP
transport), and Phase 5C (module database access) have landed. **Phase 5D (security hardening
against an independent security audit) is complete: all five passes have landed and the final
independent whole-repository re-audit has closed, under the current trusted-contributor,
not-yet-deployed threat model — and Phase 6 is unblocked and underway.** The operational
requirements that remain are prerequisites for two specific *future* activities — executing
untrusted CI contributions, and a public production launch — and neither gates Phase 6; see the
pass 5 closure below and
[Security model and deployment trust boundaries](docs/development/security.md). The repository includes
public module contracts, independently versioned capabilities, static module manifests,
deterministic automatic discovery, compiled server loading, a staged capability registry,
dependency-ordered lifecycle with rollback, module-owned Drizzle table contracts, deterministic
schema aggregation, and a cloneable backend-only module template. Migration artifacts have
module-owned Drizzle contracts, deterministic offline discovery, explicit destructive-change
declarations, and exact checksums. Core provides read-only applied-history planning and a locked,
whole-batch transactional PostgreSQL runner with exact external destructive approval matching. A
hermetic live PostgreSQL suite proves history, rollback, statement breakpoints, advisory-lock
concurrency and release, exact destructive approvals, and sanitization against the production
adapter. A Core-owned `pnpm doctor` dashboard combines the existing static discovery and read-only
migration-planning services into one concise readiness report.
Phase 4A added Core-owned identity foundations: a Core-owned Better Auth database schema
(`ekklesia_core_user`, `ekklesia_core_session`, `ekklesia_core_account`,
`ekklesia_core_verification`), a first-class Core-owned migration source that coexists with
module-owned migrations under the exact same migration system, minimal opaque-identity contracts
in `@ekklesia/contracts`, and an explicit Better Auth construction boundary in Core. See
[Identity](docs/development/identity.md) and
[ADR 0010](docs/architecture/decisions/0010-core-identity-and-better-auth-adoption.md).
Phase 4B adds identity HTTP transport: a minimal Hono-based `apps/api` composition root
(`pnpm api:start`) mounts Better Auth at `/api/auth/*` and exposes `GET /api/v1/principal` behind
Core's own Fetch-compatible identity runtime — Core still does not depend on Hono, and modules
still cannot reach any of this. 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 now **disabled by default, and cannot be enabled in
production at all** until durable abuse controls exist (Phase 5D pass 1, below). See
[API](docs/development/api.md) and
[ADR 0011](docs/architecture/decisions/0011-identity-http-transport-and-public-enrollment.md).
**No email verification, password recovery, or Dashboard exists yet** — that, along with
production approval-provider composition, jobs, physical storage provisioning, surfaces, and
feature modules, remains later vertical slices. Public registration is not yet ready for a real
public launch.
Phase 4C adds Core's authorization foundation on top of identity: a schema, evaluator, and store
for platform owners, explicit grants, and relationship-derived grants; a smallest-possible
immutable permission registry; an explicit, single-use, concurrency-safe platform-owner bootstrap
command (`pnpm authorization:bootstrap-owner`, never HTTP-exposed, never inferred from signup
order); and, in `apps/api`, reusable Hono authorization enforcement plus a narrowly scoped
`/api/v1/grants` HTTP API for managing *explicit* grants only, requiring an exact trusted `Origin`
on every mutation. Roles/groups/teams/tenants, an owner-management HTTP endpoint, and service
accounts/API tokens remain later work. See [Authorization](docs/development/authorization.md) and
[ADR 0013](docs/architecture/decisions/0013-core-authorization-and-owner-bootstrap.md).
Phase 5A wires module-declared permissions into that same authorization system: the module manifest
(version 3 at the time, version 4 today) declares permissions as complete
`{id, baseline, description}` declarations (never a bare string), and `apps/api` discovers every installed module and composes one immutable platform
permission registry — Core's own declarations plus every module's — before constructing its
database pool, Better Auth instance, or HTTP server. A module's declared permission is then
enforceable and grantable through the exact same `requirePermission` middleware and
`/api/v1/grants` HTTP API Core's own permissions already use, with `core.*` reserved and never
claimable by a module. `pnpm doctor` and `pnpm modules:validate` both validate permission
composition. Installing, removing, or editing a module currently requires an API restart; removing
one makes its permission unavailable to new checks and new grants without deleting or revoking any
historical grant or audit row. See
[Authorization](docs/development/authorization.md#module-permission-composition),
[Module development](docs/module-development/README.md), and
[ADR 0014](docs/architecture/decisions/0014-module-permission-declarations-and-platform-registry.md).
Phase 5B mounts a module's own HTTP routes in production. A module registers framework-neutral
Fetch (`Request`/`Response`) route handlers during its existing `register` hook; Core collects them
into an immutable snapshot alongside its capabilities, and `apps/api` discovers modules exactly
once, starts the module runtime from that same discovered inventory, and mounts its published
routes under `/api/v1/modules/<module-id>` — after every module has registered and started
successfully, so no request can reach a module before startup completes. Every route is enforced
through the identical `requirePermission` middleware and composed permission registry Core's own
routes use (a module can never supply its own baseline), state-changing methods require a trusted
`Origin`, and a handler failure or non-`Response` return becomes a fixed, sanitized `500` — never a
raw error, stack, path, or permission id. Relationship-derived grant creation remains later work.
See [API](docs/development/api.md#module-http-routes),
[Module development](docs/module-development/README.md#http-routes), and
[ADR 0014](docs/architecture/decisions/0014-module-permission-declarations-and-platform-registry.md#deferred).
Phase 5C gives a module's own lifecycle hooks and HTTP handlers typed access to their own
PostgreSQL tables. `context.services.database` — a restricted, typed `ModuleDatabase` view over
Drizzle's `select`/`selectDistinct`/`selectDistinctOn`/`insert`/`update`/`delete`/`execute`/
`transaction`, never a raw `pg` client, pool, or connection string — is available to `start`,
`stop`, and every authorized route handler, and reuses the one PostgreSQL pool `apps/api` already
owns; registration remains declarative and database-free. Modules continue to own and import their
own Drizzle schemas and generated migrations, applied explicitly and never automatically at
startup. See [API](docs/development/api.md#module-database-access),
[Module development](docs/module-development/README.md#database-access), and
[ADR 0016](docs/architecture/decisions/0016-module-database-access.md).
Phase 5D pass 1 of 5 hardens the public request perimeter against an independent security audit
of the Phase 5C checkpoint, without adding a Surface, a product module, or any new capability.
Better Auth's authentication rate limiter now resolves client identity from a server-owned
client-address boundary (`core/src/identity/client-address.ts`, wired in at the Node/API transport
boundary) — direct mode trusts only the real socket peer by default, and an explicit trusted-proxy
mode is required before any forwarded header is ever consulted, closing a rotating-header
rate-limit bypass. Public sign-up is now **disabled by default, and a production configuration that
tries to enable it fails closed** (`PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION`, reported by `pnpm
doctor` as a blocking failure) — the durable abuse-control prerequisites do not exist yet, so only
development/test composition may enable it. Every public body-consuming route — Better Auth,
Core's grant-mutation endpoint, and every module route (via a new, required `maxBodyBytes` field on
the executable route contract) — now has an explicit, platform-enforced byte ceiling with one
sanitized `413` contract. The Node HTTP server now has finite header/request timeouts, whose real
transport-level failures are observed and logged in sanitized form. See
[API](docs/development/api.md), [Module development](docs/module-development/README.md#http-routes),
and [ADR 0017](docs/architecture/decisions/0017-public-request-perimeter-hardening.md). **This pass
does not add distributed rate limiting, durable public-signup abuse control, or any deployment/TLS
guarantee, and it did not by itself complete Phase 5D** — at the close of this pass, passes 2–5
(identity/database transport hardening, authorization integrity, module/runtime/migration
correctness, and deployment/supply-chain closure plus a final independent re-audit) were still
outstanding, and Phase 6 (Surface work) was blocked until they landed.
Phase 5D pass 2 of 5 corrects identity session resolution and enforces a database transport policy.
A request carrying no cookies, or only unrelated cookies, now performs **no** identity database
work at all; only a request presenting the configured session cookie that Better Auth resolved to
`null` is disambiguated, by a real read of the session relation and the exact columns the adapter
depends on (under `where false`, so no row is ever read) rather than the previous `select 1`. A
missing table, dropped column, revoked privilege, unreachable database, or query timeout therefore
produces a sanitized `503`, never a silent anonymous downgrade. The identity base URL and every
trusted origin must now be exactly one canonical origin, and production rejects the full
loopback/unspecified address space rather than three literal spellings. **Production PostgreSQL
connections now require TLS with certificate *and* hostname verification** — from the API, both
migration commands, the bootstrap CLI, and `pnpm doctor` alike — with no encryption-only mode, no
`rejectUnauthorized: false` escape hatch, no plaintext exception (not even for a loopback
database), and an explicit refusal of a `DATABASE_URL` that tries to carry its own TLS parameters,
which the pinned driver would otherwise let silently override the policy. See
[Identity](docs/development/identity.md), [API](docs/development/api.md#database-transport-tls), and
[ADR 0018](docs/architecture/decisions/0018-identity-session-resolution-and-database-transport.md).
**This pass adds no secret store, no certificate rotation, no client-certificate authentication, and
no distributed rate limiting; TLS protects the connection, not the exposure, so the network boundary
remains the deployment's own responsibility. This pass did not by itself complete Phase 5D** — at
its close, passes 3–5 (authorization integrity, module/runtime/migration correctness, and
deployment/supply-chain closure plus a final independent re-audit) were still outstanding.
Phase 5D pass 3 of 5 preserves authorization security history and makes explicit-grant management
unambiguous and bounded. Authorization **audit history now survives user deletion**: the audit
subject is a retained identifier snapshot with no cascading foreign key, so deleting a user still
removes their live grants and platform-owner row but no longer destroys the record of what they had
been granted and by whom. Platform-owner bootstrap's single use moved to a dedicated singleton with
no user foreign key — previously deleting the bootstrapped owner's user row silently reopened the
one operation that establishes a platform's first administrator; the operational consequence is that
**deleting the last owner does not reactivate bootstrap**, and existing installations are backfilled
as consumed by the new migration. Two equivalent grants can no longer both be effective: creation
conflicts with a sanitized `409`, serialized under real PostgreSQL concurrency, and one revocation
now clears every unrevoked equivalent with a per-row audit entry, so revoking a grant can no longer
report success while a duplicate keeps authorizing the user. Grant ids must be canonical UUIDs and
expirations one exact future-only format, both decided before any database call; the generic HTTP
surface creates unscoped grants only; and listing is bounded keyset pagination with an opaque,
unsigned position cursor. One forward Core migration (`0002_authorization_integrity`,
`{"mode":"none"}`) must be applied before the new code starts. See
[Authorization](docs/development/authorization.md) and
[ADR 0019](docs/architecture/decisions/0019-durable-authorization-history-and-grant-management.md).
**This pass adds no roles, teams, tenants, service accounts, scope resolver, user-deletion endpoint,
or owner-recovery workflow; audit append-only behavior remains an application convention, not a
database or cryptographic guarantee. This pass did not by itself complete Phase 5D** — at its close,
passes 4 (module/runtime/migration correctness) and 5 (deployment/supply chain plus a final
independent re-audit) were still outstanding, and Phase 6 was blocked until they landed.
Phase 5D pass 4 of 5 hardens the module runtime and the migration commands. **Two routes of one
module that could both match the same request path are now rejected at registration**
(`MODULE_ROUTE_OVERLAP`) instead of being resolved by declaration order — with different permissions
on `/events/:id` and `/events/new`, registration order alone previously decided which permission
guarded `/events/new`. Decoded route parameters are documented as untrusted, module-owned input that
handlers must validate against their own domain grammar; no generic validator or route field was
added. A module whose `start` hook fails is now rolled back through **its own** `stop` hook first,
so partially acquired resources are released, and every stop/rollback hook runs under a fixed
10-second deadline while HTTP shutdown drains for at most 30 seconds before force-closing
connections through Node's supported public APIs — shutdown is finite at every stage, reports a
forced drain or a hook failure as an unsuccessful shutdown, and always still reaches pool closure.
`migrations:plan`/`migrations:apply` now connect with fixed 10-second connection, query, and
`statement_timeout` defaults that a `DATABASE_URL` cannot weaken (the pinned driver lets a
connection string override explicit options — verified), and each migration's SQL is sent to
PostgreSQL **intact, as one parsed batch** inside the existing transaction rather than split on the
literal `--> statement-breakpoint` bytes, which corrupted any migration containing those bytes in a
string literal, dollar-quoted body, quoted identifier, or comment. See
[Module development](docs/module-development/README.md#http-routes),
[API](docs/development/api.md#graceful-shutdown), and
[ADR 0020](docs/architecture/decisions/0020-module-runtime-and-migration-execution-hardening.md).
**Deadlines bound how long the platform waits; they do not cancel or sandbox trusted in-process
module code, and closing a socket does not prove a handler promise ended. No custom SQL parser
exists. Modules remain trusted compiled backend code with no process, role, schema, or tenant
isolation. This pass did not by itself complete Phase 5D** — at its close, pass 5
(deployment/supply chain plus an independent whole-repository re-audit) was still outstanding, and
Phase 6 was blocked until it landed.
Phase 5D pass 5 of 5 closes the phase with an independent whole-repository re-audit and the
repository-local half of deployment and supply-chain hardening. The re-audit swept every layer —
Core, contracts, `apps/api`, scripts, the CI workflow, and documentation — and its repository-local
findings are resolved here: every dependency advisory is remediated (both `pnpm audit` and
`pnpm audit --prod` report no known vulnerabilities), every CI action reference is pinned to a full
immutable commit SHA, installs run `--frozen-lockfile`, no workflow references a repository or
instance secret, every checkout step sets `persist-credentials: false`, a checksum-verified secret
scan found no committed credential in the history, and one canonical
[security document](docs/development/security.md) now states the trusted-module model, the
undeployed state, and the launch checklist in a single place. **CI is accepted as a
trusted-code-only boundary**: the self-hosted Gitea runner executes only code controlled and
reviewed by trusted repository collaborators, and unknown or untrusted pull requests must not
execute on it automatically. What remains is operational and cannot be proven from this repository —
upgrading Gitea past 1.24.2 (which *ignores* the workflow's `permissions`, `concurrency`, and
`timeout-minutes` declarations), clamping job-token authority server-side, enforcing timeout and
teardown in a trusted provisioner/reaper, and proving disposable runner isolation. Those are
prerequisites for **executing untrusted contributions**, alongside the deployment checklist that
gates a **public launch**; moving future public contribution workflows to a service with isolated
hosted runners is an acceptable alternative. **Neither set of requirements blocks Phase 6** — Surface
development proceeds under the current trusted-contributor, not-yet-deployed model. Runner topology
and job-token authority remain unverified and are not described as isolated. See
[Security model and deployment trust boundaries](docs/development/security.md).
Phase 6A establishes the **module product contract** and nothing else yet. Ekklesia's modules are no
longer backend-only capability packages: a module is an independently versioned *product* that owns
its domain behavior, data, permissions, routes, migrations, and — new in this pass — **its own UI**.
Core keeps the containing application shell and every platform policy (authentication, authorization
enforcement, platform context, top-level routing, navigation, global theme, error boundaries,
trusted-origin/CORS). Modules stay **flat peers**: `video`, `livestream`, and `announcements` are
independent first-class modules with no parent/child relationship, and a Dashboard folder such as
`Media` is presentation configuration an administrator can reorganize, never module architecture and
never a dependency. The long-term goal is one module implementation hosted by Ekklesia *and*, later,
by a standalone application — never two codebases for one domain. See
[ADR 0021](docs/architecture/decisions/0021-modules-as-hostable-products.md).
**What this pass actually ships is the static contract only.** `manifestVersion: 4` adds one
optional field, `entrypoints.ui`, a package-relative path to a compiled ESM artifact validated
during discovery exactly like `server` and `schema` — inside the package, real file, no source
TypeScript. Discovery stays inert: **nothing imports, mounts, or renders that entrypoint**, exactly
like the long-declared `entrypoints.jobs`. Upgrading a backend-only version 3 manifest is a
one-character edit; version 3 is rejected outright rather than reinterpreted, as version 2 was
before it. **The Dashboard shell, a UI runtime or renderer, any UI framework choice, navigation
registration, Dashboard grouping, a standalone host, a module installer or marketplace, hot-loading,
and every feature module do not exist.** `pnpm boundaries` also still enforces
`modules-do-not-own-ui`, so module UI cannot yet be authored with a UI framework — choosing that
authoring model is the next pass's first question. Restart-based discovery, explicit
operator-invoked migrations, and the trusted compiled-module model are unchanged.
**Security model.** Installed modules are trusted compiled backend code that runs arbitrary
JavaScript in the API process and shares its PostgreSQL role — table prefixes, manifests, permission
declarations, typed facades, and frozen contexts are correctness boundaries, not a sandbox. Ekklesia
is also **not currently deployed**: there is no production instance, reverse proxy, public listener,
or deployment automation, so the origin/proxy/throttling properties a public launch requires are
unproven launch-blockers rather than existing guarantees. Read
[Security model and deployment trust boundaries](docs/development/security.md) before installing a
module or exposing the API.
## Quick start
Prerequisites: Node.js 24 LTS, pnpm 11.12, and Docker with Compose.
```bash
pnpm install --frozen-lockfile
cp .env.example .env # keep its NODE_ENV=development: see below
pnpm infrastructure:up
pnpm modules:validate
pnpm migrations:validate
pnpm identity:schema:validate
pnpm doctor
pnpm migrations:plan
pnpm test:migrations:integration
pnpm verify
```
`pnpm doctor` builds the workspace first, then reports the supported Node.js runtime, installed
modules and dependency order, schema and migration inventories, the effective PostgreSQL transport
(TLS) policy, optional read-only PostgreSQL planning, and the current object-storage integration
state. `.env.example` sets `NODE_ENV=development` deliberately: anything else, including an unset
value, is validated as production, and production requires certificate-verified PostgreSQL TLS and
rejects loopback/`http` identity origins — so the plaintext loopback database this repository's own
development infrastructure provides is only usable with it set.
Doctor never applies migrations, creates a history table, starts module servers, or performs
storage operations. Schema discovery does import
compiled schema-definition entrypoints; those entrypoints are contractually side-effect-free.
With no installed modules and no `DATABASE_URL`, the current repository produces a report shaped
like this:
```text
Ekklesia Doctor
Platform
✓ Runtime version supported
Active Node.js satisfies the declared supported range.
Modules
✓ Module discovery complete
0 modules installed
✓ Dependency graph valid
Dependency order is empty.
✓ Permission registry composed
1 permission registered
Data definitions
✓ Schemas validated
0 modules · 0 tables
✓ Migrations validated
3 migrations · checksums valid
core:0000_identity_core_tables · f8f2086ef06f
core:0001_authorization_core_tables · 830e1fbc26d0
core:0002_authorization_integrity · 634294731180
PostgreSQL
✓ Database TLS verified
TLS required with certificate and hostname verification.
○ Connection check skipped
DATABASE_URL is not configured.
○ Migration plan skipped
A validated migration inventory and configured PostgreSQL connection are required.
Identity
○ Identity configuration skipped
Core identity schema defines 4 tables (ekklesia_core_account, ekklesia_core_session, ekklesia_core_user, ekklesia_core_verification).
Identity runtime configuration (signing key, base URL, trusted origins) is not set.
Object storage
○ Check unavailable
Core storage integration has not been implemented yet.
Result: READY WITH WARNINGS
```
Checks have stable pass, warning, failure, or skipped states. Warnings and skipped optional checks
produce `READY WITH WARNINGS` and exit zero; any failure produces `NOT READY` and exit one. The
dashboard is human-readable local guidance, not a replacement for automated CI evidence.
`pnpm migrations:plan` uses `DATABASE_URL` from the uncommitted `.env` and is read-only.
`pnpm migrations:apply` uses the same validated inventory but mutates PostgreSQL under Core's
advisory lock and one transaction. Review the plan before applying. Destructive migrations are
blocked by default and require an exact externally supplied approval; there is no force flag.
The runner has both deterministic unit coverage through a narrow database port and a separate live
PostgreSQL integration suite. The live command creates and destroys disposable Docker
infrastructure; it remains separate from the Docker-free `pnpm verify` gate.
`pnpm identity:schema:validate` regenerates Better Auth's canonical schema with the pinned local
CLI into a temporary location and structurally compares it against the checked-in
`core/src/identity/schema.ts`, then removes the temporary output. It requires no `DATABASE_URL`
and is part of `pnpm verify`. See [Identity](docs/development/identity.md) for the full upgrade
workflow.
`pnpm api:start` builds the workspace, then starts the identity HTTP transport
(`GET /api/v1/principal`, Better Auth mounted at `/api/auth/*`) against an already-migrated
`DATABASE_URL` — it never applies migrations itself. See [API](docs/development/api.md) for
required configuration, routes, and a full request/response transcript.
Run `pnpm infrastructure:smoke` for an isolated end-to-end PostgreSQL and authenticated S3 check.
The development services bind only to loopback:
- PostgreSQL: `127.0.0.1:5432`
- S3-compatible object storage: `127.0.0.1:8333`
The values in `.env.example` are development-only and must never be reused in a deployed
environment.
## Architecture
- [Platform architecture](docs/architecture/platform-architecture.md)
- [Repository boundaries](docs/architecture/repository-boundaries.md)
- [Repository workflow](docs/development/repository-workflow.md)
- [Testing](docs/development/testing.md)
- [Identity](docs/development/identity.md)
- [API](docs/development/api.md)
- [Authorization](docs/development/authorization.md)
- [Module development](docs/module-development/README.md)
- [Architecture decisions](docs/architecture/decisions/README.md)
No open-source license has been selected yet. A license should be added only after the project
owner makes that decision.