Skip to content

ADR-025: Package Versioning Strategy ​

Status ​

Accepted -- 2026-02-20; revised -- 2026-10-08 (Phase 2 cancelled; see Current Decision).

Update (2026-10-08): The Hybrid transition planned below as Phase 2 was never implemented and is cancelled: every @connectum/* package keeps one shared version. The historical body below -- dependency graph, phase plan, migration checklist, and rated alternatives -- is preserved unchanged. Its package inventory is historical: the text names 12 and 13 packages (a partial update on 2026-03-22), @connectum/testing has been published since 2026-03-05 (private: false), and the monorepo has 15 packages today; see Packages.

Current Decision (2026-10-08) ​

Keep every @connectum/* package in the same Changesets fixed group for stable releases as well as prereleases. Do not introduce a separate independently versioned extension group. This adopts the option assessed below as Alternative 1: Stay Fixed Forever.

Connectum consists of modular packages in one pnpm monorepo. Users install the capabilities they need, while maintainers release those packages together through Changesets. Optional installation does not imply independent versioning. At the date of this decision every package manifest carries version 1.2.0 and 1.3.0 is under review.

A shared version gives maintainers and consumers one release identifier for the framework, including its optional modules, broker adapters, CLI, catalog generator, and testing packages. The cost is that an unchanged package can receive a new version when another member changes. This is an accepted release-policy trade-off.

The versioning configuration is unchanged from Phase 1 below ("fixed": [["@connectum/*"]] in .changeset/config.json). Contributors describe the packages they changed in a changeset; Changesets determines the shared version for the group; maintainers do not split package versions by hand.

Scope ​

The group includes all framework modules and tools, including @connectum/testing and @connectum/test-fixtures. @connectum/testing is a public package (private: false) with main and /parity exports; it is not an unpublished exception to this policy. Package inventories and dependency layers belong in Packages and Architecture. A dependency layer does not define a version group.

Release identity ​

The release workflow reads the common version from packages/core/package.json for the framework's v<version> GitHub release tag. That file is a representative of the shared group, not an independently versioned core release. The release.yml change planned under Phase 2 below is not needed.

Release notes should distinguish package-specific behavior changes from version alignment. A new shared version does not mean that every package's implementation changed. Consumers still review the migration instructions and dependency ranges for the modules they install.

Consequences of the 2026-10-08 decision ​

  • One framework version identifies the complete release set.
  • Consumers do not need a separately maintained matrix of independent Connectum module versions for each release.
  • New @connectum/* packages belong to the same existing fixed group.
  • The existing Changesets configuration and release workflow already implement the chosen policy; this revision requires no runtime or release-tool changes.
  • Packages can receive version bumps for group alignment without code changes, and changelogs may include dependency or version-alignment entries -- the cost that the original Phase 2 plan tried to avoid.

Context ​

Connectum is a universal gRPC/ConnectRPC framework composed of 12 @connectum/* packages in a pnpm monorepo, managed by changesets for versioning and publishing. Packages are compiled via tsup before publication (compile-before-publish, see ADR-001).

Current State ​

All 13 packages use a fixed versioning strategy ("fixed": [["@connectum/*"]]), meaning every package always shares the same version number. The project is currently in a pre-release (rc) phase with all packages at 1.0.0-rc.4.

Current .changeset/config.json:

json
{
  "fixed": [["@connectum/*"]],
  "linked": [],
  "updateInternalDependencies": "patch"
}

Package Dependency Graph ​

The 13 packages are organized in 3 layers with varying degrees of coupling:

The Problem ​

With the current fixed strategy, all 13 packages are bumped together. This means:

  1. A patch fix in @connectum/auth bumps @connectum/otel even though otel has zero @connectum dependencies
  2. A new feature in @connectum/cli bumps @connectum/core even though core is unchanged
  3. CHANGELOGs contain "bumped for consistency" entries with no actual changes
  4. Semver loses its semantic meaning for independent packages

While this is acceptable during the pre-release phase (simplicity > precision), it becomes semantically misleading after 1.0.0 stable when consumers rely on version numbers to understand what changed.

Industry Precedents ​

FrameworkStrategyRationale
AngularFixedTight integration across all packages
NestJSFixed (core) + Independent (modules)Core packages move together, optional modules versioned independently
tRPCFixedServer/client API contract must match
EffectIndependentPluggable extensions with peer dependencies
ConnectumHybrid (this ADR)Core group tightly coupled, auth/otel/cli are optional extensions

Connectum is closest to the NestJS model: a tightly coupled core group with independently versioned optional extensions.

Original Decision (2026-02-20) ​

Adopt a two-phase versioning strategy: Fixed during pre-release, Hybrid after 1.0.0 stable.

Phase 1: Pre-release (rc) through 1.0.0 -- Stay Fixed ​

During the pre-release phase, keep the current fixed strategy unchanged.

Rationale:

  • Changing versioning strategy during rc is unnecessary risk
  • "Connectum v1.0.0" as a single unified version is a clearer launch message
  • CI/CD (release.yml) works without modifications
  • Risk = 0, focus remains on stability and API surface finalization

Configuration (no change):

json
{
  "fixed": [["@connectum/*"]],
  "linked": [],
  "updateInternalDependencies": "patch"
}

Phase 2: After 1.0.0 stable -- Transition to Hybrid ​

Phase 2 and its migration checklist were cancelled by the 2026-10-08 decision above; they are retained as the original plan.

After the 1.0.0 stable release, split packages into two groups:

Core Group (Fixed) ​

Packages that are tightly coupled through createServer() and are almost always used together by consumers:

PackageReason for inclusion
@connectum/coreServer factory, lifecycle, plugin system
@connectum/interceptorscreateDefaultInterceptors(), depends on core (a peer dependency since 1.3)
@connectum/healthcheckProtocol plugin for createServer({ protocols: [...] })
@connectum/reflectionProtocol plugin for createServer({ protocols: [...] })

These 4 packages form the standard Connectum setup. A typical service uses all four:

typescript
import { createServer } from '@connectum/core';
import { createDefaultInterceptors } from '@connectum/interceptors';
import { Healthcheck } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';

const server = createServer({
  services: [routes],
  interceptors: createDefaultInterceptors(),
  protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
});

Independent Group ​

Packages that are optional extensions with distinct lifecycles:

PackageReason for independence
@connectum/authOptional authentication/authorization; consumers may handle auth at the gateway level
@connectum/eventsOptional EventBus; consumers may use external message brokers directly
@connectum/events-natsNATS adapter for EventBus; depends on @connectum/events
@connectum/events-kafkaKafka adapter for EventBus; depends on @connectum/events
@connectum/events-redisRedis Streams adapter for EventBus; depends on @connectum/events
@connectum/otelZero @connectum dependencies; pure OpenTelemetry instrumentation
@connectum/cliDeveloper tool; only devDependencies; not a runtime concern
@connectum/testingPrivate package; devDependency only; never published

Hybrid Configuration ​

json
{
  "fixed": [
    [
      "@connectum/core",
      "@connectum/interceptors",
      "@connectum/healthcheck",
      "@connectum/reflection"
    ]
  ],
  "linked": [],
  "updateInternalDependencies": "minor",
  "access": "public",
  "baseBranch": "main"
}

Key changes from Phase 1:

  • fixed narrows from ["@connectum/*"] to the 4 core packages
  • updateInternalDependencies changes from "patch" to "minor" -- when a core group package bumps minor, internal dependents (like auth depending on core) get their dependency range updated to "minor" rather than "patch", providing wider compatibility

Version Grouping Diagram ​

CI/CD Changes (release.yml) ​

The current release.yml extracts the version for GitHub Release tagging using .[0].version (first published package). After the transition, it must target the core group explicitly:

Before (Phase 1):

bash
# Any package works since all versions are identical
VERSION=$(jq -r '.[0].version' < published.json)

After (Phase 2):

bash
# Target core specifically for tag and GitHub Release
VERSION=$(jq -r '.[] | select(.name == "@connectum/core") | .version' < published.json)

The GitHub Release and git tag (v1.2.0) are tied to the core group version, since the core group represents the framework version. Independent packages (auth, otel, cli) do not create separate GitHub Releases -- their changes are tracked in per-package CHANGELOGs.

Migration Checklist ​

When transitioning from Phase 1 to Phase 2 (after 1.0.0 stable release):

  • [ ] Update .changeset/config.json with the hybrid configuration above
  • [ ] Update release.yml to use select(.name == "@connectum/core") for version extraction
  • [ ] Verify all 13 packages are at 1.0.0 before the switch
  • [ ] Update docs/en/contributing/development-setup.md with versioning guidelines
  • [ ] Add a note to each independent package README explaining its versioning policy
  • [ ] Create a compatibility matrix in documentation (core group version vs auth/otel/cli versions)
  • [ ] Run a dry-run changeset version to confirm the hybrid config works: pnpm changeset version --snapshot test
  • [ ] Update this ADR status to reflect Phase 2 activation

Consequences ​

The consequences and alternatives below assess the original two-phase plan. The 2026-10-08 decision adopts Alternative 1.

Positive ​

  1. Phase 1: Zero risk -- proven strategy for the rc phase, no changes needed
  2. Phase 2: Semantic accuracy -- core group stays synchronized while auth/otel/cli bump only on real changes
  3. Cleaner CHANGELOGs -- no more "bumped for consistency" entries in independent packages
  4. Smaller consumer updates -- consumers of @connectum/otel don't need to update when only @connectum/auth changed
  5. Industry-aligned -- follows the NestJS model, familiar to Node.js ecosystem developers
  6. Flexible evolution -- independent packages can iterate faster without blocking or being blocked by core releases

Negative ​

  1. CI/CD update required -- release.yml needs modification during Phase 2 transition
  2. Compatibility awareness -- consumers must understand that core group = one version, while auth/otel/cli are independent; needs clear documentation
  3. Version matrix complexity -- documentation must maintain a compatibility matrix (e.g., "[email protected] requires core@^1.0.0")
  4. Changeset discipline -- contributors must understand which packages belong to the fixed group and which are independent
  5. Potential confusion -- @connectum/[email protected] alongside @connectum/[email protected] may look inconsistent to users unfamiliar with the strategy

Alternatives Considered ​

Alternative 1: Stay Fixed Forever ​

Rating: 6/10

Keep "fixed": [["@connectum/*"]] permanently.

Pros:

  • Simplest mental model: "Connectum is version X"
  • Zero compatibility questions -- all packages always match
  • Minimal CI/CD complexity

Cons:

  • Semantically inaccurate: @connectum/otel gets bumped when @connectum/auth changes, despite zero coupling
  • Noisy CHANGELOGs with empty "bumped for consistency" entries
  • Consumers update packages unnecessarily

Alternative 2: Full Independent ​

Rating: 4/10

Every package versioned independently with no fixed groups.

Pros:

  • Maximum semantic precision
  • Each package bumps only on its own changes
  • True semver for every package

Cons:

  • "Is [email protected] + [email protected] + [email protected] compatible?" -- consumers cannot easily answer this
  • Core group packages are tightly coupled; independent versioning creates a false impression of independence
  • Significantly more complex release process and documentation

Alternative 3: Linked (instead of Fixed for core group) ​

Rating: 5/10

Use "linked" instead of "fixed" for the core group. With linked, packages share the same bump type but only packages with actual changesets are bumped.

Pros:

  • More precise than fixed -- unchanged packages don't bump
  • Same bump type ensures version alignment intent

Cons:

  • In practice, for 4 tightly coupled packages, nearly every change touches multiple packages
  • Creates version drift within the core group (e.g., [email protected] + [email protected]), confusing consumers
  • For Connectum's core group, the result is approximately the same as fixed but with added complexity

References ​

  1. ADR-001: Native TypeScript Migration -- compile-before-publish strategy
  2. ADR-003: Package Decomposition -- package structure and layer rules
  3. Changesets documentation: Fixed packages
  4. Changesets documentation: Linked packages
  5. NestJS versioning strategy -- similar hybrid approach
  6. Framework versioning configuration
  7. Release workflow

Changelog ​

DateAuthorChange
2026-02-20Software ArchitectInitial ADR: two-phase versioning strategy (Fixed for rc, Hybrid after 1.0.0)
2026-10-08Maintainer revisionCancel the Phase 2 Hybrid transition; keep all packages at one shared version (Current Decision). Historical body retained; ## Decision renamed to ## Original Decision (2026-02-20).