# postal.js v3 is a ground-up rewrite, and the root manifest still describes v2

> A pub/sub message bus for JavaScript and TypeScript, with wildcard topics, channel scoping, request and response, wire taps, and five transports for iframes, workers, tabs and Node processes. The install is one package with no dependencies; the cost of the rewrite is that v2 users must migrate.

**postaljs/postal.js** — JavaScript pub/sub library supporting advanced subscription features, and several helpful add-ons.

- Repository: https://github.com/postaljs/postal.js
- Website: http://ifandelse.com
- Stars: 2,834 · Forks: 187
- Language: TypeScript
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/postaljs-postal-js

## Version 3 replaced the module system, the callbacks, and the envelope

There is a callout in the readme addressed to people on the previous major version, and it is not subtle about the scale of the change. Postal v3 is described as a ground-up rewrite with breaking changes to the module system, subscriber callbacks, the envelope shape, and more. A separate migration guide is published for the move.

Three of those four are structural rather than cosmetic. A new module system means new import paths, a new callback shape means every subscription site is touched, and a new envelope shape means every handler that reaches into the message has to change. Only the idea of the library survives.

The new shape is compact. You obtain a named channel from a function, subscribe on that channel to a topic string, and receive an envelope whose payload carries the data you published. Publishing takes the topic and a plain object.

That is worth reading next to the upgrade notice, because the design is not more complicated than v2 was. The cost of this rewrite is migration effort, not ongoing complexity, which is the right way round for a library whose selling point is that it stays out of your way.

## Wildcards follow AMQP, one segment for a star and any number for a hash

The topic syntax borrows from a messaging standard rather than inventing its own, and the two wildcards have the standard meanings.

A single asterisk matches exactly one topic segment. A hash matches zero or more segments. So a subscription to a wildcard segment listens for every topic at that depth regardless of which channel is used, and a subscription ending in the hash listens at that depth and below.

The alternative would be a string prefix match, which is why naming the two wildcards after the standard is useful documentation in itself: someone arriving from that standard already knows the semantics, and someone arriving fresh learns a syntax that will not change under them.

Combined with channel scoping, this gives two levels of organisation. A channel is a named message domain, and the topic within it is a dotted string with wildcards available at each segment. The quick example uses a single channel named for orders and a single topic on it.

Full type inference is claimed for channels, topics, and message data, which is the feature the project leads with and the reason it is TypeScript-first rather than TypeScript-compatible.

## One core package and five transports, published separately

The package table lists six entries, and only one of them is the library most people came for.

The core package is the message bus. The other five are transports, each published under its own name, and between them they cover the places a browser or a server puts messages apart.

A MessagePort transport handles iframes and workers. A BroadcastChannel transport handles separate browser tabs. A ServiceWorker transport is the most elaborated of the three browser ones: it is described as a dedicated MessagePort per tab, with presence tracking and resilience to the service worker restarting, which is the failure mode that makes naive implementations lose messages. A child_process and cluster transport handles inter-process communication inside Node.js. A Unix domain socket transport handles communication between independent Node.js processes rather than between children of one.

The split matters because the core does not know about any of them. A consumer installs the core package and gets a bus; crossing a process or frame boundary is an explicit, separate dependency with its own name.

That is a deliberate position for a library whose feature list leads with zero dependencies.

## Node 22.22 is the floor, pnpm is pinned, and Turborepo runs the gate

The root manifest sets the floor high. The engines field requires Node 22.22 or newer, which is a specific patch level of a recent major rather than a loose major range. The package manager is pinned to an exact pnpm release with a version suffix, and the workspace is declared as a pnpm workspace with a Turborepo configuration at the root.

Every root script delegates to Turborepo rather than doing the work directly: build, development, lint, and test all fan out across packages. The aggregate check runs lint, then tests, then the build, and the readme calls that combination the CI gate:

```bash
pnpm install       # Install dependencies
pnpm build         # Build all packages
pnpm test          # Run all tests
pnpm lint          # Lint all packages
pnpm run checks    # lint + test + build (CI gate)
```

So the same commands a contributor runs locally are what continuous integration runs.

Release handling uses changesets, with a script to version the packages and a release script that builds before publishing. A commit hook directory and a lint-staged block in the manifest mean formatting runs on staged files at commit time rather than being left to a separate command.

For a consumer none of this applies. The install instruction is a single npm command for one package, and the core package is described as having no runtime dependencies at all.

## Wire taps see every message, which is the opposite of encapsulation

One feature deserves attention precisely because it is not scoped. Wire taps are described as global observers that see every message on the bus.

Every other routing feature in the library narrows what a given listener receives. Channels isolate domains. Wildcards narrow by topic shape. A subscription to one channel cannot see another channel's traffic. A wire tap can see all of it, regardless of channel.

