Migration & Changelog
This page covers breaking changes and migration steps between Connectum releases.
BREAKING: Resilience interceptors are now opt-in in createDefaultInterceptors()
Applies to 1.0.0.
createDefaultInterceptors() now enables only the structural interceptors — errorHandler and validation. The resilience interceptors (timeout, bulkhead, circuitBreaker, retry) are opt-in: enable each one explicitly with true or an options object.
Why: no hidden behavioral logic. Implicitly enabled resilience caused a confirmed production incident — a server-side circuit breaker was tripped by expected business errors (such as invalid_argument) and started rejecting healthy traffic.
| Interceptor | Before | After |
|---|---|---|
| errorHandler | enabled | enabled (unchanged) |
| timeout | enabled (30s) | opt-in (30s when enabled) |
| bulkhead | enabled (10/10) | opt-in (10/10 when enabled) |
| circuitBreaker | enabled (5 failures) | opt-in (5 failures when enabled) |
| retry | enabled (3 attempts) | opt-in (3 attempts when enabled) |
| fallback | disabled | opt-in (unchanged) |
| validation | enabled | enabled (unchanged) |
| serializer | disabled | opt-in (unchanged) |
Migration:
// Before — timeout, bulkhead, circuitBreaker, retry were implicitly active
createDefaultInterceptors()
// After — to keep the previous behavior, enable them explicitly
createDefaultInterceptors({
timeout: true,
bulkhead: true,
circuitBreaker: true,
retry: true,
})
// After — if you only need errorHandler + validation (no change needed)
createDefaultInterceptors()Code that already passes explicit options (timeout: { duration: 10_000 }, retry: false, ...) keeps working: an options object or true means enabled, false means disabled.
Circuit breaker: error classification and placement
The circuit breaker now classifies errors. By default only infrastructure errors count as circuit failures: Unknown, DeadlineExceeded, Internal, Unavailable, DataLoss, ResourceExhausted (plus any non-ConnectError thrown value). Business codes (invalid_argument, not_found, ...) never open the breaker, and in half-open state they close it.
Customize via the new failurePredicate(error, defaultPredicate) option; the default classifier is exported as defaultFailurePredicate:
// Restore legacy behavior (every error trips the breaker)
createCircuitBreakerInterceptor({ failurePredicate: () => true });The circuit breaker is repositioned as an outbound/client-transport pattern. For server inbound protection prefer explicit timeout + bulkhead. See @connectum/interceptors for details.
Minimum Node.js raised to 22.13.0
Applies to 1.0.0.
Node.js 20 reached end-of-life on 2026-04-30 and no longer receives security updates. The minimum supported runtime for all @connectum/* packages is now Node.js 22.13.0 (the current LTS line).
| Aspect | Before | After |
|---|---|---|
| Minimum consumer Node.js | >= 20.0.0 | >= 22.13.0 |
| Development Node.js | >= 25.2.0 | >= 25.2.0 (unchanged) |
Migration: upgrade your runtime to Node.js 22.13.0 or later. Packages continue to ship compiled JavaScript, so no build-step or code changes are required.
BREAKING: @connectum/core validates streaming transport at startup
Applies to 1.0.0.
server.start() now rejects when a user-registered service defines a bidi-streaming method and the effective transport is plaintext HTTP/1.1 (the default http.createServer mode, allowHTTP1: true without TLS). Bidi streaming requires HTTP/2 — on HTTP/1.1 the first send hangs forever or the client gets an HTTP 505, so this fails fast instead of hanging in production. The error carries the stable code CONNECTUM_UNSUPPORTED_STREAMING_TRANSPORT and names the offending service.method. Protocol-contributed services (e.g. gRPC Reflection, whose ServerReflectionInfo is itself bidi) are excluded.
A new transportValidation option controls the behavior:
| Value | Behavior |
|---|---|
"error" (default) | reject start() for bidi on plaintext HTTP/1.1 |
"warn" | log a one-time warning and start anyway |
"off" | skip the check entirely |
Migration: the correct fix is to serve HTTP/2 — set allowHTTP1: false (h2c) or run behind TLS with ALPN. To preserve the previous (broken-at-runtime) behavior temporarily, set transportValidation: "warn" or "off". A TLS server with allowHTTP1: true is not a hard error but emits a one-time warning, since a client may still negotiate HTTP/1.1 over TLS. See the transport matrix for the full support grid.
BREAKING: PublishOptions.sync removed from @connectum/events
Applies to 1.0.0.
The sync flag on PublishOptions has been removed. It was a no-op: every adapter already confirms publishes per message (NATS PubAck, Kafka producer.send, Redis XADD, AMQP per-message broker ack with typed errors on nack/return/timeout). A resolved publish() already means the broker accepted the message — there was never a fire-and-forget mode to opt out of.
Migration: remove sync from any publish() calls. There is no behavior change — publish() already awaited broker confirmation. This is a compiling breaking change only (the field no longer exists on the type).
RC.9 to RC.10
New: Client-side auth interceptors in @connectum/auth
Two new factories were added for outbound ConnectRPC clients, simplifying service-to-service authentication without hand-rolled header wiring.
createClientBearerInterceptor()-- sets theAuthorizationheader with a static string or an async token provider (e.g., refreshable access tokens).createClientGatewayInterceptor()-- forwards the gateway shared secret and propagates auth context headers for internal service-to-service calls.
import { createClient } from '@connectrpc/connect';
import { createGrpcTransport } from '@connectrpc/connect-node';
import {
createClientBearerInterceptor,
createClientGatewayInterceptor,
} from '@connectum/auth';
const transport = createGrpcTransport({
baseUrl: 'https://upstream.internal',
interceptors: [
createClientBearerInterceptor({ token: async () => getAccessToken() }),
createClientGatewayInterceptor({ secret: process.env.GATEWAY_SECRET! }),
],
});
const client = createClient(UserService, transport);No breaking changes -- both factories are additive.
@connectum/events: auto-resolve publish topic from proto annotations
EventBus.publish() now automatically resolves the topic from the proto (connectum.events.v1.event).topic option when no explicit topic is passed. Existing code that already sets publishOptions.topic is unaffected; the explicit value still wins.
Priority order: explicit publishOptions.topic → declared topic from routes/publishes → schema.typeName (backward-compatible fallback).
Subscriber processes (those with routes) already have the topic lookup populated — no changes needed.
Publisher-only processes (no routes) must declare the event service in the new publishes option so the declared topic is resolved end-to-end:
import { OrderEventService } from '#gen/orders/v1/orders_pb.js';
const eventBus = createEventBus({ adapter, publishes: [OrderEventService] });
// publish() now uses the proto-declared topic automatically
await eventBus.publish(OrderCancelledSchema, data);Without publishes, publisher-only processes still fall back to schema.typeName — same as before 1.1.0.
@connectum/events: per-handler middleware configuration
Handlers registered via router.service() can now override the global middleware pipeline on a per-handler basis:
// Simple handler (uses global middleware)
onEvent: async (msg, ctx) => { /* ... */ }
// Config object with per-handler middleware override
onEvent: {
handler: async (msg, ctx) => { /* ... */ },
middleware: [retryMiddleware, metricsMiddleware],
}Both forms coexist -- existing simple-function handlers continue to work without changes.
@connectum/events: stricter handler input types (fix)
ServiceEventHandlers now derives handler input types from the concrete GenService record instead of a generic DescMethod array. This preserves concrete protobuf message types in handlers and eliminates the need for as unknown as T casts. If your handlers relied on such casts, you can now remove them -- no functional change is required.
No breaking changes in this release.
RC.8 to RC.9
@connectum/auth: AuthContext resilient to multiple module evaluations (bug fix)
The internal authContextStorage (an AsyncLocalStorage used by AuthContext) now uses globalThis + Symbol.for() to guarantee a single instance per process, even when the module is evaluated through multiple runtime paths (for example, tsx source alongside built workspace output in development).
When dual initialization is detected, a one-time CONNECTUM_AUTH_DUP_INIT warning is emitted to help diagnose mixed src/dist import issues.
No breaking changes and no action required. If you previously observed missing AuthContext values when mixing compiled and source imports in dev, this release transparently fixes it. If the warning appears in your logs, review your import paths to ensure @connectum/auth is not loaded from both source and built outputs.
RC.7 to RC.8
BREAKING: Serializer interceptor disabled by default
The serializer interceptor is now disabled by default in createDefaultInterceptors().
Why: Implicit JSON serialization caused issues with streaming between microservices and was unexpected for gRPC services using binary protobuf format.
When to enable:
- Your service uses the Connect protocol (HTTP/1.1 JSON) and needs protobuf ↔ JSON conversion
- You serve both Connect and gRPC clients and want JSON responses for Connect
When NOT needed (no action required):
- Pure gRPC services (binary protobuf)
- Services using
serializer: falsealready
Migration:
// Before (serializer was auto-enabled)
createDefaultInterceptors()
// After — if you need JSON serialization
createDefaultInterceptors({ serializer: true })
// After — if you use gRPC only (no change needed)
createDefaultInterceptors()Comprehensive test coverage
RC.8 ships with a substantial test-coverage expansion across 10 packages (+225 tests), including new suites for core/envSchema, core/server-lifecycle, auth/errors, auth/authz-utils, cli/proto-sync, events/topic, and healthcheck/healthcheck-grpc. This is an internal quality improvement -- no API changes, no action required.
RC.6 to RC.7
New Package: @connectum/events-amqp
AMQP/RabbitMQ adapter for the EventBus. Supports durable queues, topic exchanges, dead letter exchanges, and competing consumers via shared queue names.
import { createEventBus } from '@connectum/events';
import { AmqpAdapter } from '@connectum/events-amqp';
const bus = createEventBus({
adapter: AmqpAdapter({ url: 'amqp://localhost:5672' }),
routes: [myRoutes],
});See @connectum/events-amqp documentation for details.
EventBus: Auto-Derive Broker Client Identity
@connectum/events now automatically derives broker client identity (connection name, client ID) from protobuf service names registered in event routes. This improves observability in broker management UIs without manual configuration.
OpenTelemetry: Idempotent initProvider()
initProvider() in @connectum/otel is now idempotent -- subsequent calls are no-ops if the provider is already active. This simplifies initialization in tests and multi-module setups.
No breaking changes in this release.
RC.5 to RC.6
New Package: @connectum/events
Universal event adapter layer with proto-first pub/sub, pluggable broker adapters, middleware pipeline, and DLQ support. See ADR-026 for design rationale.
Three broker adapters ship alongside the core package:
@connectum/events-nats-- NATS JetStream adapter@connectum/events-kafka-- Kafka / Redpanda adapter@connectum/events-redis-- Redis Streams adapter
import { createEventBus, MemoryAdapter } from '@connectum/events';
const bus = createEventBus({
adapter: MemoryAdapter(),
routes: [myRoutes],
});See Events Getting Started for a step-by-step guide.
New Package: @connectum/testing
Testing utilities for ConnectRPC interceptors and services: mock factories for requests, streams, next functions, protobuf descriptors, plus a lightweight test server with automatic lifecycle management.
See @connectum/testing documentation for details.
EventBus: Error Classes and Graceful Drain
@connectum/events includes NonRetryableError and RetryableError typed error classes for explicit retry control in event handlers. Active message tracking ensures in-flight handlers complete during shutdown (configurable via drainTimeout).
@connectum/core: EventBusLike Integration
createServer() now accepts an eventBus option. When provided, the server manages the event bus lifecycle (start/stop) alongside the gRPC server, passing its shutdown signal for coordinated graceful shutdown.
No breaking changes in this release.
RC.4 to RC.5
@connectum/auth: JWT Key Resolution Priority Change
The JWT auth interceptor changed the key resolution priority:
| Before (rc.4) | After (rc.5) | |
|---|---|---|
| Priority | jwksUri > secret > publicKey | jwksUri > publicKey > secret |
This is potentially breaking if you provide both publicKey and secret. See Key Resolution Priority Change below for migration details.
@connectum/auth: Proto-Based Authorization
New createProtoAuthzInterceptor() factory reads authorization rules directly from .proto file custom options (connectum.auth.v1), eliminating the need for hardcoded rule arrays in application code.
See Proto-Based Authorization for details.
@connectum/core: HTTP/1.1 Plaintext Transport Mode
The server now supports three transport modes:
| Mode | Description | Use Case |
|---|---|---|
| TLS (default) | HTTP/2 with TLS | Production |
| h2c | HTTP/2 cleartext | Development, service mesh |
| HTTP/1.1 | HTTP/1.1 plaintext | Legacy clients, ConnectRPC JSON |
Security Fixes
minimatchoverridden to >=10.2.1 (ReDoS vulnerability)ajvbumped to 8.18.0 (CVE-2025-69873)
No other breaking changes in this release.
RC.3 to RC.4
Compile-Before-Publish with tsup
All @connectum/* packages now ship compiled .js + .d.ts + source maps via tsup. This is the most significant change in rc.4.
| Aspect | Before (rc.3) | After (rc.4) |
|---|---|---|
| Published format | Raw .ts source | Compiled .js + .d.ts + .js.map |
| Consumer Node.js | >= 25.2.0 (type stripping required) | >= 20.0.0 (compiled JS) |
| Loader/register hook | Required @connectum/core/register | Not needed |
| Runtime compatibility | Node.js 25+ only | Node.js 20+, Bun, tsx |
Migration: Remove any --import @connectum/core/register flags or register() calls from your startup scripts. Packages now work out of the box.
Removed: @connectum/core/register
The @connectum/core/register subpath export has been removed. It was needed in rc.3 to enable type stripping for raw TypeScript source. Since packages now ship compiled JavaScript, no register hook is needed.
- node --import @connectum/core/register server.ts
+ node server.jsNew Package: @connectum/auth
A complete authentication and authorization package with 5 interceptor factories:
createAuthInterceptor()-- Generic pluggable authcreateJwtAuthInterceptor()-- JWT with JWKS support (jose)createGatewayAuthInterceptor()-- Gateway pre-authenticated headerscreateSessionAuthInterceptor()-- Session-based auth (better-auth, lucia)createAuthzInterceptor()-- Declarative rules-based authorization
See @connectum/auth documentation for details.
Error Handler: SanitizableError Protocol
The error handler interceptor in @connectum/interceptors now recognizes the SanitizableError protocol from @connectum/core. Errors implementing this interface have their clientMessage sent to clients while serverDetails are preserved for logging.
New onError callback option replaces console.error for structured error handling:
createDefaultInterceptors({
errorHandler: {
onError: ({ error, code, serverDetails, stack }) => {
logger.error('RPC error', { code, serverDetails });
},
},
});OpenTelemetry: Streaming RPC Support
@connectum/otel now instruments streaming RPCs (client, server, and bidirectional). Span lifecycle is deferred to stream completion for accurate duration measurement.
Cross-Runtime Testing
All packages now include test:bun and test:esbuild scripts via @exodus/test. Known incompatibilities (interceptors/bun, otel/bun, cli/bun) gracefully skip.
Build Pipeline Changes
The turbo build graph has changed:
build:proto → build (tsup) → typecheck (tsc --noEmit) → testtypecheck and test now depend on build (they require compiled dist/ artifacts). Always run pnpm build before pnpm typecheck or pnpm test.
Breaking Changes from Alpha
If you are migrating from v0.2.0-alpha.x to v1.0.0, the following breaking changes apply:
API Changes
| Before (alpha) | After (beta) | ADR |
|---|---|---|
Runner(options) | createServer(options) | ADR-023 |
withReflection() | Reflection() | ADR-022 |
withHealthcheck() | Healthcheck({ httpEnabled: true }) | ADR-022 |
server.health.update() | healthcheckManager.update() | ADR-022 |
builtinInterceptors: { ... } | interceptors: createDefaultInterceptors({ ... }) | ADR-023 |
Package Changes
| Change | Details |
|---|---|
New package: @connectum/healthcheck | Extracted from @connectum/core |
New package: @connectum/reflection | Extracted from @connectum/core |
New package: @connectum/cli | Proto sync via gRPC Server Reflection |
Removed: @connectum/utilities | Merged into other packages |
| Total packages | 6 → 7 |
Migration Example
Before (alpha):
import { Runner } from '@connectum/core';
const server = Runner({
services: [routes],
port: 5000,
builtinInterceptors: {
errorHandler: true,
logger: { level: 'debug' },
tracing: true,
},
health: { enabled: true },
reflection: true,
});
server.on('ready', () => {
server.health.update(ServingStatus.SERVING);
});
await server.start();After (beta):
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';
import { createDefaultInterceptors } from '@connectum/interceptors';
const server = createServer({
services: [routes],
port: 5000,
protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
interceptors: createDefaultInterceptors(),
shutdown: { autoShutdown: true },
});
server.on('ready', () => {
healthcheckManager.update(ServingStatus.SERVING);
});
await server.start();Interceptors: No Auto-Defaults
Starting from v1.0.0, @connectum/core has zero internal dependencies. Omitting the interceptors option (or passing []) means no interceptors are applied. To use the default interceptor chain, explicitly pass createDefaultInterceptors():
import { createServer } from '@connectum/core';
import { createDefaultInterceptors } from '@connectum/interceptors';
const server = createServer({
services: [routes],
interceptors: createDefaultInterceptors(), // explicit!
});Key Resolution Priority Change (@connectum/auth)
Affected versions: v1.0.0 onwards
The JWT auth interceptor (createJwtAuthInterceptor) changed the key resolution priority when multiple key sources are provided:
| Before | After | |
|---|---|---|
| Priority | jwksUri > secret > publicKey | jwksUri > publicKey > secret |
Impact
This change only affects configurations that provide both publicKey and secret simultaneously. Previously the symmetric secret was used; now the asymmetric publicKey takes precedence.
- No impact if you use only one of
jwksUri,secret, orpublicKey - No impact if you use
jwksUri(highest priority, unchanged)
Migration
If you rely on the previous behavior where secret wins over publicKey, remove the publicKey option:
// Before: secret was used (publicKey was ignored)
const auth = createJwtAuthInterceptor({
secret: process.env.JWT_SECRET,
publicKey: myPublicKey, // was silently ignored
});
// After: remove publicKey if you want HMAC verification
const auth = createJwtAuthInterceptor({
secret: process.env.JWT_SECRET,
});Rationale
Asymmetric keys (publicKey) are cryptographically stronger than symmetric secrets (secret). When both are provided, the more secure option should take precedence.
Changelog
For detailed changelog, see the CHANGELOG.md in the main repository.
Architecture Decision Records
All significant architectural decisions are documented as ADRs:
- ADR Index -- Complete list of all accepted Architecture Decision Records
