Migrating from v2 to v3
postal v3 is a ground-up TypeScript rewrite. The core idea — channels, topics, wildcard subscriptions — is the same. Almost everything else changed. This guide walks through each breaking change with before/after code so you can migrate methodically.
The big picture
Section titled “The big picture”| v2 | v3 | |
|---|---|---|
| Module | Global postal object (UMD) | Named exports (ESM + CJS) |
| Dependencies | lodash | None |
| Subscriber args | (data, envelope) | (envelope) — data is envelope.payload |
| Subscription chaining | .debounce(), .throttle(), .constraint(), etc. | Removed — compose your own |
| RPC | Add-on (postal.request-response) | Built-in request() / handle() |
| Federation | postal.federation, postal.xframe | addTransport() + transport packages |
| Types | None | Full TypeScript with inference |
| Error handling | Per-subscription .catch() | AggregateError after all subscribers run |
| Channel cleanup | postal.reset() | channel.dispose() + resetChannels() |
Installation
Section titled “Installation”Zero runtime dependencies. The lodash peer dependency is gone.
Imports and channel access
Section titled “Imports and channel access”The global postal object is gone. Everything is a named export.
getChannel is a singleton factory — same name, same instance. Calling it with no arguments returns the default channel (named "__default__"). In v2, the default channel was "/".
Subscriber callback signature
Section titled “Subscriber callback signature”This is the change that’ll touch the most lines. Subscribers no longer receive data as the first argument.
Search pattern to find v2 subscribers in your codebase:
Envelope shape
Section titled “Envelope shape”The envelope itself has changed.
Key renames:
envelope.data→envelope.payloadenvelope.timeStamp→envelope.timestamp(lowercases, now a number not a Date)envelope.headers— removed entirely
Publish
Section titled “Publish”The signature simplified. No more object form, no more callbacks.
The publish callback that reported { activated, skipped } stats is gone. If you need dispatch metrics, use a wire tap.
Removed subscription methods
Section titled “Removed subscription methods”v2 had a rich chainable API on SubscriptionDefinition. v3 returns a plain unsubscribe function — no chaining, no middleware pipeline.
Here’s how to replace each method:
.constraint(predicate) / .constraints([...])
Section titled “.constraint(predicate) / .constraints([...])”Filter inside your callback.
.distinctUntilChanged()
Section titled “.distinctUntilChanged()”Track the previous value yourself.
.distinct()
Section titled “.distinct()”Track all seen values.
.debounce(ms)
Section titled “.debounce(ms)”Use setTimeout / clearTimeout.
.throttle(ms)
Section titled “.throttle(ms)”Standard throttle pattern.
.delay(ms) / .defer()
Section titled “.delay(ms) / .defer()”Wrap in setTimeout.
.disposeAfter(n) / .once()
Section titled “.disposeAfter(n) / .once()”Count invocations and unsubscribe.
.withContext(ctx) / .context(ctx)
Section titled “.withContext(ctx) / .context(ctx)”Use arrow functions or .bind().
.catch(errorHandler)
Section titled “.catch(errorHandler)”Wrap your callback in try/catch.
.logError()
Section titled “.logError()”Same idea — catch and log.
Unsubscribing
Section titled “Unsubscribing”v2 returned a SubscriptionDefinition with an .unsubscribe() method (or you passed it to postal.unsubscribe()). v3 returns a plain function.
Wire taps
Section titled “Wire taps”The API is similar but the callback signature changed.
Changes:
postal.addWireTap→ named exportaddWiretap(lowercaset)- Callback receives only
(envelope)— no separatedataorpublishDepthargs - Wire tap errors are silently swallowed (they never affect dispatch)
Request / Handle (new)
Section titled “Request / Handle (new)”v2 had no built-in RPC. You either used the postal.request-response add-on or rolled your own with paired subscribe/publish calls.
v3 has first-class RPC with automatic correlation, timeouts, and error propagation:
See Subscriptions — Request / Handle for the full API.
Federation → Transports
Section titled “Federation → Transports”v2 used postal.federation and packages like postal.xframe for cross-boundary messaging. v3 replaces this with a Transport interface and dedicated packages.
Available transport packages:
postal-transport-messageport— iframes and Web Workerspostal-transport-broadcastchannel— cross-tab communication
See Transports for writing custom transports.
Error handling
Section titled “Error handling”In v3, if one subscriber throws, the rest still execute. All errors are collected and thrown as a single AggregateError after dispatch completes.
Channel disposal (new)
Section titled “Channel disposal (new)”v3 channels can be explicitly torn down:
Disposal is idempotent — calling dispose() twice is safe.
Configuration
Section titled “Configuration”v2 had a postal.configuration object with properties like DEFAULT_CHANNEL, SYSTEM_CHANNEL, enableSystemMessages, and the resolver cache settings. This is all gone in v3.
The v2 system channel ("postal") that broadcast subscription.created / subscription.removed events is gone. If you relied on subscription lifecycle events, use the return value of subscribe() to track subscription state in your own code.
Test isolation
Section titled “Test isolation”Removed APIs
Section titled “Removed APIs”These v2 APIs have no direct replacement in v3:
| v2 API | Status |
|---|---|
postal.noConflict() | Removed — ESM modules don’t have global conflicts |
postal.getSubscribersFor(options) | Removed — no subscriber introspection API |
postal.unsubscribeFor(options) | Removed — track your own unsubscribe functions |
postal.configuration.resolver | Removed — internal resolver is not configurable |
postal.SubscriptionDefinition | Removed — subscriptions are not objects |
postal.ChannelDefinition | Removed — channels are opaque objects |
| System channel events | Removed — no subscription.created / subscription.removed |
TypeScript types (new)
Section titled “TypeScript types (new)”v2 had no TypeScript types (community @types/postal existed but was limited). v3 ships full types with inference:
See Getting Started — Typed channels for details.
Migration checklist
Section titled “Migration checklist”- Update the install —
npm install postal(removelodashif it was only a postal dependency) - Replace imports —
require("postal")→import { getChannel, ... } from "postal" - Replace
postal.channel(name)→getChannel(name) - Update subscriber callbacks —
(data, envelope)→(envelope), thendata.x→envelope.payload.x - Replace
envelope.data→envelope.payloadeverywhere - Replace
envelope.timeStamp→envelope.timestamp(lowercases) — it’s nowDate.now()(a number), not aDateobject - Replace subscription chaining — see Removed subscription methods
- Replace
.unsubscribe()with the returned function - Update wire taps —
addWireTap→addWiretap, update callback signature - Migrate federation to transports if applicable
- Replace
postal.reset()→resetChannels()in tests - Run your tests — most remaining issues will be
envelope.data→envelope.payloadmisses