Skip to content

feat(convex/broadcast): audience-export pipeline for PRO launch#3431

Merged
koala73 merged 4 commits into
mainfrom
feat/pro-launch-broadcast-pipeline
Apr 26, 2026
Merged

feat(convex/broadcast): audience-export pipeline for PRO launch#3431
koala73 merged 4 commits into
mainfrom
feat/pro-launch-broadcast-pipeline

Conversation

@koala73

@koala73 koala73 commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Why

Second PR in the WorldMonitor PRO launch broadcast chain. Builds on the customers.normalizedEmail index landed in #3424 (now on main).

Goal: build the deduped waitlist audience and push contacts to a Resend Audience for one-shot launch broadcasting.

Dedup formula

registrations − emailSuppressions − customers

Any user who's been through Dodo checkout (active, cancelled, expired — all statuses) is excluded. Pitching paid users a "buy PRO!" email is the worst-case failure mode of the entire launch and the reason this PR exists.

What this adds

convex/broadcast/audienceExport.ts — three internal queries + one internal action:

  • getSuppressedEmails — snapshot of emailSuppressions.normalizedEmail (all bounces, complaints, manual)
  • getPaidEmails — snapshot of customers.normalizedEmail (anyone who paid at any point)
  • getRegistrationsPage — cursor-paginated page of registrations rows
  • exportProLaunchAudience — orchestrates: materializes the two exclusion sets, paginates registrations, posts each non-excluded email to Resend's POST /audiences/{id}/contacts

Action behavior

  • Idempotent on re-run: Resend's 422 already_exists is counted as alreadyExists, not failed. Re-running the same page is safe.
  • Cursor-paginated: returns continueCursor for the next call. Loop until isDone: true.
  • Dry-run mode: dryRun: true returns counts without calling Resend — verify exclusion math before the first real send.
  • Required env: RESEND_API_KEY (already set; same key used by subscriptionEmails.ts)
  • Default page size: 200 — at Resend's ~10 req/s limit that's ~20s of wall time, comfortably under the Convex 10-min action cap.

Output shape

{
  upserted: number;           // newly added to the Resend audience
  suppressedSkipped: number;  // hit emailSuppressions
  paidSkipped: number;        // hit customers (paid)
  alreadyExists: number;      // already in the Resend audience (re-run)
  failed: number;             // non-422 Resend errors
  emptyEmail: number;         // registrations with empty normalizedEmail
  isDone: boolean;
  continueCursor: string;
  pageProcessed: number;
}

Codegen catch-up

_generated/api.d.ts now declares both:

Convex regenerates api.d.ts identically on convex dev / convex deploy. No drift risk.

Verification

  • npx tsc --noEmit -p convex/tsconfig.json → clean
  • All queries and the action are internal only — never exposed to clients
  • Dry-run path tested: skips the RESEND_API_KEY requirement so the exclusion math can be verified independently of the Resend infra

Operational sequence

After this PR merges and Convex deploys:

# Confirm backfill is complete (from #3424)
npx convex run payments/backfillCustomerNormalizedEmail:countPending
# Expect: { pending: 0 }

# Dry-run to verify the dedup math
npx convex run broadcast/audienceExport:exportProLaunchAudience \
  '{"audienceId":"aud_xxx","dryRun":true}'

# Real run — loop until isDone:true
npx convex run broadcast/audienceExport:exportProLaunchAudience \
  '{"audienceId":"aud_xxx"}'
npx convex run broadcast/audienceExport:exportProLaunchAudience \
  '{"audienceId":"aud_xxx","cursor":"<continueCursor from previous>"}'
# ... repeat until isDone:true

Then verify in Resend dashboard that the audience contact count matches upserted + alreadyExists (across all calls).

Out of scope (future PRs)

