From 47e64c6f39a6c994679496e1b114a1ef17e2dd60 Mon Sep 17 00:00:00 2001 From: ekklesia Date: Wed, 7 Oct 2026 20:46:32 -0500 Subject: [PATCH] docs: propose ADR 0053, standalone module builds from Ekklesia packages over the npm registry protocol 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 --- ...-standalone-module-development-packages.md | 253 ++++++++++++++++++ docs/architecture/decisions/README.md | 1 + 2 files changed, 254 insertions(+) create mode 100644 docs/architecture/decisions/0053-standalone-module-development-packages.md diff --git a/docs/architecture/decisions/0053-standalone-module-development-packages.md b/docs/architecture/decisions/0053-standalone-module-development-packages.md new file mode 100644 index 0000000..521b496 --- /dev/null +++ b/docs/architecture/decisions/0053-standalone-module-development-packages.md @@ -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 + `//-/-.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:/// +``` + +```ini +# User or CI configuration, never committed: the credential, from the environment. +///:_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. diff --git a/docs/architecture/decisions/README.md b/docs/architecture/decisions/README.md index 9c3dcad..cff44a2 100644 --- a/docs/architecture/decisions/README.md +++ b/docs/architecture/decisions/README.md @@ -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 - [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 +- [0053: A module builds outside the monorepo, from Ekklesia packages published over the npm registry protocol](0053-standalone-module-development-packages.md) — Proposed