Multi-tenant notifications platform
A notifications product with a backend SDK for sending events and a frontend SDK for reading them, built to make in-app notifications simple to add and reliable to run.

Most teams start notifications as a database table and a frontend badge. Pigeon treats them as a cross-cutting product surface from the first commit.
Backends should call one client to send. Frontends should mount a provider and render a bell that stays live, survives reconnects, and reads optimistically. Customers should receive signed webhooks with retries that leave evidence. None of that should be assembled from scratch inside the consuming app.
The result is a TypeScript monorepo: an API, a worker, a web dashboard, a Node SDK, a React SDK, and a demo that exercises the full loop end-to-end.
The Node SDK sends. The API persists the request, hands work to background processing, and returns. Workers render, publish live updates, and dispatch signed webhooks. The React SDK opens an SSE stream and resumes cleanly after reconnects.
Accept fast, deliver async, fan out everywhere. The send returns after durable persistence and job handoff. The slower work stays out of the caller's request path.
Projects are top-level. Each project has development and production environments with their own scoped credentials. Almost everything that matters, including keys, users, notifications, templates, and webhooks, lives behind an environment, not just a project.
Backends authenticate with long-lived API keys. Frontends use short-lived client tokens minted per end user. Different blast radius and different rate budgets, but one API enforces both.
The schema is organized around a simple rule: human collaboration lives at the project level, while credentials, recipients, notifications, templates, and webhooks are scoped per environment. That keeps tenancy visible in the data model instead of depending on scattered application checks.
dashboard auth and session state
workspace and membership boundaries
credential storage and recipient identity
delivery records, content, and idempotency
delivery destinations and attempt history
Live fanout and reconnect replay are handled separately. Connected clients receive updates immediately, while reconnecting clients get a short recovery window so brief network drops do not turn into missed notifications.
Duplicate sends are blocked at the persistence layer, not just discouraged in application code.
Transient coordination stays separate from the system of record, which keeps recovery and reasoning much simpler.
The notification commits before background processing begins, so failures surface clearly instead of disappearing into side effects.
Webhook delivery leaves a traceable attempt history, which matters for debugging and support.
The product needs one-way delivery, not full duplex messaging. SSE keeps the auth and reconnect story simpler for this scope.
Workers may retry a job after partial success. Idempotency keys carry the weight at the boundaries.
Reconnect history exists to smooth over short disconnects, not to act as a permanent event ledger.
No multi-region or active-active deployment story yet. The MVP picks scope over posture.
Rendering, fanout, and logging cannot sit on the caller's request. The send has to return fast or every integration feels the slowest dependency.
Live fanout does not retain history. Reconnects need a cursor and a replay window the SDK can drive without consumer code.
Endpoints time out, 5xx, or 200-then-crash. Delivery has to retry, and every attempt has to leave evidence.
Backends authenticate with long-lived keys, frontends with short-lived tokens. Different blast radius, different rate budgets, one API.
In-app + webhooks today. No email, SMS, or push.
Single-region in spirit. No multi-region or DR story shipped.
Basic dashboard authentication only. No SSO or enterprise identity layer yet.
One Node SDK call sends a notification, another mints a frontend token. Validation and typed errors live at the boundary, not inside the app.
A provider, a hook, and a bell. Token caching, reconnects, and optimistic reads are handled inside the SDK, not in consumer code.
Live events stream into the SDK while reconnects resume from the last seen event. Backgrounded tabs and brief network loss stop dropping notifications.
A platform is only as good as the libraries integrators actually touch. Two clean SDKs do more for adoption than another feature behind a flag.
Live delivery and replay want different storage. Solve them separately and they compose.
Project and environment scoping in indexes and uniqueness constraints made the rest of the system easier to reason about than enforcing it in code paths.
Slow work goes to a queue, fast work stays inline. That line is the architecture.
Long-form to short-form, automated