Relationships
↑ child of #2008#2021 feat(nurture): deliver to the swamp-club inbox on the same dedupe decision as email
Opened by keeb · 9/4/2026
Parent: the product-nurture epic. Adds the second channel.
The whiteboard says these go out as emails and broadcast messages in the app. The in-app half is almost entirely built already — this issue wires nurture to it without giving the dedupe decision a second owner.
The terminology trap, first
"Broadcast" on the whiteboard means "in-app message". In this codebase a
BroadcastMessage goes to everyone. Nurture is targeted. The correct call
is:
sendSystemMessage(deps, {
sender: { service: "web" },
kind: "nurture",
audience: { type: "users", userIds: [...] }, // NOT { type: "broadcast" }
dedupKey: nurtureRow._id,
...
})Using the broadcast arm would put a dormancy nudge on the site for every operative at once.
What ships
One decision, two deliveries. The nurture_send row created in Phase A is
the single decision record. The inbox delivery is a channel on that row, not a
second producer. Concretely, the ledger's _id becomes the dedupKey passed
to sendSystemMessage — and since the notifications collection carries a
unique (recipientId, dedupKey) index, the inbox side is idempotent by
construction even if the email side retries.
Per-flow channel matrix, controlled from /admin/nurture (A4). Proposed
defaults: celebratory and behavioural flows go to both; the two dormancy flows
go to email only at first, since an in-app message reaches someone who by
definition is not visiting.
Channel outcomes are independent. An email that defers does not block the inbox delivery, and vice versa — the row records both, and the panel renders both. A flow is "sent" when its enabled channels have each reached a terminal state.
No self-notification, no unknown recipients. sendSystemMessage already
drops both and reports the counts; surface those in the panel rather than
treating them as errors.
Verification
tests/app/nurture_inbox_channel_test.ts— one nurture row produces exactly one notification; a re-delivery produces zero; an email failure still leaves the inbox deliverysent.- An assertion that the broadcast arm is never reachable from the nurture producer.
- On the local stack: arm a flow with both channels, trigger it, and read the
message in
/inboxas the recipient.
Out of scope
The CLI hint (E1/E2), which reads the inbox this issue fills.
Open
No activity in this phase yet.
Sign in to post a ripple.