Known caveats

  • getSuppressedEmails and getPaidEmails use .collect() — bounded by Convex's 16,384-doc read limit per query. Both tables are well under that today (low thousands). Comment in code flags the limit; if either grows, switch to streamed/paginated counts.
  • No rate-limit handling for Resend — relies on Resend's documented 10 req/s and the 200/page default to stay under it. If Resend returns 429 we count as failed. Acceptable for the launch send cadence (250→500/wk in the canary plan); revisit if blast volume exceeds 10k/hour.

Stacked on feat/customers-normalized-email-index — needs that branch's
customers.normalizedEmail field + index + backfill before this action
can produce a clean dedup.

Adds:
- convex/broadcast/audienceExport.ts: paginated internalAction plus
  three internalQueries that build the deduped audience and push
  contacts to a Resend Audience via the Contacts API.

Dedup formula: registrations − emailSuppressions − customers. Any
user who's been through Dodo checkout (active, cancelled, expired)
is excluded — pitching paid users a "buy PRO!" email is the worst-
case failure mode the launch is trying to avoid.

Idempotent on re-run: Resend's 422 already_exists response is counted
as `alreadyExists`, not `failed`. Cursor-paginated for safe resume
mid-export. Dry-run mode (counts only, no Resend calls) for verifying
exclusion math before the first real send.

Codegen catch-up:
- _generated/api.d.ts updated to declare broadcast/audienceExport
  (this file cross-references its own siblings via internal.* and
  needs the typed reference).
- Also catches up payments/backfillCustomerNormalizedEmail from the
  base PR — that file didn't cross-reference so its CI passed
  without the codegen update; adding now so future calls into it
  resolve correctly.
- Convex regenerates identically on deploy — no drift risk.

Operational:
  npx convex run broadcast/audienceExport:exportProLaunchAudience \
    '{"audienceId":"aud_xxx"}'
  # Loop, passing continueCursor from each response, until isDone:true
@vercel

vercel Bot commented Apr 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Apr 26, 2026 11:14am

Request Review

@greptile-apps

greptile-apps Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces convex/broadcast/audienceExport.ts, a cursor-paginated internal action that builds a deduplicated launch-email audience (registrations − emailSuppressions − customers) and pushes qualifying contacts to a Resend Audience, along with a codegen catch-up to api.d.ts.

  • P1 — 422 error conflation: Both 409 and 422 HTTP responses from Resend are counted as alreadyExists without inspecting the response body. Resend uses 422 for all validation failures (invalid email format, schema mismatches, etc.), so genuine errors would be silently absorbed into the alreadyExists counter rather than failed, giving operators a misleading success signal.
  • P2 — Missing User-Agent: The fetch call to Resend omits the User-Agent header required by AGENTS.md for all server-side fetch calls.
  • P2 — PII in logs: Raw email addresses are emitted to Convex dashboard logs in the error path.

Confidence Score: 3/5

Not safe to merge as-is — the 422 conflation can silently mis-report export failures as successes, giving operators incorrect data before the launch send.

One P1 logic bug (422 conflation) directly affects the correctness of the export stats that operators rely on to verify the launch is clean. The dedup logic itself is sound, but the Resend error handling needs fixing before production use.

convex/broadcast/audienceExport.ts — specifically the 409/422 error handling block and the console.error PII log.

Security Review

  • PII exposure in logs (convex/broadcast/audienceExport.ts line 199): raw email addresses are written to Convex dashboard logs on every Resend error, making user contact details observable to anyone with log access.

Important Files Changed

