Skip to content

feat(convex-email): finish the Convex component - #126

Merged
leoisadev1 merged 2 commits into
mainfrom
posthog-code/finish-convex-component
Jul 6, 2026
Merged

leoisadev1 merged 2 commits into
mainfrom
posthog-code/finish-convex-component

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Jul 6, 2026 •

Copy link
Copy Markdown
Member

Part of #106.

The package turned out to be in better shape than the empty issue suggested — the queue, retries, fallback plumbing, idempotency, test mode, cron recovery, and cleanup all exist and work. What was actually unfinished, from a full audit of the package against its README and docs page:

What was missing

  • Delivery status tracking. The README promises "reactive delivery state", but webhooks were only appended to the event log — nothing on the email row ever reflected delivered/bounced/complained. This was the biggest promised-but-missing piece.
  • Narrow webhook parsing. Generic webhooks only matched messageId/message_id, so Postmark (MessageID) and Mailgun (event-data.message.headers["message-id"]) payloads could never link back to a stored email.
  • enqueueBatch perf bug. The config document was read once per message inside the loop instead of once per batch.
  • setConfig merge trap. Patch-merge semantics meant a config field could never be cleared (the validator can't carry an explicit undefined), so a stale defaultFrom or cleanupAfterDays was permanent.
  • Untyped client. Every ConvexEmail method returned unknown.
  • Test gaps. No coverage for the retry/backoff path to terminal failure, cancel, missing-from, batch happy path, in-batch idempotency, or any webhook-to-email linking.

What this PR does

  • Normalizes provider webhook event names (Resend email.*, Postmark RecordType, Mailgun event-data, common generic keys) onto a per-email deliveryStatus of delivered / bounced / complained, plus deliveredAt. Only permanent failures count as bounced: Mailgun failed events are gated on severity: "permanent" and Postmark Bounce records on a permanent Type (HardBounce, BadEmailAddress, ManuallyDeactivated) — soft/temporary failures stay stored-but-unmapped so a retried send can still land as delivered. Bounces and complaints are sticky against delivered (out-of-order webhook retries never overwrite them with a late "delivered"); between themselves the most recent webhook wins. Unknown events (opens, clicks) stay in the event history without touching deliveryStatus.
  • Stores the normalized event on webhookDeliveries and widens generic message-id extraction so Postmark- and Mailgun-shaped payloads link to their emails. Postmark's numeric uppercase ID now serves as the dedupe delivery id instead of falling back to a body hash.
  • Hoists the config read in enqueueBatch to one read per batch (enqueueEmail still reads it standalone) — same shape as the unmerged fix on tembo/optimize-performance-bottlenecks.
  • Changes setConfig to replace the stored config document; omitted fields are now cleared, documented in README and docs.
  • Types the client surface: send → Promise<string>, status → Promise<ConvexEmailDoc | null>, etc., with new ConvexEmailDoc, ConvexEmailEventDoc, and ConvexEmailDeliveryStatus exports.
  • Adds 17 tests covering the above (35 total, all green) and updates the README plus the fumadocs page to match the finished behavior exactly.

Deliberately not done

  • No changeset. .changeset/config.json sets privatePackages: { version: false, tag: false }, and I verified locally that changeset version neither bumps nor consumes a changeset referencing this package — it would linger in .changeset/ forever and keep triggering the release workflow's path filter. No merged changeset has referenced convex-email since it went private in Version packages #84. When the package goes public for npm Trusted Publishing, this work should ride in that first public version bump.
  • No provider-native batch sending in the worker. The component's sendBatch is a queue-level batch (one durable row per message); per-provider batch APIs are the SDK's concern.
  • No fallback-at-send integration test. All real adapter kinds resolve credentials from env at client build time, so there is no config-constructible adapter that fails at send without network. The fallback logic itself lives in and is tested by @opencoredev/email-sdk.
  • No per-provider signature verification helpers. The docs already delegate verification to the app-mounted route's verify callback by design.

Maintainer notes

  • No codegen or deployment needed. The committed _generated stubs derive their types from schema.ts (DataModelFromSchemaDefinition) and use anyApi, so the schema additions (emails.deliveryStatus, emails.deliveredAt, webhookDeliveries.event) flow through without regen. All new fields are optional, so existing deployments need no migration.
  • setConfig is a behavior change (replace vs merge) — safe while the package is private/unpublished, and now documented.

Validation

  • packages/convex-email: check-types, build, bun test → 35 pass / 0 fail (repeated runs)
  • apps/fumadocs: types:check passes
  • .launch-smoke/convex-email-smoke.test.ts passes against the rebuilt package
  • bunx oxlint clean on all touched files

Created with PostHog Code

Close out the remaining gaps in @opencoredev/convex-email:

- Track delivery state from webhooks: provider event names (Resend
  email.*, Postmark RecordType, Mailgun event-data, and common generic
  keys) are normalized onto a per-email deliveryStatus of delivered,
  bounced, or complained, plus deliveredAt. Bounces and complaints are
  sticky so out-of-order webhook retries never hide a bounce.
- Widen generic webhook parsing: MessageID (Postmark), event-data.id
  and event-data.message.headers["message-id"] (Mailgun) now link
  deliveries to stored emails.
- Fix the enqueueBatch perf bug: read the shared config document once
  per batch instead of once per message; enqueueEmail still reads it
  itself when called standalone.
- Make setConfig replace the stored config instead of patch-merging,
  so omitted fields are cleared; merge semantics made it impossible to
  unset defaultFrom or cleanupAfterDays.
- Type the ConvexEmail client surface: send/sendBatch/status/
  listEvents/cancel/getConfig/processWebhook now return typed results
  (new ConvexEmailDoc, ConvexEmailEventDoc, ConvexEmailDeliveryStatus
  exports) instead of unknown.
- Add lifecycle tests: batch enqueue with config defaults, in-batch
  idempotency dedupe, retry backoff to terminal failure, cancel,
  missing-from error, config replace semantics, and webhook delivery
  status for Resend, Postmark, and Mailgun shapes.
- Update the README and docs page to match.

The hand-maintained _generated stubs derive types from schema.ts, so
the schema additions need no Convex codegen or deployment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Generated-By: PostHog Code
Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
email-sdk-fumadocs Ready Ready Preview, Comment Jul 6, 2026 3:05pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR completes the Convex email component delivery-state workflow. The main changes are:

  • Adds per-email deliveryStatus and deliveredAt fields.
  • Normalizes Resend, Postmark, Mailgun, and generic webhook events.
  • Links webhook payloads to stored emails by provider message id.
  • Changes setConfig to replace stored config instead of merging it.
  • Adds typed client return values and exported document types.
  • Expands tests and documentation for queueing, retries, cancellation, idempotency, webhooks, and config behavior.

Confidence Score: 5/5

The changes appear safe to merge based on the touched component behavior, optional schema additions, and expanded tests described for the queue, webhook, config, and client surfaces.

No blocking code issues were identified in the finalized review, and the package changes are covered by focused tests plus type/build validation across the affected workspace and docs app.

T-Rex T-Rex Logs

What T-Rex did

  • Reviewed the webhook delivery status proofs: the before run showed base status/webhook rows without deliveryStatus fields, and the after run showed deliveryStatus transitions, deliveredAt values, and MessageID linking.
  • Validated the setconfig replace proofs: the before artifact showed stale cleanupAfterDays and defaultFrom alongside new testMode/sandboxTo fields, and the after artifact showed only sandboxTo and testMode, with both runs exiting 0 and Bun test passing.
  • Verified the enqueuebatch-config-read proofs: the before log showed batch IDs ["emails:1","emails:3","emails:5"] with configReadCount=3, while the after run showed the same batch IDs with configReadCount=1, and the test harness script for the run is saved.
  • Confirmed the client-typing contract validation: the before log showed the Bun tsc command failing due to missing exported types and unknown returns, while the after log shows the head run completing with Exit code 0 and the contract/tsconfig artifacts saved.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (2): Last reviewed commit: "fix(convex-email): gate bounce mapping o..." | Re-trigger Greptile

…rk bounce Type

Review findings on the webhook normalization:

- Mailgun sends event "failed" (permanent_fail is only the webhook config
  name), so bounces never mapped. Map failed + severity "permanent" to
  bounced; temporary failures stay stored-but-unmapped since Mailgun retries.
- Postmark fires RecordType "Bounce" for soft bounces too, which made a
  Transient bounce permanently poison deliveryStatus. Only permanent Types
  (HardBounce, BadEmailAddress, ManuallyDeactivated) map to bounced now.
- Document that bounced/complained are sticky against delivered, but between
  themselves the most recent webhook wins.
- Accept Postmark's numeric uppercase ID as a dedupe delivery id instead of
  falling back to the body hash.

Adds 6 tests (35 total) and aligns README + docs page with the exact mapping.

Generated-By: PostHog Code
Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@leoisadev1
leoisadev1 merged commit eadcee3 into main Jul 6, 2026
4 checks passed
@leoisadev1
leoisadev1 deleted the posthog-code/finish-convex-component branch July 6, 2026 15:22
leoisadev1 added a commit that referenced this pull request Jul 6, 2026
Merge brings in delivery status tracking (#126), recipient variables (#125),
scheduled sends (#127), the fumadocs SSR README (#121), AGENTS.md updates,
and Homebrew 0.6.5. Conflicts resolved by keeping the humanized prose and
main's feature facts; the field-support matrix re-verified cell-for-cell
against SUPPORTED_MESSAGE_FIELDS, including the new Send at column.

Also: clarify one-tag semantics (Postmark keeps name:value, Mailtrap and
Lettermint keep only the value), document sendBulk in the adapter contract,
add all_recipients_failed to the errors page, note per-recipient idempotency
suffixing and per-recipient hook firing, cross-link recipientVariables and
sendAt from the quickstart and landing page, and pre-add the telemetry
disclosure sections to both READMEs ahead of the telemetry PR.

Generated-By: PostHog Code
Task-Id: ec2538f2-5c80-4142-b4b3-b1a53394862e

This branch was successfully deployed

1 active deployment
Preview — f196ce2e Deployed Jul 6, 2026 by vercel[bot]
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