a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

don't trigger newsletter sends on firehose update events (#302)

The firehose UPDATE path was also running the newsletter claim/send logic.
Combined with the fact that the idempotency row in publication_post_sends
is only inserted when newsletters are enabled at upsert time, this meant
that editing an old post after newsletters were turned on for the
publication would mail the post out to subscribers retroactively.

Gate the newsletter block on event_type === "create" so only firehose
creates can fire sends. Metadata sync (indexed flag, bsky like counts,
bridgy deletion) still runs on updates as before.

Co-authored-by: Claude <noreply@anthropic.com>

authored by

Jared Pereira
Claude
and committed by
GitHub
(May 19, 2026, 6:27 PM EDT) 3e9583ca 21cab2c2

+57 -48
+2
appview/index.ts
··· 118 118 data: { 119 119 document_uri: evt.uri.toString(), 120 120 bsky_post_uri: record.value.postRef?.uri, 121 + event_type: evt.event, 121 122 }, 122 123 }); 123 124 if (record.value.publication) { ··· 285 286 data: { 286 287 document_uri: evt.uri.toString(), 287 288 bsky_post_uri: record.value.bskyPostRef?.uri, 289 + event_type: evt.event, 288 290 }, 289 291 }); 290 292
+5 -1
app/api/inngest/client.ts
··· 42 42 schema: staticSchema<{ documentUris?: string[] }>(), 43 43 }), 44 44 appviewSyncDocumentMetadata: eventType("appview/sync-document-metadata", { 45 - schema: staticSchema<{ document_uri: string; bsky_post_uri?: string }>(), 45 + schema: staticSchema<{ 46 + document_uri: string; 47 + bsky_post_uri?: string; 48 + event_type?: "create" | "update"; 49 + }>(), 46 50 }), 47 51 userWriteRecordsToPds: eventType("user/write-records-to-pds", { 48 52 schema: staticSchema<{
+50 -47
app/api/inngest/functions/sync_document_metadata.ts
··· 20 20 triggers: [events.appviewSyncDocumentMetadata], 21 21 }, 22 22 async ({ event, step }) => { 23 - const { document_uri, bsky_post_uri } = event.data; 23 + const { document_uri, bsky_post_uri, event_type } = event.data; 24 24 25 25 const did = new AtUri(document_uri).host; 26 26 ··· 57 57 .select(); 58 58 }); 59 59 60 - const broadcast = await step.run( 61 - "maybe-claim-newsletter-broadcast", 62 - async () => { 63 - const { data: docInPub } = await supabaseServerClient 64 - .from("documents_in_publications") 65 - .select("publication") 66 - .eq("document", document_uri) 67 - .maybeSingle(); 68 - const publication_uri = docInPub?.publication; 69 - if (!publication_uri) return { skipped: "no_publication" as const }; 60 + // Only fire newsletter broadcasts on first-time document creation. An 61 + // update to an existing post must never trigger a send — otherwise editing 62 + // an old post after newsletters were enabled would mail subscribers a post 63 + // they never signed up for. 64 + if (event_type === "create") { 65 + const broadcast = await step.run( 66 + "maybe-claim-newsletter-broadcast", 67 + async () => { 68 + const { data: docInPub } = await supabaseServerClient 69 + .from("documents_in_publications") 70 + .select("publication") 71 + .eq("document", document_uri) 72 + .maybeSingle(); 73 + const publication_uri = docInPub?.publication; 74 + if (!publication_uri) return { skipped: "no_publication" as const }; 70 75 71 - const { data: settings } = await supabaseServerClient 72 - .from("publication_newsletter_settings") 73 - .select("enabled") 74 - .eq("publication", publication_uri) 75 - .maybeSingle(); 76 - if (!settings?.enabled) { 77 - return { skipped: "newsletter_not_enabled" as const }; 78 - } 76 + const { data: settings } = await supabaseServerClient 77 + .from("publication_newsletter_settings") 78 + .select("enabled") 79 + .eq("publication", publication_uri) 80 + .maybeSingle(); 81 + if (!settings?.enabled) { 82 + return { skipped: "newsletter_not_enabled" as const }; 83 + } 79 84 80 - // Composite PK on (publication, document) is the idempotency guard: 81 - // re-runs on document updates and races with publishToPublication 82 - // both no-op here. 83 - const { data: inserted } = await supabaseServerClient 84 - .from("publication_post_sends") 85 - .upsert( 86 - { 87 - publication: publication_uri, 88 - document: document_uri, 89 - status: "pending", 90 - }, 91 - { onConflict: "publication,document", ignoreDuplicates: true }, 92 - ) 93 - .select(); 94 - if (!inserted || inserted.length === 0) { 95 - return { skipped: "already_sent_or_pending" as const }; 96 - } 97 - return { claimed: true as const, publication_uri }; 98 - }, 99 - ); 100 - 101 - if ("claimed" in broadcast) { 102 - await step.sendEvent("send-newsletter-broadcast", { 103 - name: "newsletter/post.send.requested", 104 - data: { 105 - publication_uri: broadcast.publication_uri, 106 - document_uri, 85 + const { data: inserted } = await supabaseServerClient 86 + .from("publication_post_sends") 87 + .upsert( 88 + { 89 + publication: publication_uri, 90 + document: document_uri, 91 + status: "pending", 92 + }, 93 + { onConflict: "publication,document", ignoreDuplicates: true }, 94 + ) 95 + .select(); 96 + if (!inserted || inserted.length === 0) { 97 + return { skipped: "already_sent_or_pending" as const }; 98 + } 99 + return { claimed: true as const, publication_uri }; 107 100 }, 108 - }); 101 + ); 102 + 103 + if ("claimed" in broadcast) { 104 + await step.sendEvent("send-newsletter-broadcast", { 105 + name: "newsletter/post.send.requested", 106 + data: { 107 + publication_uri: broadcast.publication_uri, 108 + document_uri, 109 + }, 110 + }); 111 + } 109 112 } 110 113 111 114 if (!bsky_post_uri) {