docs: propose ADR 0053, standalone module builds from Ekklesia packages over the npm registry protocol
CI / verify (pull_request) Successful in 1m9s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / migration-smoke (pull_request) Successful in 1m56s
CI / image-build (pull_request) Successful in 24s
CI / deploy-smoke (same-origin) (pull_request) Successful in 43s
CI / deploy-smoke (split) (pull_request) Successful in 1m38s
CI / verify (pull_request) Successful in 1m9s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / migration-smoke (pull_request) Successful in 1m56s
CI / image-build (pull_request) Successful in 24s
CI / deploy-smoke (same-origin) (pull_request) Successful in 43s
CI / deploy-smoke (split) (pull_request) Successful in 1m38s
Defines the external module-development and package-distribution contract that ADR 0049 deferred. - Protocol, not provider: packages are distributed over the standard npm registry protocol. Gitea may host the private phase. Host-neutral lockfiles and an Ekklesia proxy are not required; a registry move may need one configuration change and a lockfile regeneration, while integrity hashes keep verifying content. - Public set: - Author API: module-sdk, module-database, module-ui-sdk, design-tokens. - Tooling: module-ui-build, module-pack, typescript-config. - ui as a pre-1.0 component API. - contracts only as a transitive implementation dependency. - Core, apps, module-ui-host, and module-template stay unpublished. - Curated SDK before first publication: no full contracts barrel, no forced Drizzle at the SDK root, and no legacy Drizzle database contract in the standalone surface. Drizzle support lives in module-database's adapter. - Dependency rules: React/ReactDOM are peers of the public UI packages (bundling per ADR 0023 is unchanged); Drizzle is an optional peer only where the adapter needs it; modules put runtime packages in dependencies and tooling/config in devDependencies. - Versioning: one coordinated 0.x release version equal to the platform version, with no mixed trains. Ranges go in source and exact artifacts in lockfiles; module-pack is pinned exactly; independent versions deferred. - Standalone packaging: no monorepo checkout. The module declares its own Node range (engines.node). The packager recognizes Ekklesia packages by identity and metadata, not path. No monorepo-root commands at install time. - Supply chain: immutable versions, frozen-lockfile integrity, and a pre-publication smoke that installs the packed tarballs into a clean standalone fixture and proves build, package, and verify. Signing deferred. - Private phase: no committed secrets; credentials come from the environment; verify actual read access before the first publication. Status Proposed. ADR 0047 (still proposed) must be revised against this. No code, registry, credential, or Signage changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5803757153
commit
47e64c6f39
@@ -0,0 +1,253 @@
|
|||||||
|
# 0053: A module builds outside the monorepo, from Ekklesia packages published over the npm registry protocol
|
||||||
|
|
||||||
|
- Status: Proposed
|
||||||
|
- Date: 2026-10-07
|
||||||
|
- 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.
|
||||||
@@ -61,3 +61,4 @@ ADRs capture durable choices that affect more than one package or phase.
|
|||||||
- [0050: Module Database ABI v1: modules and Core exchange SQL, never ORM objects](0050-module-database-abi-v1.md) — Accepted
|
- [0050: Module Database ABI v1: modules and Core exchange SQL, never ORM objects](0050-module-database-abi-v1.md) — Accepted
|
||||||
- [0051: Module packages declare their own license and carry its text](0051-module-package-license-material.md) — Accepted
|
- [0051: Module packages declare their own license and carry its text](0051-module-package-license-material.md) — Accepted
|
||||||
- [0052: Reviewed license supplements cover redistributed dependencies whose packages omit required material](0052-reviewed-license-supplements.md) — Proposed
|
- [0052: Reviewed license supplements cover redistributed dependencies whose packages omit required material](0052-reviewed-license-supplements.md) — Proposed
|
||||||
|
- [0053: A module builds outside the monorepo, from Ekklesia packages published over the npm registry protocol](0053-standalone-module-development-packages.md) — Proposed
|
||||||
|
|||||||
Reference in New Issue
Block a user