CI / verify (pull_request) Successful in 1m4s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m46s
CI / package-release (pull_request) Successful in 33s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 48s
CI / deploy-smoke (split) (pull_request) Successful in 1m27s
CI / package-publication (pull_request) Successful in 43s
ADR 0053 was accepted assuming a private package phase before launch. The project owner has decided otherwise: Ekklesia is not secret, its source is already public, and only publication must be restricted. ADR 0054 supersedes ADR 0053 only where it describes that phase as private or requires authenticated package reads. Packages may be read anonymously, which is neither a launch nor an API stability promise beyond pre-1.0 SemVer; publication stays authenticated and restricted; published versions stay immutable; the River_of_Glory_Church Gitea namespace is an acceptable host; a public launch remains a separate decision. Every other ADR 0053 decision is unchanged. ADR 0053 gets only a cross-reference line. The ADR index, the release and module-development guides, the README status, and the publish workflow's comment now point at ADR 0054. Co-Authored-By: Claude Opus 5.5 <[email protected]>
256 lines
14 KiB
Markdown
256 lines
14 KiB
Markdown
# 0053: A module builds outside the monorepo, from Ekklesia packages published over the npm registry protocol
|
|
|
|
- Status: Accepted
|
|
- Date: 2026-10-07
|
|
- Superseded in part by: [0054](0054-public-package-reads-before-launch.md) — only where this ADR
|
|
describes the package phase before launch as private, or requires authenticated package reads.
|
|
- Extends: [0049](0049-module-package-v1.md). This is the step ADR 0049 deferred: "publishing the
|
|
Ekklesia SDK packages for module builds outside a platform workspace". A registry is used only to
|
|
build a module. Installing one stays registry-free: an installed package is self-contained, as ADR
|
|
0049 decided.
|
|
- Related: [ADR 0002](0002-pnpm-workspace-and-typescript.md),
|
|
[ADR 0006](0006-versioned-capabilities-and-compiled-module-runtime.md),
|
|
[ADR 0021](0021-modules-as-hostable-products.md),
|
|
[ADR 0023](0023-module-ui-build-pipeline.md),
|
|
[ADR 0039](0039-shared-design-tokens-and-elements.md),
|
|
[ADR 0047](0047-module-ci-harness.md),
|
|
[ADR 0048](0048-generic-image-and-installed-module-storage.md),
|
|
[ADR 0050](0050-module-database-abi-v1.md),
|
|
[ADR 0051](0051-module-package-license-material.md),
|
|
[ADR 0052](0052-reviewed-license-supplements.md)
|
|
|
|
## Context
|
|
|
|
Modules are independently versioned products in their own repositories (ADR 0021). Yet no module can
|
|
be built outside Ekklesia's monorepo today. Signage's continuous integration clones the platform at
|
|
a pinned commit, places Signage under `modules/signage`, and installs and builds inside that
|
|
workspace, because its Ekklesia dependencies exist only as `workspace:*` packages.
|
|
|
|
An investigation of the current packages found the following.
|
|
|
|
- **One coordinated version.** Every `@ekklesia/*` package, and the platform, is version 0.1.2. The
|
|
release process already bumps them together. The platform reports its Module SDK version, which
|
|
manifests' `compatibility.moduleSdk` ranges are checked against (ADR 0006), by reading
|
|
`@ekklesia/module-sdk`'s own package version.
|
|
- **The tarballs already stand alone.** `pnpm pack` rewrites `workspace:*` ranges to exact versions,
|
|
and each tarball carries only its built `dist`, declarations, and `LICENSE`. But every package's
|
|
`prepack` runs a command at the monorepo root, and `@ekklesia/typescript-config` is marked
|
|
`private`.
|
|
- **The SDK exposes everything.** `@ekklesia/module-sdk` re-exports the whole `@ekklesia/contracts`
|
|
barrel. That includes contracts no module needs, such as authorization and identity, and the
|
|
legacy Drizzle `ModuleDatabase` type. `@ekklesia/contracts` depends on `drizzle-orm` for that type,
|
|
so every module install pulls Drizzle in.
|
|
- Neither the module template nor Signage imports `@ekklesia/contracts` directly.
|
|
- **Frameworks are declared twice.** `@ekklesia/ui` lists `react`, and `@ekklesia/module-sdk` lists
|
|
`drizzle-orm`, as both a dependency and a peer dependency. That invites a second copy of a runtime
|
|
that must be single within one module bundle.
|
|
- **The canonical packager assumes the monorepo**, in two ways:
|
|
- it finds the module's Node.js range only through an enclosing `pnpm-workspace.yaml`;
|
|
- it decides that an Ekklesia package is the platform's own, not third-party, because its real
|
|
path does not contain `node_modules`.
|
|
|
|
An `@ekklesia/*` package installed from a registry would therefore be treated as third-party.
|
|
Packaging would omit it from `incorporates` and skip the Module SDK compatibility check.
|
|
- **The registry and its hosts.**
|
|
- The current private host is a Gitea 1.27.2 server. Its npm registry is enabled for the
|
|
organization, and that endpoint and the repositories are currently readable anonymously.
|
|
- pnpm leaves a tarball URL out of its lockfile only when the URL has the canonical
|
|
`<registry>/<name>/-/<name>-<version>.tgz` form. Gitea advertises another form, so lockfiles
|
|
produced against Gitea name the Gitea host.
|
|
- The integrity hashes in those lockfiles are independent of the host.
|
|
|
|
## Decision
|
|
|
|
### The npm registry protocol, not a provider
|
|
|
|
Ekklesia's module-development packages are distributed over the **standard npm registry protocol**,
|
|
and modules install them with ordinary pnpm or npm commands.
|
|
|
|
- **No provider is part of the architecture.** Not Gitea, GitHub, npmjs, or any other. A module
|
|
repository depends on the protocol and on a scope-to-registry mapping in normal npm/pnpm
|
|
configuration.
|
|
- **Gitea may host the private phase.** It is a host for this phase, not a permanent decision.
|
|
- **No host-neutral lockfile or Ekklesia proxy is required.** Moving to another registry may need a
|
|
one-time change of a module's registry configuration and a regeneration of its lockfile. The
|
|
integrity hashes keep verifying the package content across such a move.
|
|
|
|
A module repository's registry configuration is, for example:
|
|
|
|
```ini
|
|
# .npmrc, committed: where the @ekklesia scope lives, and nothing secret.
|
|
@ekklesia:registry=https://<registry endpoint>/
|
|
```
|
|
|
|
```ini
|
|
# User or CI configuration, never committed: the credential, from the environment.
|
|
//<registry endpoint>/:_authToken=${EKKLESIA_PACKAGES_TOKEN}
|
|
```
|
|
|
|
### The public package set
|
|
|
|
Only packages a module author needs are published. Nothing is published merely because it exists in
|
|
the workspace.
|
|
|
|
| Category | Packages |
|
|
| --- | --- |
|
|
| Author API | `@ekklesia/module-sdk`, `@ekklesia/module-database`, `@ekklesia/module-ui-sdk`, `@ekklesia/design-tokens` |
|
|
| Tooling | `@ekklesia/module-ui-build`, `@ekklesia/module-pack`, `@ekklesia/typescript-config` |
|
|
| Pre-1.0 component API | `@ekklesia/ui`: publishable, explicitly unstable before 1.0 |
|
|
| Implementation dependency only | `@ekklesia/contracts`: may be published because the packages above depend on it, but it is **not** a supported direct module-author API |
|
|
| Never published | `@ekklesia/core`, the applications, `@ekklesia/module-ui-host`, `@ekklesia/module-template`, and every other platform internal |
|
|
|
|
### A curated SDK surface
|
|
|
|
Before the first external publication, the author-facing SDK is narrowed.
|
|
|
|
- **A curated API.** `@ekklesia/module-sdk` exposes what a module author needs, and no longer
|
|
re-exports the complete `@ekklesia/contracts` barrel.
|
|
- **No forced Drizzle.** The stable SDK root does not make Drizzle a dependency of modules that do
|
|
not use it.
|
|
- **No legacy database contract.** The legacy Drizzle database contract (ADR 0016's facade type) is
|
|
not part of the standalone author-facing surface. Core still serves the legacy facade for its
|
|
deprecation window (ADR 0050). This decision only keeps its type out of what new standalone
|
|
modules build against.
|
|
- **Drizzle support lives in one place.** Drizzle-specific helpers belong to the
|
|
`@ekklesia/module-database` Drizzle adapter (ADR 0050).
|
|
|
|
### Dependency rules
|
|
|
|
These rules apply to the published packages:
|
|
|
|
- **React and ReactDOM** are peer dependencies of the public UI packages, plus development
|
|
dependencies for those packages' own builds and tests. This does not change ADR 0023: a module UI
|
|
artifact still bundles React. The peer only ensures there is one React in that bundle, the
|
|
module's own.
|
|
- **Drizzle** is an optional peer dependency only where the Drizzle adapter needs it.
|
|
- **Real implementation dependencies,** at run time or build time, remain normal dependencies.
|
|
|
|
These rules apply to a module repository:
|
|
|
|
- **Runtime and module-code packages** go in `dependencies`: the Author API packages, `@ekklesia/ui`,
|
|
React, and the module's ORM if it uses one.
|
|
- **Build tooling and configuration** go in `devDependencies`: `@ekklesia/module-ui-build`,
|
|
`@ekklesia/module-pack`, `@ekklesia/typescript-config`, and type packages.
|
|
|
|
### Versioning
|
|
|
|
- **One coordinated version during 0.x.** Every public Ekklesia module-development package uses one
|
|
release version, equal to the platform release version. The version the platform reports for
|
|
`moduleSdk` compatibility is therefore the version of the packages a module was built with.
|
|
- **No mixed trains.** Within a published train, Ekklesia-owned dependencies name the coordinated,
|
|
tested version, never a version from another train.
|
|
- **Ranges in source, exact artifacts in lockfiles.** A module may declare compatible ranges in its
|
|
source `package.json`; its lockfile pins exact artifacts with their integrity hashes.
|
|
- **`@ekklesia/module-pack` is pinned exactly.** A module repository pins it in its source
|
|
`package.json`, not only in its lockfile, because its version affects the package it produces.
|
|
- **Independent package versions are deferred.**
|
|
|
|
### Standalone packaging
|
|
|
|
- **No monorepo checkout.** A standalone module builds and packages from its own repository and its
|
|
installed dependencies.
|
|
- **Its own Node.js range.** The module declares its own Node.js compatibility, preferably in
|
|
`engines.node`, and the canonical packager reads it from there or from an explicit option. It
|
|
never requires an enclosing `pnpm-workspace.yaml`.
|
|
- **Ekklesia packages recognized by identity.** The canonical packager recognizes Ekklesia-owned
|
|
installed packages by explicit package identity and metadata, never by workspace path. So
|
|
`incorporates` and the Module SDK compatibility check (ADR 0049) work the same whether a package
|
|
came from the monorepo or from a registry. Ekklesia packages are never reported as third-party
|
|
software (ADR 0051).
|
|
- **No monorepo-root commands at install time.** Publishing workflows for the public packages run
|
|
their build in the platform's release pipeline. Nothing in a published package runs a
|
|
monorepo-root command when a consumer installs or packs it.
|
|
|
|
### Supply chain
|
|
|
|
- **Immutable versions.** A version is never republished with other content, and never withdrawn to
|
|
be reused.
|
|
- **Lockfiles.** pnpm lockfile integrity is part of the reproducibility model: modules install with a
|
|
frozen lockfile.
|
|
- **Release CI proves the tarballs before publication.** It must:
|
|
1. build and pack the public package set;
|
|
2. inspect every tarball: only the intended contents, a `LICENSE`, and no `workspace:` range;
|
|
3. install those tarballs into a clean standalone module fixture, outside any monorepo;
|
|
4. prove there that the fixture builds, packages with the canonical packager, and verifies.
|
|
|
|
Only then are the versions published.
|
|
- **Signing and provenance are deferred.**
|
|
|
|
### Registry access and the private phase
|
|
|
|
- **No secrets in module repositories.** Registry configuration for the scope is normal npm/pnpm
|
|
configuration.
|
|
- **Credentials from the environment.** Read and publish credentials come from environment or user
|
|
configuration: a read credential in module CI where reads need one, and a publish credential only
|
|
in Ekklesia's release pipeline.
|
|
- **No permanent provider.**
|
|
- **Verify access before the first publication.** Check the chosen registry's actual private and
|
|
read-access behaviour first. Ekklesia's packages must not be published unintentionally to a
|
|
registry that serves them anonymously while Ekklesia is not public.
|
|
|
|
### The release workflow
|
|
|
|
During 0.x, public package releases form a **coordinated SDK train** associated with platform
|
|
releases:
|
|
|
|
1. build;
|
|
2. pack;
|
|
3. standalone tarball smoke (see [Supply chain](#supply-chain));
|
|
4. publish immutable versions;
|
|
5. downstream module CI installs and tests those versions.
|
|
|
|
### Signage
|
|
|
|
The intended result is a normal standalone Signage repository:
|
|
- versioned `@ekklesia/*` dependencies;
|
|
- its own lockfile;
|
|
- continuous integration that installs, builds, packages, and verifies on its own.
|
|
|
|
That CI must not clone Ekklesia or place Signage under `modules/`. The migration itself is a separate
|
|
change.
|
|
|
|
## Consequences
|
|
|
|
- A module repository is an ordinary pnpm project. Clone it, install, build, and package it with the
|
|
canonical packager, with no Ekklesia checkout.
|
|
- Module authors see a smaller, deliberate API, and the platform can change its internal contracts
|
|
without breaking them.
|
|
- One coordinated version keeps compatibility simple: SDK version X is what platform version X
|
|
reports and accepts. The cost is republishing unchanged packages with each platform release.
|
|
- A registry move costs each module a configuration change and a lockfile regeneration, but no
|
|
source change. Package content stays verifiable by integrity hash throughout.
|
|
- Before the first publication, the SDK surface must be curated and the packager's two monorepo
|
|
assumptions removed. Both are breaking for code that relied on them.
|
|
- ADR 0047, still proposed, assembles a module into the platform workspace to test it. Before it is
|
|
accepted it must instead consume published packages and canonical module packages, consistent with
|
|
this decision and with ADR 0048.
|
|
|
|
## Alternatives
|
|
|
|
- **Git URL, tag, or commit dependencies.** Rejected: they tie modules to one Git host, need a build
|
|
on every install because `dist` is not committed, and cannot express version ranges.
|
|
- **Release-asset tarball URLs.** Rejected: each dependency becomes a host-specific URL, and ranges
|
|
cannot be resolved.
|
|
- **OCI artifacts for packages.** Rejected: npm and pnpm cannot install from them. (A harness image
|
|
is a different matter; see ADR 0047.)
|
|
- **Vendoring Ekklesia packages into each module repository.** Rejected: the copies drift, updates
|
|
are manual, and there would be no single canonical SDK.
|
|
- **Requiring host-neutral lockfiles, or an Ekklesia registry proxy that rewrites tarball URLs.** Not
|
|
required: a one-time lockfile regeneration on a registry move is an acceptable cost, and integrity
|
|
hashes already keep content verifiable. Ekklesia may still choose a stable endpoint later.
|
|
- **Publishing `@ekklesia/contracts` as a direct author API.** Rejected: it holds platform-wide
|
|
contracts. Modules get a curated surface through the SDK packages.
|
|
- **Externalizing React from module UI bundles to share one runtime.** Rejected again: ADR 0023's
|
|
reasoning stands, and peers only prevent duplicates within one bundle.
|
|
- **Independent versions per package now.** Deferred: during 0.x a coordinated train is simpler for
|
|
module authors and matches how the platform reports its SDK version.
|
|
|
|
## Deferred
|
|
|
|
- Provisioning a registry, and its credentials.
|
|
- Public publication on npmjs or any other public registry.
|
|
- Package signing and provenance.
|
|
- Independent package versioning.
|
|
- Dashboard and UI redesign.
|
|
- The Signage migration itself.
|