Skip to content

Getting Started

npm install postal
pnpm add postal
yarn add postal

postal ships as ESM and CJS with full TypeScript declarations. No peer dependencies.

import { getChannel } from "postal";

// Get (or create) a channel
const orders = getChannel("orders");

// Subscribe to a topic
const unsub = orders.subscribe("item.placed", envelope => {
    console.log("New order:", envelope.payload);
    console.log("Topic:", envelope.topic);
    console.log("Timestamp:", envelope.timestamp);
});

// Publish a message
orders.publish("item.placed", { sku: "WIDGET-42", qty: 3 });

// Unsubscribe when done
unsub();

getChannel is a singleton factory — calling it with the same name always returns the same channel instance. Subscribers receive the full Envelope, not just the payload.

const ch = getChannel("events");

// * matches exactly one segment
ch.subscribe("user.*", env => {
    // Matches: user.created, user.deleted
    // Does NOT match: user.profile.updated
});

// # matches zero or more segments
ch.subscribe("user.#", env => {
    // Matches: user, user.created, user.profile.updated
});

See Concepts for the full wildcard matching rules.

postal supports compile-time payload inference via module augmentation. Declare your channel’s topic-to-payload map once in a .d.ts or any TypeScript file, and every getChannel call site picks it up automatically — no generics required:

declare module "postal" {
    interface ChannelRegistry {
        orders: {
            "item.placed": { sku: string; qty: number };
            "item.cancelled": { sku: string; reason: string };
        };
    }
}

// getChannel("orders") now infers the full topic map automatically
const orders = getChannel("orders");
orders.publish("item.placed", { sku: "WIDGET-42", qty: 3 }); // Typed!

postal includes correlation-based RPC. Define RPC topics with { request, response } payloads:

declare module "postal" {
    interface ChannelRegistry {
        pricing: {
            "quote.request": {
                request: { sku: string; qty: number };
                response: { total: number; currency: string };
            };
        };
    }
}

const pricing = getChannel("pricing");

// Register a handler (one per topic per channel)
const unhandle = pricing.handle("quote.request", envelope => {
    const { sku, qty } = envelope.payload;
    return { total: qty * 9.99, currency: "USD" };
});

// Send a request — returns a Promise
const quote = await pricing.request("quote.request", {
    sku: "WIDGET-42",
    qty: 5,
});
console.log(quote); // { total: 49.95, currency: "USD" }