Filename Overview
convex/broadcast/audienceExport.ts New internal action + 3 queries implementing the PRO-launch audience export pipeline; P1 issue with 422 error conflation, plus P2 concerns on PII logging, missing User-Agent, and dry-run counter semantics.
convex/_generated/api.d.ts Codegen catch-up adding type declarations for broadcast/audienceExport and payments/backfillCustomerNormalizedEmail; mechanical and correct.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI (npx convex run)
    participant Action as exportProLaunchAudience
    participant Q1 as getSuppressedEmails
    participant Q2 as getPaidEmails
    participant Q3 as getRegistrationsPage
    participant Resend as Resend API

    CLI->>Action: audienceId, cursor?, numItems?, dryRun?
    Action->>Q1: runQuery getSuppressedEmails
    Action->>Q2: runQuery getPaidEmails
    Q1-->>Action: suppressed[]
    Q2-->>Action: paid[]
    Action->>Q3: runQuery getRegistrationsPage(cursor, numItems)
    Q3-->>Action: page{page[], continueCursor, isDone}

    loop for each registration in page
        alt email empty
            Action->>Action: emptyEmail++
        else email in suppressedSet
            Action->>Action: suppressedSkipped++
        else email in paidSet
            Action->>Action: paidSkipped++
        else dryRun
            Action->>Action: upserted++ (dry)
        else live
            Action->>Resend: POST /audiences/{id}/contacts
            alt 200 OK
                Resend-->>Action: success → upserted++
            else 409 or 422
                Resend-->>Action: conflict → alreadyExists++
            else other error
                Resend-->>Action: error → failed++
            end
        end
    end

    Action-->>CLI: ExportStats{upserted, skipped counts, isDone, continueCursor}
Loading

Reviews (1): Last reviewed commit: "feat(convex/broadcast): audience-export ..." | Re-trigger Greptile

Comment thread convex/broadcast/audienceExport.ts Outdated
Comment on lines +190 to +194
} else if (res.status === 409 || res.status === 422) {
// Resend returns 422 with `name: "validation_error"` and message
// mentioning duplicate when the email is already in the audience.
// Treat as already-imported, not a failure.
stats.alreadyExists++;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 422 conflates duplicate-already-exists with genuine validation errors

Both 409 and 422 are counted as alreadyExists, but Resend uses 422 for any validation failure — not just duplicate contacts. A malformed email address, a missing required field, or a payload schema mismatch would also return 422 yet be silently counted as alreadyExists instead of failed. When the export finishes, the operator would see a healthy-looking alreadyExists count while those emails were never actually added to the audience.

The body should be parsed to confirm the error is specifically already_exists before routing to that counter.

Comment thread convex/broadcast/audienceExport.ts Outdated
Comment on lines +176 to +186
const res = await fetch(
`${RESEND_API_BASE}/audiences/${encodeURIComponent(audienceId)}/contacts`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ email, unsubscribed: false }),
},
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing User-Agent header on Resend fetch

AGENTS.md requires "Always include User-Agent header in server-side fetch calls," but the Resend contacts POST omits it. Add a descriptive User-Agent to the headers object alongside Content-Type and Authorization.

Context Used: AGENTS.md (source)

