Produces the exact public package tarballs of a platform release, validates them, and proves a standalone module consumes those artifacts end to end. Nothing is published and no credential is configured. - scripts/package-release.ts (pnpm packages:release) is the canonical release builder: from a clean commit it rebuilds the nine public packages from clean output, packs each, inspects every tarball, checks the release set and coordinated versions, and only then writes ekklesia-packages-<version>/ with the tarballs, SHA256SUMS, and a provider-neutral manifest (ekklesia-package-train.json: version, revision, and each package's file, size, SHA-256, npm integrity). - Packing uses a pack-only pnpm beforePacking hook (passed with --config.pnpmfile, never the workspace's install settings). pnpm inserts rewritten workspace: dependencies in async completion order, so two checkouts of one commit packed module-pack's package.json with keys in a different order; the hook restores the source key order (and the inspection enforces it). It also drops devDependencies, which named private packages outside the train. - scripts/test-package-release.sh (pnpm test:package-release, the CI package-release gate) builds the train twice, the second time from a separate worktree, requires byte-identical release directories, then runs the standalone consumer proof on the release: manifest checked, offline install from the tarballs only, lockfile integrity equal to the manifest's, frozen-lockfile reinstall, no Drizzle, one React, contracts only transitive, every @ekklesia package and module-pack at the release version, skipLibCheck: false, UI build, pack, and Core verification. - docs/development/releases.md documents the release order, the train, the artifacts, and the reproducibility findings; registry publication remains unimplemented. - ADR 0053 is Accepted: everything it decides that does not need a registry is now proven, and registry provisioning is on its own Deferred list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
14 KiB
0053: A module builds outside the monorepo, from Ekklesia packages published over the npm registry protocol
- Status: Accepted
- Date: 2026-10-07
- Extends: 0049. 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, ADR 0006, ADR 0021, ADR 0023, ADR 0039, ADR 0047, ADR 0048, ADR 0050, ADR 0051, ADR 0052
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.moduleSdkranges are checked against (ADR 0006), by reading@ekklesia/module-sdk's own package version. -
The tarballs already stand alone.
pnpm packrewritesworkspace:*ranges to exact versions, and each tarball carries only its builtdist, declarations, andLICENSE. But every package'sprepackruns a command at the monorepo root, and@ekklesia/typescript-configis markedprivate. -
The SDK exposes everything.
@ekklesia/module-sdkre-exports the whole@ekklesia/contractsbarrel. That includes contracts no module needs, such as authorization and identity, and the legacy DrizzleModuleDatabasetype.@ekklesia/contractsdepends ondrizzle-ormfor that type, so every module install pulls Drizzle in.- Neither the module template nor Signage imports
@ekklesia/contractsdirectly.
- Neither the module template nor Signage imports
-
Frameworks are declared twice.
@ekklesia/uilistsreact, and@ekklesia/module-sdklistsdrizzle-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 fromincorporatesand skip the Module SDK compatibility check. - it finds the module's Node.js range only through an enclosing
-
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>.tgzform. 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:
# .npmrc, committed: where the @ekklesia scope lives, and nothing secret.
@ekklesia:registry=https://<registry endpoint>/
# 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-sdkexposes what a module author needs, and no longer re-exports the complete@ekklesia/contractsbarrel. - 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-databaseDrizzle 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
moduleSdkcompatibility 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-packis pinned exactly. A module repository pins it in its sourcepackage.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 enclosingpnpm-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
incorporatesand 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:
- build and pack the public package set;
- inspect every tarball: only the intended contents, a
LICENSE, and noworkspace:range; - install those tarballs into a clean standalone module fixture, outside any monorepo;
- 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:
- build;
- pack;
- standalone tarball smoke (see Supply chain);
- publish immutable versions;
- 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
distis 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/contractsas 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.