Files
Ekklesia/docs/architecture/decisions/0053-standalone-module-development-packages.md
ekklesiaandClaude Opus 5.5 486356a01e
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
docs: ADR 0054, Ekklesia's module-development packages are publicly readable before launch
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]>
2026-10-08 10:24:17 -05:00

14 KiB

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 — only where this ADR describes the package phase before launch as private, or requires authenticated package reads.
  • 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.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:

# .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-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);
  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.