Displays are platform-owned, but a screen could only start pairing on a
module's display page: every pairing request named a module, so an
installation with no module that uses displays could not set one up.
- /display/setup is the platform's own display page, served whenever
the deployment has device support, whatever is installed. It pairs a
platform display with no module. Once paired, it opens the module it
is set to show; with none set, it shows its name and waits, rechecking
so it stays alive and notices revocation. "setup" is now a reserved
module id.
- Migration core:0009_platform_display_pairing lets a pairing request
name no module. It relaxes module_id and permission_id and adds a
both-or-neither check. It is additive and destroys nothing.
- POST /api/v1/device-pairing/requests accepts {} for a platform
display. Approval accepts an optional moduleId, so the administrator
can choose an installed display module and the device gets that
module's declared permission. A module display's own request can't be
redirected.
- GET /api/v1/device-pairing/self lets a paired device describe itself:
its name and the module displays it may show. It renews the device's
cookie.
- GET /api/v1/devices/setup is replaced by
GET /api/v1/devices/display-modules: the modules a display can be set
to show.
- Settings -> Displays always offers the /display/setup address, Copy
setup link and Add display. Add display has an optional "Show on this
screen" choice, and each paired display says what it shows.
- /display/<module-id> is unchanged, so released Signage 0.1.5 works as
before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
10 KiB
CLAUDE.md
Guidance for Claude Code (and other coding agents) working in this repository. This file is intentionally short and durable — it points at authoritative sources instead of restating their contents, because restated details drift.
What this repository is
Ekklesia is a modular, self-hosted church platform. This repository is the platform — Core, the
API, the Dashboard and display shell, and the public @ekklesia/* module-development packages — built
as a strictly layered pnpm/TypeScript monorepo:
- Core (
core/) owns platform infrastructure: identity, database connectivity, migration execution, module discovery/lifecycle, devices, and other feature-agnostic services. Core never imports a feature module, a surface, or an app. - Modules are independently versioned product packages. First-party modules live in their own
repositories (Signage is
Zayin/Signage), build against the published@ekklesia/*packages (ADR 0053), and reach an installation as a Module Package (ADR 0049) installed into module storage outside the image (ADR 0048). No module source lives in this repository;templates/moduleis the example, andmodules/is only an optional, ignored local installed-modules directory. A module owns its data, migrations, and its own domain UI (ADR 0021), declares capabilities/permissions through@ekklesia/contractsand@ekklesia/module-sdk, and never imports Core, another module, or a surface. Manifest v8 permits independently optional primary and full-viewport display artifacts, authored in React against the role-neutral@ekklesia/module-ui-sdkABI; database access is the Module Database ABI v1 (ADR 0050). - Surfaces (
surfaces/*) own the containing application shell, navigation, and every presentation concern a module does not. The shell hosts module UI through the generic loader; it never rebuilds a module's domain screens. Surfaces consume public API/client contracts only.
These boundaries are enforced by pnpm boundaries (scripts/check-boundaries.ts), not just
documented. Read docs/architecture/repository-boundaries.md and
docs/architecture/platform-architecture.md before proposing any change that crosses a layer.
Product direction lives beside this repository
The current roadmap and product/interface design contract are ROADMAP.md and DESIGN.md in the
parent development directory (../ROADMAP.md and ../DESIGN.md from a checkout in the standard
~/development/ekklesia/ layout). They are deliberately not committed here; do not copy them into
this repository to make them available. They are authoritative for product direction: read the
relevant stages and sections before designing a change. If this repository's documentation disagrees
with them, flag the conflict, correct the repository-local documentation where appropriate, and never
silently follow the stale text. Architectural decisions still change only through ADRs (below).
Rules from those documents that every change here must respect:
- Ownership. Modules own their domain data, behaviour, and UI. Shared resources — displays and devices, accounts and identity, roles and authorization, branding, mail, push, storage providers, and module installation/lifecycle — belong to the platform, and modules consume them.
- Branding. Normal user-facing UI never hard-codes "Ekklesia"; product identity comes from installation branding (ADR 0046). Package names, diagnostics, logs, and developer tooling may. Existing hard-coded shell strings are known debt for Stage 1, not a pattern to copy.
- Visual direction. Simple, minimal, and calm; responsive for phone, tablet, and desktop; light,
dark, and system modes. Normal UI colours come only from the shared design tokens
(
@ekklesia/design-tokens, ADR 0039) — never a hard-coded colour. - No browser-native dialogs.
window.alert,window.confirm, andwindow.promptare prohibited in product UI. Destructive actions use in-product confirmation proportional to their impact. - Displays are platform-owned. A screen starts at the platform's own
/display/setup, whatever modules are installed, and is paired in Settings → Displays (ADR 0064, ADR 0065); the sidebar is navigation only. Signage's own display roster is still a transitional implementation that Stage 1 replaces with the platform's displays. Keep existing paired displays working across any change, and do not build a module installer, theme editor, or display features beyond what a task asks for early.
ADRs are authoritative
docs/architecture/decisions/ records durable architectural decisions. Accepted ADRs are
immutable except for status and cross-reference corrections — do not edit their content to fit a
new change. A changed decision gets a new ADR that supersedes the old one. Read the full ADR index
(docs/architecture/decisions/README.md) and every ADR relevant to the area you are touching
before designing a change; do not assume this file's summary of any decision is complete or
current.
Read before you design
At minimum, read the current versions of:
docs/architecture/decisions/README.mdand every ADR relevant to your changedocs/architecture/platform-architecture.mdanddocs/architecture/repository-boundaries.mdREADME.md(current platform status — what is and is not implemented)../ROADMAP.mdand../DESIGN.md(current product direction; see above)docs/development/repository-workflow.mdanddocs/development/testing.mdCONTRIBUTING.md
These files change as the platform evolves. Inspect the actual current repository state (source, tests, package.json scripts, CI workflow) before relying on anything in this file or in your own memory of a prior session — treat stale assumptions as a bug to catch, not a shortcut.
Working conventions
- The canonical repositories are on the church Gitea under the
Zayinorganization:ssh://[email protected]:2222/Zayin/Ekklesia.git(this one),Zayin/Signage.git, andZayin/Classroom_Desktop.git. A local clone may be behind the server:git fetch --prune --tagsand branch fromorigin/main, never from whatever is checked out. - One branch per PR, off an up-to-date
origin/main. Never commit or push directly tomain, never merge your own PR, and never change branch-protection settings. - Never create or push a release tag. Release tags belong to the human maintainer.
- A change that needs both a platform contract change and a module change is two PRs: Core first, on its own branch, with the compatibility boundary stated explicitly; then the module. Do not copy platform logic into a module to avoid making the correct Core change.
- Keep PRs focused on one bounded slice. Record major architectural changes in an ADR rather than burying them in an implementation.
- Add or update tests when you change a contract or architectural rule. New behavior needs new test coverage, not just a passing existing suite.
- Errors that reach a user, log, CLI, or
pnpm doctoroutput must be sanitized: never expose secrets, connection strings, raw SQL, raw driver/exception messages, stack traces, causes, or machine-specific paths. Match the sanitization pattern already used bycore/src/migrations/runner.tsandcore/src/diagnostics/doctor.tsrather than inventing a new one. - Never run a database migration automatically (on import, on startup, in a test that isn't explicitly a migration test, or as a side effect of an unrelated command). Migrations are always an explicit, operator-invoked action.
- Report what you find. A defect, flaky test, documentation/ADR conflict, or CI problem outside the
task at hand gets an issue in the repository it belongs to, as soon as you find it. Search for an
existing issue first; something fixed in the same PR goes in that PR's description instead. Be
descriptive: what you saw, the evidence (run and job links, commit, file and line), how to
reproduce it, the impact, and options, and never a secret. Label it
found-by-claude(an agent that cannot apply labels ends the body with a line saying so), leave it unassigned, and list what you filed when you report back.
Canonical verification commands
Run from the repository root; package.json is the source of truth if this list and the file
ever disagree:
pnpm verify— the full code-only gate (format, lint, boundaries, typecheck, tests, module and migration validation, identity schema drift check, build). Docker-free; never touches a live database.pnpm test:migrations:integration— live, disposable-PostgreSQL evidence: migration discovery, planning, and execution, plus theapps/apiHTTP transport (signup, sign-in, principal resolution, sign-out). Requires Docker. Run this for any change touching either area.pnpm test:package-release— builds the public@ekklesia/*package train, proves it reproducible, and proves a standalone module consumes exactly those tarballs. Publishes nothing. Run it for any change to the public packages or their release tooling.pnpm test:package-publication— publishes a release train to a disposable Gitea (Docker) and proves the publication tool. Never the real registry. The real publication is the manualPublish packagesworkflow only; never runscripts/publish-package-train.tsagainst the real registry without the user's explicit intent.pnpm doctor— read-only local readiness report. Never treat it as a substitute forpnpm verifyor the integration suite.pnpm migrations:plan/pnpm migrations:apply— read-only planning vs. mutating execution againstDATABASE_URL. Never runmigrations:applywithout the user's explicit intent.pnpm api:start— starts the identity HTTP transport against an already-migratedDATABASE_URL. It never applies migrations itself. Never run it without the user's explicit intent.
When you're unsure
If a task would require weakening an existing ADR's guarantee, introducing a second mechanism where one already exists (a second migration runner, a second discovery path, a second config system), or exposing something the boundary checker currently blocks, stop and surface the conflict instead of working around it.