That makes wire taps the mechanism for cross-cutting concerns: logging, metrics, tracing, debugging, and the kind of instrumentation you cannot add at every subscription site because you do not own every call site. It also makes them the mechanism you would use to build a competing router on top of the bus, since a tap sees the full stream.

The library does not present that as a risk, and it is not one in a hostile sense. But it is a design decision with a consequence: any code that can install a wire tap on your bus can observe every message, so the tap is the place to look when auditing who is watching a message flow.

The request and handle pattern sits alongside it as the built-in request and response form, which is the other feature aimed at structure rather than routing.

## The root manifest is private, versioned 0.1.0, and still describes v2

The root package manifest is the monorepo's own, not a published package. It is marked private, which prevents it from being published by accident, and its version is 0.1.0, which is a placeholder rather than the library's version. Per-package versions come from the changesets workflow.

Its description field, however, is inherited from the previous generation. It reads as a pub and sub library providing wildcard subscriptions and complex message handling, and it says the library works server and client side. That is a v2 era summary: the word mediator appears among the keywords, and mediator pattern libraries are the previous identity of this project.

The same file also disagrees with the repository metadata in two small ways. It gives the project homepage as the GitHub repository URL, while the repository's own homepage field points at the author's personal site. And the repository URL uses the git protocol rather than https, which is the older of the two forms and the one fewer tools handle.

None of this affects an install. All of it is what a reader sees when they look at the manifest to find out who maintains the thing.

## A second README for models, a docs site built on Starlight, and an empty badge block

Three details in the tree and the front page suggest the project is being maintained with machine readers in mind.

There is a second readme file written for language models rather than for people, and a text file at the root following the convention of listing a site for crawlers. Both sit alongside the normal readme, so the documentation surface is deliberately duplicated in machine-readable form.

There are also two agent instruction files at the root, one for a general convention and one named for a specific assistant tool, which is the same pattern of committing machine-readable guidance you see in a lot of current TypeScript repositories.

The documentation site is built from a configuration whose asset directory name identifies the generator as Astro's Starlight, and the readme points at a documentation domain rather than at the repository for the full API. There is a development container configuration and a node version file as well.

The front page has one small unfinished thing: a comment marking where badges belong, immediately above the features list, with no badges rendered in its place.

## Conclusion

postal.js suits a TypeScript codebase that wants typed pub/sub without adopting a message broker or a state library, particularly one that spans frames, workers, tabs, or Node processes and has outgrown hand-rolled events. It does not suit an existing v2 installation that cannot absorb a migration, because the rewrite changed the module system, the subscriber callbacks, and the envelope shape. Before you adopt it, check three things: whether your Node build machines are on 22.22 or newer, which the workspace requires; which transport you actually need, since five are published separately and only one of them is about the browser; and whether you want a bus you can observe globally, because the wire tap feature sees every message and is the opposite of encapsulation.

## FAQ

### What is postal.js version 3?

A pub/sub message bus for JavaScript and TypeScript. It provides full type inference on channels, topics and message data, AMQP-style wildcard topics where a single asterisk matches one segment and a hash matches zero or more, channel-scoped messaging, a built-in request and response pattern, global wire taps, a transport system, and no runtime dependencies. It is MIT licensed.

### How do I upgrade from postal v2 to v3?

Version 3 is described as a ground-up rewrite with breaking changes to the module system, subscriber callbacks, the envelope shape, and more, and a dedicated v2 to v3 migration guide is published. The new usage shape is a named channel obtained from a function, a subscribe call taking a topic and a handler that receives an envelope, and a publish call taking a topic and a plain object.

### Which transports does postal.js publish?

Five, each as its own package alongside the core: a MessagePort transport for iframes and workers, a BroadcastChannel transport for cross-tab messaging, a ServiceWorker transport with a dedicated MessagePort per tab plus presence tracking and resilience to a restarted worker, a child_process and cluster transport for Node.js, and a Unix domain socket transport for independent Node.js processes.

### What does postal.js need to install and build?

Consumers install one package from npm with a single command, and the core package is described as having zero runtime dependencies. Working on the repository itself needs Node 22.22 or newer, a pinned pnpm release, and Turborepo, with lint, tests and build forming the CI gate. Release versions for the packages are produced by changesets rather than by hand.

## Sources

- [Issues](https://github.com/postaljs/postal.js/issues)
- [License: MIT](https://github.com/postaljs/postal.js/blob/master/LICENSE)
- [postaljs/postal.js on GitHub](https://github.com/postaljs/postal.js)
- [Project website](http://ifandelse.com)
- [README](https://github.com/postaljs/postal.js/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/postaljs-postal-js
