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/testinghas 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:
{
"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:
- A patch fix in
@connectum/authbumps@connectum/oteleven though otel has zero@connectumdependencies - A new feature in
@connectum/clibumps@connectum/coreeven though core is unchanged - CHANGELOGs contain "bumped for consistency" entries with no actual changes
- 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
| Framework | Strategy | Rationale |
|---|---|---|
| Angular | Fixed | Tight integration across all packages |
| NestJS | Fixed (core) + Independent (modules) | Core packages move together, optional modules versioned independently |
| tRPC | Fixed | Server/client API contract must match |
| Effect | Independent | Pluggable extensions with peer dependencies |
| Connectum | Hybrid (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):
{
"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:
| Package | Reason for inclusion |
|---|---|
@connectum/core | Server factory, lifecycle, plugin system |
@connectum/interceptors | createDefaultInterceptors(), depends on core (a peer dependency since 1.3) |
@connectum/healthcheck | Protocol plugin for createServer({ protocols: [...] }) |
@connectum/reflection | Protocol plugin for createServer({ protocols: [...] }) |
These 4 packages form the standard Connectum setup. A typical service uses all four:
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:
| Package | Reason for independence |
|---|---|
@connectum/auth | Optional authentication/authorization; consumers may handle auth at the gateway level |
@connectum/events | Optional EventBus; consumers may use external message brokers directly |
@connectum/events-nats | NATS adapter for EventBus; depends on @connectum/events |
@connectum/events-kafka | Kafka adapter for EventBus; depends on @connectum/events |
@connectum/events-redis | Redis Streams adapter for EventBus; depends on @connectum/events |
@connectum/otel | Zero @connectum dependencies; pure OpenTelemetry instrumentation |
@connectum/cli | Developer tool; only devDependencies; not a runtime concern |
@connectum/testing | Private package; devDependency only; never published |
Hybrid Configuration
{
"fixed": [
[
"@connectum/core",
"@connectum/interceptors",
"@connectum/healthcheck",
"@connectum/reflection"
]
],
"linked": [],
"updateInternalDependencies": "minor",
"access": "public",
"baseBranch": "main"
}Key changes from Phase 1:
fixednarrows from["@connectum/*"]to the 4 core packagesupdateInternalDependencieschanges from"patch"to"minor"-- when a core group package bumps minor, internal dependents (likeauthdepending oncore) 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):
# Any package works since all versions are identical
VERSION=$(jq -r '.[0].version' < published.json)After (Phase 2):
# 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.jsonwith the hybrid configuration above - [ ] Update
release.ymlto useselect(.name == "@connectum/core")for version extraction - [ ] Verify all 13 packages are at
1.0.0before the switch - [ ] Update
docs/en/contributing/development-setup.mdwith 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
- Phase 1: Zero risk -- proven strategy for the rc phase, no changes needed
- Phase 2: Semantic accuracy -- core group stays synchronized while auth/otel/cli bump only on real changes
- Cleaner CHANGELOGs -- no more "bumped for consistency" entries in independent packages
- Smaller consumer updates -- consumers of
@connectum/oteldon't need to update when only@connectum/authchanged - Industry-aligned -- follows the NestJS model, familiar to Node.js ecosystem developers
- Flexible evolution -- independent packages can iterate faster without blocking or being blocked by core releases
Negative
- CI/CD update required --
release.ymlneeds modification during Phase 2 transition - Compatibility awareness -- consumers must understand that core group = one version, while auth/otel/cli are independent; needs clear documentation
- Version matrix complexity -- documentation must maintain a compatibility matrix (e.g., "[email protected] requires core@^1.0.0")
- Changeset discipline -- contributors must understand which packages belong to the fixed group and which are independent
- 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/otelgets bumped when@connectum/authchanges, 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
- ADR-001: Native TypeScript Migration -- compile-before-publish strategy
- ADR-003: Package Decomposition -- package structure and layer rules
- Changesets documentation: Fixed packages
- Changesets documentation: Linked packages
- NestJS versioning strategy -- similar hybrid approach
- Framework versioning configuration
- Release workflow
Changelog
| Date | Author | Change |
|---|---|---|
| 2026-02-20 | Software Architect | Initial ADR: two-phase versioning strategy (Fixed for rc, Hybrid after 1.0.0) |
| 2026-10-08 | Maintainer revision | Cancel 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). |