Skip to content

How it works

palumb has two halves. Understanding the split is most of understanding palumb.

palumb runs the whole durable platform for you. Two services:

palumb-core — the managed multitenant control plane. It is the only part that holds your state and secrets, and it does exactly four jobs:

  1. Authenticates you. Every request from your backend carries a tenant API key — the key is the machine identity. (Humans manage those keys, channels, and billing by signing in to the dashboard; the machine API itself stays key-scoped.) See Tenants & API keys.
  2. Knows your audience. Subscribers, topics, and subscriptions live here, so a trigger can target “one user” or “everyone subscribed to this topic”. See Audience.
  3. Holds your channels. A channel is a concrete provider (e.g. an SMTP relay). You attach your credentials once; palumb stores them encrypted at rest and never returns them. See Channels.
  4. Receives triggers and makes deliveries. It is the front door (POST /v1/events) and the back door that actually talks to providers.

palumb-engine — the durable runtime, built on Restate. It runs a generic executor that drives your workflows step by step and persists their state and timers. You never operate it, and no Restate runs on your side.

Your notification logic is plain TypeScript built with the @palumb/sdk Bridge SDK, exposed as one signed HTTP endpoint — the Bridge. You host nothing durable: just that webhook. Two ideas cover almost everything:

  • workflow(id, fn) — declares a workflow. id is what a trigger names, and fn is plain TypeScript that awaits durable steps. See Triggers and Writing a workflow.
  • step.* — the durable steps inside a workflow: step.run (arbitrary code), step.send (deliver a message), step.delay (durable wait), and step.waitForEvent (suspend until an external signal). palumb’s engine records each completed step and never re-runs it. See Sending messages.

palumb’s engine calls your Bridge to ask “what’s the next step?”, runs it durably, and repeats until the workflow finishes — so the durability lives in palumb while the logic lives in your code.

On top of those, palumb ships building blocks — durable patterns you would otherwise hand-roll. The first is Digest: accumulate many items for a subscriber and flush them as one message on a window or a schedule.

  1. Your app fires a trigger. POST /v1/events with { workflowId, to, payload }, authenticated by your API key.

  2. The control plane resolves recipients. to is either one subscriber or a topic fan-out; palumb expands it to a deduplicated set of subscribers.

  3. The engine starts a run — once per subscriber. palumb-core submits a run of the generic executor on palumb-engine, carrying your Bridge binding. Each run targets exactly one subscriber.

  4. The engine drives your Bridge, durably. It calls your webhook (HMAC-signed) to run the next step, records the result, and loops. Your workflow can branch, wait days for an event, or route into a digest — state and timers survive restarts.

  5. Your workflow declares a send. step.send(id, "email", () => content) names a channel kind and renders already-formatted content. The engine turns that into an internal delivery call.

  6. The control plane delivers. It resolves your enabled channel of that kind, decrypts its credentials, looks up the subscriber’s address, calls the provider, and records the attempt — idempotently.

The integration is two HTTP edges: your app → /v1/events (a trigger in) and palumb → your signed Bridge (palumb driving your workflow). That split keeps your code free of provider secrets and keeps palumb free of your business logic.

  • Architecture — the same loop, with the data model and failure behavior.
  • Quickstart — run all six steps end to end.