{ "schemaVersion": "0.1.1", "kind": "graph", "generatedAt": "2026-08-19T18:24:00.000Z", "title": "Batch broadcast sending through Postmark", "summary": "Broadcast delivery moves from one Postmark request per recipient to batched requests of 500, with suppression filtering pulled in front of the send and the payload builder extracted into a shared library.", "lenses": [ "architecture", "data-flow" ], "provenance": { "repo": { "owner": "ohansemmanuel", "name": "bestregards", "host": "github.com" }, "base": { "sha": "3f5c1ab9d24e7f08c6b1a5d3e9074c2b8a6f1d40", "ref": "main" }, "head": { "sha": "b71e0d4c8a92f5361de7c0b4a8f2593d6c1e8a77", "ref": "batch-broadcast-send" }, "pullRequest": { "number": 128, "title": "Send broadcasts in batches of 500", "url": "https://github.com/ohansemmanuel/bestregards/pull/128" }, "generator": { "name": "pr-lens-examples", "version": "0.1.0" } }, "lanes": [ { "id": "web", "label": "Next.js", "subtitle": "Vercel", "order": 0 }, { "id": "functions", "label": "Cloud Functions", "subtitle": "Firebase", "order": 1 }, { "id": "external", "label": "External", "subtitle": "Postmark", "order": 2 } ], "nodes": [ { "id": "broadcast-composer", "label": "Broadcast composer", "kind": "ui", "delta": "unchanged", "lane": "web", "subtitle": "app/broadcasts/new", "summary": "Where an author writes a broadcast and hits send. Untouched by this change.", "files": [ { "path": "app/broadcasts/new/page.tsx" } ], "badges": [] }, { "id": "queue-route", "label": "POST /api/broadcasts/queue", "kind": "route", "delta": "modified", "lane": "web", "summary": "Writes the queue document. Now stamps the recipient count and batch size the sender will use instead of leaving batching to the worker.", "files": [ { "path": "app/api/broadcasts/queue/route.ts", "startLine": 24, "endLine": 96 } ], "badges": [ "+38 / -12" ] }, { "id": "broadcast-queue", "label": "broadcastQueue", "kind": "datastore", "delta": "modified", "lane": "functions", "subtitle": "Firestore collection", "summary": "Queue documents gained batchSize and suppressedCount fields, and results are now written back per batch rather than per recipient.", "files": [ { "path": "functions/src/broadcast/schema.ts", "startLine": 12, "endLine": 48 } ], "badges": [] }, { "id": "send-broadcast-bulk", "label": "sendBroadcastBulk", "kind": "function", "delta": "added", "lane": "functions", "subtitle": "onWrite trigger", "summary": "New trigger handler. Fetches suppressions once, builds batched payloads, and posts them to Postmark in chunks of 500.", "files": [ { "path": "functions/src/broadcast/sendBroadcastBulk.ts", "startLine": 1, "endLine": 142 } ], "badges": [ "new" ] }, { "id": "build-bulk-payload", "label": "buildBulkPayload", "kind": "function", "delta": "added", "lane": "functions", "summary": "Turns a broadcast and its recipient slice into a Postmark batch request body.", "files": [ { "path": "packages/broadcast-lib/src/buildBulkPayload.ts", "startLine": 1, "endLine": 74 } ], "badges": [] }, { "id": "get-suppressed-emails", "label": "getSuppressedEmails", "kind": "function", "delta": "added", "lane": "functions", "summary": "Pulls the Postmark suppression dump once per broadcast so suppressed addresses are filtered before any batch is sent.", "files": [ { "path": "packages/broadcast-lib/src/getSuppressedEmails.ts", "startLine": 1, "endLine": 58 } ], "badges": [] }, { "id": "broadcast-lib", "label": "broadcast-lib", "kind": "package", "delta": "added", "lane": "functions", "subtitle": "packages/broadcast-lib", "summary": "New shared package so the queue route and the sender agree on payload shape and batch size.", "files": [ { "path": "packages/broadcast-lib/src/index.ts" } ], "badges": [ "new package" ] }, { "id": "process-broadcast", "label": "processBroadcast", "kind": "function", "delta": "removed", "lane": "functions", "subtitle": "onWrite trigger", "summary": "The per-recipient loop this change replaces.", "files": [ { "path": "functions/src/broadcast/processBroadcast.ts", "startLine": 1, "endLine": 118, "revision": "base" } ], "badges": [] }, { "id": "send-single-email", "label": "sendSingleEmail", "kind": "function", "delta": "removed", "lane": "functions", "summary": "One Postmark request per recipient. Gone with the loop that called it.", "files": [ { "path": "functions/src/broadcast/sendSingleEmail.ts", "startLine": 1, "endLine": 46, "revision": "base" } ], "badges": [] }, { "id": "postmark", "label": "Postmark", "kind": "external", "delta": "modified", "lane": "external", "subtitle": "Email API", "summary": "Same provider, different endpoints: the batch endpoint and the suppression dump replace repeated single sends.", "files": [], "badges": [] } ], "edges": [ { "id": "composer-to-queue", "from": "broadcast-composer", "to": "queue-route", "kind": "http", "delta": "unchanged", "label": "send broadcast", "emphasis": "normal", "animated": false, "files": [] }, { "id": "queue-to-firestore", "from": "queue-route", "to": "broadcast-queue", "kind": "data", "delta": "modified", "label": "enqueue job", "emphasis": "normal", "animated": false, "files": [] }, { "id": "queue-to-lib", "from": "queue-route", "to": "broadcast-lib", "kind": "dependency", "delta": "added", "label": "batch size", "emphasis": "normal", "animated": false, "files": [] }, { "id": "firestore-to-bulk", "from": "broadcast-queue", "to": "send-broadcast-bulk", "kind": "event", "delta": "added", "label": "onWrite", "emphasis": "normal", "animated": false, "files": [] }, { "id": "firestore-to-process", "from": "broadcast-queue", "to": "process-broadcast", "kind": "event", "delta": "removed", "label": "onWrite", "emphasis": "normal", "animated": false, "files": [] }, { "id": "process-to-single", "from": "process-broadcast", "to": "send-single-email", "kind": "call", "delta": "removed", "label": "per recipient", "emphasis": "normal", "animated": false, "files": [] }, { "id": "single-to-postmark", "from": "send-single-email", "to": "postmark", "kind": "http", "delta": "removed", "label": "POST /email · 1 msg/call", "emphasis": "normal", "animated": false, "files": [] }, { "id": "bulk-to-payload", "from": "send-broadcast-bulk", "to": "build-bulk-payload", "kind": "call", "delta": "added", "emphasis": "normal", "animated": false, "files": [] }, { "id": "bulk-to-suppressions", "from": "send-broadcast-bulk", "to": "get-suppressed-emails", "kind": "call", "delta": "added", "emphasis": "normal", "animated": false, "files": [] }, { "id": "bulk-to-lib", "from": "send-broadcast-bulk", "to": "broadcast-lib", "kind": "dependency", "delta": "added", "emphasis": "normal", "animated": false, "files": [] }, { "id": "suppressions-to-postmark", "from": "get-suppressed-emails", "to": "postmark", "kind": "http", "delta": "added", "label": "GET suppression dump", "emphasis": "normal", "animated": true, "files": [] }, { "id": "bulk-to-postmark", "from": "send-broadcast-bulk", "to": "postmark", "kind": "http", "delta": "added", "label": "500 msgs/call", "emphasis": "hero", "animated": true, "summary": "The change in one edge: a broadcast to 10,000 recipients drops from 10,000 requests to 20.", "files": [] }, { "id": "bulk-to-firestore", "from": "send-broadcast-bulk", "to": "broadcast-queue", "kind": "data", "delta": "added", "label": "write results", "emphasis": "normal", "animated": false, "files": [] } ], "flows": [ { "id": "send-pipeline", "title": "Sending a broadcast", "summary": "The path a queued broadcast takes now, from enqueue to per-message results.", "delta": "modified", "participants": [ { "node": "queue-route", "label": "queue route" }, { "node": "broadcast-queue", "label": "Firestore" }, { "node": "send-broadcast-bulk", "label": "sendBroadcastBulk" }, { "node": "postmark", "label": "Postmark" } ], "messages": [ { "id": "enqueue", "from": "queue-route", "to": "broadcast-queue", "label": "enqueue broadcast job", "kind": "async", "delta": "modified", "animated": true, "files": [] }, { "id": "trigger", "from": "broadcast-queue", "to": "send-broadcast-bulk", "label": "onWrite trigger", "kind": "async", "delta": "added", "animated": true, "files": [] }, { "id": "suppressions-request", "from": "send-broadcast-bulk", "to": "postmark", "label": "GET suppression dump", "kind": "sync", "delta": "added", "animated": true, "files": [] }, { "id": "suppressions-response", "from": "postmark", "to": "send-broadcast-bulk", "label": "suppressed addresses", "kind": "return", "delta": "added", "animated": true, "note": "Fetched once per broadcast, not once per recipient.", "files": [] }, { "id": "batch-post", "from": "send-broadcast-bulk", "to": "postmark", "label": "POST /email/batch · 500 msgs", "kind": "sync", "delta": "added", "animated": true, "repeat": 4, "note": "One request per 500 recipients; four for this 2,000-recipient broadcast.", "files": [] }, { "id": "batch-results", "from": "postmark", "to": "send-broadcast-bulk", "label": "per-message results", "kind": "return", "delta": "added", "animated": true, "files": [] }, { "id": "write-results", "from": "send-broadcast-bulk", "to": "broadcast-queue", "label": "write results", "kind": "async", "delta": "added", "animated": true, "files": [] } ] } ], "stats": { "filesChanged": 14, "additions": 486, "deletions": 212, "chips": [ { "label": "Postmark calls", "value": "500× fewer", "tone": "hero" }, { "label": "New", "value": "4 units", "tone": "added" }, { "label": "Retired", "value": "2 units", "tone": "removed" } ] }, "views": [ { "id": "overview", "title": "Architecture — blast radius", "lens": "architecture", "summary": "Everything this change touches, across all three lanes.", "scope": { "kind": "all" }, "defaultOpen": true, "children": [ { "id": "new-batch-path", "title": "The new batch path", "lens": "architecture", "summary": "What replaced the per-recipient loop.", "scope": { "kind": "selection", "lanes": [], "nodes": [ "send-broadcast-bulk", "build-bulk-payload", "get-suppressed-emails", "broadcast-lib", "postmark" ], "edges": [ "bulk-to-payload", "bulk-to-suppressions", "bulk-to-lib", "suppressions-to-postmark", "bulk-to-postmark", "bulk-to-firestore" ], "flows": [] }, "defaultOpen": false, "children": [] }, { "id": "retired-path", "title": "What was retired", "lens": "architecture", "summary": "The single-send path, kept visible so a reviewer can confirm nothing else called it.", "scope": { "kind": "selection", "lanes": [], "nodes": [ "process-broadcast", "send-single-email" ], "edges": [ "firestore-to-process", "process-to-single", "single-to-postmark" ], "flows": [] }, "defaultOpen": false, "children": [] } ] }, { "id": "send-pipeline-view", "title": "Data flow — sending a broadcast", "lens": "data-flow", "scope": { "kind": "selection", "lanes": [], "nodes": [], "edges": [], "flows": [ "send-pipeline" ] }, "defaultOpen": false, "children": [] } ], "walkthrough": { "steps": [ { "id": "batches-of-500", "heading": "sendBroadcastBulk and buildBulkPayload added", "body": "Nothing loops over recipients any more. The sender works on a whole batch at a time.", "stage": { "kind": "view", "view": "overview" }, "focus": { "kind": "selection", "lanes": [], "nodes": [ "send-broadcast-bulk", "build-bulk-payload", "postmark" ], "edges": [], "messages": [] } }, { "id": "suppression-first", "heading": "getSuppressedEmails added before the send", "body": "It pulls the blocked addresses once, before any batch is built.", "stage": { "kind": "view", "view": "new-batch-path" }, "focus": { "kind": "selection", "lanes": [], "nodes": [ "get-suppressed-emails", "postmark" ], "edges": [], "messages": [] } }, { "id": "old-path-goes-dark", "heading": "processBroadcast and sendSingleEmail removed", "body": "sendBroadcastBulk does their job for whole batches.", "stage": { "kind": "view", "view": "overview" }, "focus": { "kind": "selection", "lanes": [], "nodes": [ "process-broadcast", "send-single-email" ], "edges": [], "messages": [] } }, { "id": "sequence-start-to-finish", "heading": "The send sequence gained 6 new steps", "body": "The queue write is the only step that was there before, and it now stamps the batch size.", "stage": { "kind": "flow", "flow": "send-pipeline" }, "focus": { "kind": "all" } }, { "id": "four-batch-calls", "heading": "Postmark now gets 500 emails per call", "body": "One call per batch, and Postmark answers with a result for each message.", "stage": { "kind": "flow", "flow": "send-pipeline" }, "focus": { "kind": "selection", "lanes": [], "nodes": [], "edges": [], "messages": [ "batch-post", "batch-results" ] } }, { "id": "blast-radius", "heading": "4 parts added, 2 removed, across 3 lanes", "body": "A 2,000-person broadcast used to make 2,000 calls to Postmark. It now makes 4.", "stage": { "kind": "view", "view": "overview" }, "focus": { "kind": "all" } } ] }, "layout": { "direction": "right", "laneOrder": [ "web", "functions", "external" ], "rank": { "queue-route": 0, "send-broadcast-bulk": 1, "postmark": 2 } } }