Comment thread convex/broadcast/audienceExport.ts Outdated
Comment on lines +196 to +201
stats.failed++;
const body = await res.text().catch(() => "<no body>");
console.error(
`[exportProLaunchAudience] Resend ${res.status} for ${email}: ${body}`,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Raw email PII logged in error path

Line 199 emits for ${email}: ${body} into Convex's dashboard logs. Every Resend error (transient 5xx, network timeout, etc.) will write the contact's email address to the observable log stream. Consider logging only the HTTP status and an anonymised hash or redacted placeholder instead of the raw address.

Comment on lines +171 to +174
if (dry) {
stats.upserted++;
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 upserted counter semantics differ between dry-run and live run

In dry-run mode, stats.upserted++ is incremented for every email that passes the dedup filters, but in the live path upserted only increments on a successful 200 OK from Resend. The same field name therefore means two different things depending on dryRun. An operator comparing dry-run vs. live totals to validate the dedup math will see a meaningful discrepancy if any Resend calls fail or return 409/422. Consider using a dedicated wouldUpsert counter for dry mode, or at minimum calling this out clearly in the returned stats key.

All three findings valid; verified line numbers match commit 7a440c1
and the Resend API claim against current docs at
https://resend.com/docs/api-reference/contacts/create-contact.

P1 #1 — Paid-customer exclusion fails open when normalizedEmail missing:
`getPaidEmails` now derives the join key from `customers.email` (the
existing `email.trim().toLowerCase()` convention) when `normalizedEmail`
is unset. The backfill becomes a perf optimization — a missed/incomplete
run can no longer silently leak paid users into the launch audience.

P1 #2 — Wrong Resend endpoint:
Switched from `POST /audiences/{id}/contacts` (legacy, may still resolve
but no longer the canonical 2026 path) to `POST /contacts` with
`segments: [{ id: segmentId }]` in the body, per current Resend docs.
Renamed the action arg from `audienceId` to `segmentId` to match
Resend's renaming of Audiences → Segments. Updated JSDoc usage examples.

P1 #3 — 409/422 catch-all masking validation errors:
Added `isDuplicateContactError()` helper that parses the 422 body and
matches on `name` (`*_already_exists` / `*_duplicate`) and `message`
(`/already (exists|in)|duplicate/`). Only duplicate-shaped responses
increment `alreadyExists`; everything else (missing segment, invalid
email, unauthorized field, etc.) increments `failed` and logs the
parsed body. Also dropped the speculative 409 branch — Resend doesn't
use 409 for contacts; 422 is the real status.
@koala73

koala73 commented Apr 26, 2026

Copy link
Copy Markdown
Owner Author

All three P1s addressed in ebf2bf8. Verified each citation against the actual file at commit 7a440c1, and verified the Resend API claim against the current docs at https://resend.com/docs/api-reference/contacts/create-contact before changing the endpoint.

P1 #1 — Paid-customer exclusion failing open when normalizedEmail is missing: getPaidEmails now falls back to deriving the join key from customers.email (row.email.trim().toLowerCase(), matching the convention at every write site) when normalizedEmail isn't set. The backfill is now a perf optimization, not a correctness requirement — a missed or incomplete run can no longer silently leak paid users into the launch audience.

P1 #2 — Legacy audience-scoped endpoint: Switched from POST /audiences/{id}/contacts to POST /contacts with segments: [{ id: segmentId }] in the body, per the current Resend docs. Renamed the action arg from audienceId to segmentId to match Resend's Audiences → Segments rename, and updated the JSDoc usage examples.

P1 #3 — 409/422 catch-all masking validation errors: Added isDuplicateContactError() that parses the 422 body and matches on name (*_already_exists / *_duplicate) and message (/already (exists|in)|duplicate/). Only duplicate-shaped 422s now increment alreadyExists; missing-segment / invalid-email / unauthorized-field errors increment failed and log the parsed body so misconfiguration surfaces instead of masquerading as duplicates. Also dropped the speculative 409 branch — Resend doesn't use 409 for contacts.

Round-2 review finding: previous fix counted POST /contacts 422 duplicates
as alreadyExists without verifying the contact ended up in OUR segment.
Resend's global-Contacts model means a 422 duplicate could mean the
contact exists in a different segment or no segment at all — and the
`segments` field on the duplicate path is not applied — so existing
global contacts could be silently OMITTED from the launch segment while
the export reports success.

New `upsertContactToSegment(apiKey, email, segmentId)` helper does a
deterministic two-step:

  1. POST /contacts with `segments: [{ id }]`. Brand-new globally →
     creates and assigns in one call → outcome=created.
  2. If step 1 returns a duplicate-shaped 422, the contact exists
     globally. Disambiguate with POST /contacts/{email}/segments/{id}:
       - 2xx → was global-only or in another segment, now linked here
         → outcome=linkedExisting
       - 422 duplicate-shaped → was already in this segment
         → outcome=alreadyInSegment
       - anything else → outcome=failed

Stats updated:
- `upserted` now counts BOTH `created` AND `linkedExisting` (anything
  that ended up in the segment via this call).
- `linkedExisting` added as a separate diagnostic counter — operator
  can compare to verify how many were pre-existing global contacts.
- `alreadyExists` now strictly means "was already in this segment
  before this call" — never inferred from a global-duplicate response
  without segment-membership verification.
@koala73

koala73 commented Apr 26, 2026

Copy link
Copy Markdown
Owner Author

Valid catch — addressed in f6c079c. The previous fix counted POST /contacts 422 duplicates as alreadyExists without verifying segment membership; with Resend's global-contacts model that's exactly the silent-omission failure you described.

New upsertContactToSegment(apiKey, email, segmentId) helper does a deterministic two-step:

  1. POST /contacts with segments: [{ id }]. Brand-new globally → creates and assigns in one call → outcome = created.
  2. If step 1 returns a duplicate-shaped 422 (global contact exists), explicitly POST /contacts/{email}/segments/{segmentId}:
    • 2xx → was global-only or in another segment, now linked here → outcome = linkedExisting
    • 422 duplicate-shaped → was already in this segment → outcome = alreadyInSegment
    • anything else → outcome = failed

Stats reshaped:

  • upserted now sums both created and linkedExisting — anything that ended up in the segment as a result of this call.
  • linkedExisting added as a separate diagnostic counter so the operator can see how many were pre-existing global contacts that got linked into the launch segment.
  • alreadyExists now strictly means "was already in this segment before this call" — never inferred from a global-duplicate response without explicit segment-membership verification.

Net diff: +120/−39 on one file. Typecheck clean.

Stale: P1 "422 conflates duplicate with validation errors" was cited
on commit 7a440c1; addressed in ebf2bf8 — replied as resolved.

Three valid P2s on current code, all fixed:

P2 — Missing User-Agent header on Resend fetches:
AGENTS.md:185 requires User-Agent on every server-side fetch. Added
USER_AGENT constant and applied to both /contacts and
/contacts/{email}/segments/{id} POSTs.

P2 — Raw email PII written to Convex dashboard logs:
The action's failure-path console.error interpolated `${email}` into
log lines that anyone with project access can read. Added maskEmail()
helper (`[email protected]` → `jo******@example.com`) and applied
to the failure-path log.

P2 — `upserted` semantics differ between dry-run and live:
The previous draft incremented stats.upserted in BOTH dry-run (every
email passing dedup) and live (only successful Resend calls). Operators
comparing dry-run to live totals to validate dedup math would see a
spurious discrepancy on any failure / linkedExisting / alreadyInSegment.

Reshaped ExportStats:
- Dry-run-only: `wouldUpsertAfterDedup` — count of emails that pass
  the dedup filters and would be attempted on a live run.
- Live-only: `upserted`, `linkedExisting`, `alreadyExists`, `failed`
  — strictly result of Resend interactions; all zero in dry-run mode.
- Shared (dedup-only): `suppressedSkipped`, `paidSkipped`, `emptyEmail`.

Operator can now compare dry-run `wouldUpsertAfterDedup` to live
`upserted + alreadyExists` and any divergence is a real Resend issue,
not a counter-semantics artefact.
@koala73

koala73 commented Apr 26, 2026

Copy link
Copy Markdown
Owner Author

Round 3 — addressed in cf091ee.

Stale (no action): the P1 "422 conflates duplicate with validation errors" was cited on commit 7a440c1ce; addressed in ebf2bf849 (this PR's first round-fix). Verified: the current code parses the 422 body via isDuplicateContactError() and only routes duplicate-shaped responses to the duplicate counter.

Three valid P2s, all fixed:

Missing User-Agent header (AGENTS.md:185): Added USER_AGENT constant and applied to both POST /contacts and POST /contacts/{email}/segments/{id}.

Raw email PII in Convex dashboard logs: Added maskEmail() helper ([email protected]jo******@example.com) and applied to the failure-path console.error. Convex dashboard logs are observable to anyone with project access — raw waitlist addresses must not land there.

upserted semantics differing between dry-run and live: Real concern — operators comparing the two would see spurious diff on every failure / linkedExisting / alreadyInSegment. Reshaped ExportStats:

  • Dry-run only: wouldUpsertAfterDedup (emails that pass dedup and would be attempted live)
  • Live only: upserted, linkedExisting, alreadyExists, failed (strictly result of Resend interactions; all zero in dry-run)
  • Shared: suppressedSkipped, paidSkipped, emptyEmail (dedup-only, no Resend dependence)

Operators can now compare dry-run's wouldUpsertAfterDedup to live's upserted + alreadyExists and any divergence is a real Resend issue, not a counter-semantics artefact.

Net diff: +46/−8 on one file. Typecheck clean.

@koala73
koala73 merged commit ca50474 into main Apr 26, 2026
10 checks passed
@koala73
koala73 deleted the feat/pro-launch-broadcast-pipeline branch April 26, 2026 11:15
koala73 added a commit that referenced this pull request Apr 26, 2026
…ed content (#3434)

* feat(convex/broadcast): PRO-launch broadcast trigger + metrics + locked content

Third PR in the WorldMonitor PRO-launch broadcast chain. Builds on the
audience-export pipeline (#3431, merged) to deliver the actual send +
canary kill-gate observability.

Adds:
- convex/schema.ts: new `broadcastEvents` table with `by_webhookEventId`
  (idempotency) and `by_broadcast_event` (aggregation) indexes. Stores
  no recipient email — Convex dashboard logs are observable; we rely on
  Resend's `email_id` for traceability via their dashboard.

- convex/broadcast/proLaunchEmailContent.ts: source-of-truth locked
  email content (subject, from, reply-to, plain-text fallback, HTML,
  CAN-SPAM physical-address footer placeholder). Copy was finalised
  with Elie 2026-04-26; future re-sends edit this file.

- convex/broadcast/metrics.ts:
  - `recordBroadcastEvent` (internalMutation): idempotent on svix-id,
    drops untracked event types defensively.
  - `getBroadcastStats` (internalQuery): live aggregate per broadcast
    with bounce / complaint / open / click rates and `bouncesOver-
    Threshold` / `complaintsOverThreshold` flags driven by the project
    plan's kill-gates (>4% bounce, >0.08% complaint).

- convex/broadcast/sendBroadcast.ts:
  - `createProLaunchBroadcast` (internalAction): POST /broadcasts to
    Resend with the locked content + given segment_id. Returns the
    Resend broadcast id.
  - `sendProLaunchBroadcast` (internalAction): POST /broadcasts/{id}/send,
    optional `scheduled_at` ISO timestamp for time-of-day scheduling.

- convex/resendWebhookHandler.ts: extended to also forward any tracked
  event with a `broadcast_id` to `recordBroadcastEvent` BEFORE the
  existing suppression handling. Metrics record failure does NOT 500
  the webhook — suppression is the more important path and Resend
  retries would amplify the problem.

Codegen catch-up:
- _generated/api.d.ts: declare broadcast/metrics, broadcast/sendBroadcast,
  broadcast/proLaunchEmailContent. Convex regenerates identically on
  next deploy — no drift risk.

Operational sequence (after merge + deploy):
  # Pre-flight (one-time)
  - Configure Dodo coupon EARLYWM30 (30% off, 30-day expiry, usage cap)
  - Set up `[email protected]` sender on Resend domain
  - Manually create canary + main Resend Segments in dashboard
  - Run `payments/backfillCustomerNormalizedEmail:backfill` until done

  # Populate the audience (uses #3431's action)
  npx convex run broadcast/audienceExport:exportProLaunchAudience \
    '{"segmentId":"<canary>","dryRun":true}'  # verify dedup
  npx convex run broadcast/audienceExport:exportProLaunchAudience \
    '{"segmentId":"<canary>"}'  # populate canary first
  npx convex run broadcast/audienceExport:exportProLaunchAudience \
    '{"segmentId":"<main>"}'  # then the rest

  # Fire the canary
  npx convex run broadcast/sendBroadcast:createProLaunchBroadcast \
    '{"segmentId":"<canary>","nameSuffix":"Canary 250"}'  # → broadcastId
  npx convex run broadcast/sendBroadcast:sendProLaunchBroadcast \
    '{"broadcastId":"<id>"}'

  # Watch the kill-gates
  npx convex run broadcast/metrics:getBroadcastStats \
    '{"broadcastId":"<id>"}'  # poll until clean or threshold trips

  # Repeat for main segment if canary clean

Out of scope (deferred):
- Programmatic segment splitting (canary-vs-main from a single source).
  For first send, segments are split manually in Resend dashboard.
- Live monitoring loop / dashboard.
- Send-time RFC 8058 List-Unsubscribe-Post header verification — must
  be checked by inspecting raw headers on a test send before canary.

* copy(proLaunch): add 50K GitHub stars beat to the build-pace line

Repo just crossed 50K stars (verified: 52,668 at time of commit).
Combining with the existing "more than half didn't exist 45 days ago"
sentence makes the build-pace claim carry two proof points (code volume
+ community signal) in one beat instead of standing alone.

Updated both the plain-text and HTML versions of the locked launch
content. Bare https://github.com/koala73/worldmonitor URL in plain
text, hyperlinked "open-source repo" in HTML.

* fix(broadcast): address PR review (P1×3)

Three P1s on PR #3434, all valid:

P1 — `rawPayload: event.data` leaks recipient email PII:
The schema's "no recipient email stored" claim was false — Resend's
`event.data` includes `to: string[]`, `from`, `subject`. Anyone with
Convex dashboard access could enumerate the entire waitlist.

Fix: removed `rawPayload` from the `broadcastEvents` table entirely.
Identifier metadata (webhookEventId, broadcastId, emailMessageId,
eventType, occurredAt) is enough for canary metrics and Resend
dashboard cross-reference. Updated the schema comment + handler to
not pass event.data through.

P1 — Placeholder physical address would ship if not replaced:
`createProLaunchBroadcast` reads `PRO_LAUNCH_PHYSICAL_ADDRESS` and
sends it to Resend verbatim. One missed manual step → CAN-SPAM
non-compliant launch email.

Fix: hard-throw at the top of `createProLaunchBroadcast` if the
constant still contains "TBD" or "placeholder". Operator must edit
`proLaunchEmailContent.ts` with a real address before any send.

P1 — `getBroadcastStats` `.collect()` would fail on main-segment scale:
Counting events per type via `.collect()` on `broadcastEvents`
exceeds Convex's 16,384-doc per-query read limit the moment a 30k+
recipient send accumulates `email.delivered` events. Stats query
breaks → main-send monitoring breaks → kill-gates can't be evaluated.

Fix: pre-aggregate via a new `broadcastEventCounts` table, one row
per (broadcastId, eventType). `recordBroadcastEvent` upserts the
counter on every successful (idempotent) insert, gated on
insert-success so duplicate webhook deliveries can't inflate the
count. `getBroadcastStats` now reads N counts (N = tracked event
types = 8) — constant time regardless of broadcast size.

Schema deltas:
- broadcastEvents: dropped `rawPayload: v.any()` field
- broadcastEventCounts: new table with `by_broadcast_event` index

* fix(resendWebhookHandler): drop redundant svixId guard

Greptile P2 round 4: verifySignature already returns false if svix-id
is absent, and the handler 401s above on invalid signature. The
post-verify if(svixId) was dead code — would never fire, but if it
ever did, it silently swallowed the broadcast event. Replaced with
a non-null assertion + comment explaining the invariant.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant