Skip to content

Migrate transactional email from Bento to AutoSend#1

Open
multiplehats wants to merge 1 commit into
mainfrom
migrate-bento-to-autosend
Open

Migrate transactional email from Bento to AutoSend#1
multiplehats wants to merge 1 commit into
mainfrom
migrate-bento-to-autosend

Conversation

@multiplehats

@multiplehats multiplehats commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Why

Migrating all apps off Bento (@bentonow/bento-node-sdk) onto AutoSend (autosendjs) for transactional email. This is the EmitKit app.

What changed (9 files, +192 / -209)

File Change
src/lib/server/bento.ts Deleted (-149) — the Bento Analytics wrapper
src/lib/server/autosend.ts New (+107) — AutosendClient wrapper: structured from/replyTo, throws on !res.success, batch via Promise.all
src/lib/features/email/server/email.service.ts bentoClientautosendClient; defaultFrom/defaultReplyTo/from/replyTo retyped stringEmailAddress; html_bodyhtml; to now { email }
src/lib/features/email/server/index.ts Single-key init (if (AUTOSEND_API_KEY)), env rename, parseEmailAddress helper to normalize "Name <email>" strings into the structured object
src/routes/email-preview/[...email]/+page.server.ts Copy + skip-message env references
src/routes/api/footprint/ingest/+server.ts Stale "(Bento)" comment only — endpoint uses @stacksee/analytics, untouched
.env.example Added Autosend section
package.json / pnpm-lock.yaml -@bentonow/bento-node-sdk, +autosendjs@^1.0.3

API / field mapping

Bento AutoSend
to: string to: { email }
from: 'Name <email>' from: { email, name }
html_body html
transactional: true (default, no flag)
Batch.sendTransactionalEmails({ emails }) Promise.all(emails.map(send))
bento.V1.Subscribers.createSubscriber client.contacts.upsert(...)

Gotchas handled

  • Structured from, not RFC-5322 string. Passing "Name <email>" to AutoSend returns success-shaped but never sends. The wrapper + parseEmailAddress enforce { email, name }.
  • res.success is checked. client.emails.send() does not throw on API-level failures; the wrapper inspects the result and throws so failures surface.
  • replyTo now actually sends — AutoSend supports it; the old Bento code logged-and-ignored it.

Runtime / await discipline

SvelteKit on Vercel (serverless)@sveltejs/adapter-vercel (nodejs20.x), vercel.json. Better Auth callbacks (sendResetPassword, sendVerificationEmail, emailOTP.sendVerificationOTP) already await through the email service inside try/catch that swallows errors — correct for "check your inbox" flows. No auth changes needed.

Event tracking

None on Bento. Analytics run via @stacksee/analytics (which declares Bento as an optional transitive dep — that's why it may still appear in the lockfile; it is not our direct dependency). Nothing dropped.

Remote env changes required before deploy (Vercel)

  • Remove: BENTO_PUBLISHABLE_KEY, BENTO_SECRET_KEY, BENTO_SITE_UUID, BENTO_DEFAULT_FROM, BENTO_REPLY_TO
  • Add: AUTOSEND_API_KEY (required); optional AUTOSEND_DEFAULT_FROM, AUTOSEND_REPLY_TO
  • .env is plaintext (not dotenvx) — no encryption step

Test plan

  • pnpm run check — 0 errors, 0 warnings
  • Real email smoke-test — green types do NOT catch the from-address / res.success runtime gotchas. Trigger signup / OTP / password reset and confirm delivery.
  • Set AUTOSEND_API_KEY locally + in Vercel before testing
  • Confirm the AutoSend sender domain for AUTOSEND_DEFAULT_FROM (defaults to EmitKit <noreply@emitkit.com>) is verified

Replace @bentonow/bento-node-sdk with autosendjs. Swap the email
service to AutoSend's structured from-address and res.success checks,
switch to single-key (AUTOSEND_API_KEY) init, and delete the Bento
wrapper. No event tracking was on Bento (analytics run via
@stacksee/analytics), so nothing was dropped.
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request migrates the application's email delivery backend from Bento to Autosend. It introduces a new AutosendClient wrapper, updates EmailService to use structured EmailAddress types, adds environment variables and dependencies, and wires the new client into server initialization.

Changes

Email provider migration: Bento → Autosend

Layer / File(s) Summary
Environment and dependency setup
.env.example, package.json
New Autosend environment variables (API key, default sender, reply-to) and dependency swap from @bentonow/bento-node-sdk to autosendjs.
Autosend client wrapper
src/lib/server/autosend.ts
New AutosendClient class wraps autosendjs with optional API key initialization, TransactionalEmail type contract, single and batch email methods with logging and error handling.
Email service refactor to Autosend
src/lib/features/email/server/email.service.ts
EmailService now uses AutosendClient and EmailAddress types instead of Bento; sendEmail and sendBatch construct TransactionalEmail payloads and invoke corresponding client methods.
Server initialization and email address parsing
src/lib/features/email/server/index.ts
Instantiate AutosendClient from environment, add parseEmailAddress helper to convert env strings to structured EmailAddress objects, compute and export defaults.
Documentation and comment updates
src/routes/api/footprint/ingest/+server.ts, src/routes/email-preview/[...email]/+page.server.ts
Update docstrings and error messages to reference Autosend and AUTOSEND_API_KEY.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 From Bento's nest to Autosend's flow,
Email wings now fetch and go,
Structured addresses, typed with care,
Batch and single, floating through air!
A hop, a skip, migration complete—
The long-ear way to make it neat! 🌷

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: migrating the transactional email service from Bento to AutoSend. It is concise, clear, and directly related to the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migrate-bento-to-autosend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/features/email/server/email.service.ts`:
- Around line 65-71: The current sendEmail path silently returns { skipped: true
} when autosendClient.isEnabled() is false; change it to fail fast outside
explicit local-preview/dev flows by throwing an error (e.g., throw new
Error('Autosend provider not configured') ) instead of returning skipped when
not in a local preview mode; keep the existing skip behavior only when an
explicit local-preview flag or NODE_ENV==='development' (or an isLocalPreview
config) is present. Update the branch that checks autosendClient.isEnabled()
(and the analogous branch at lines ~116-122) to throw in production/non-local
modes so callers of sendEmail() (such as auth.ts awaiting sendEmail()) cannot
silently drop emails, and additionally add a startup validation that verifies
autosendClient is configured to fail-fast during init if the app is expected to
send real emails.

In `@src/lib/features/email/server/index.ts`:
- Around line 15-23: The parseEmailAddress function may return { email: '' } for
whitespace-only inputs; normalize and trim the incoming value first and also
trim the regex capture groups, then if the resulting email string is empty
return the provided fallback. Update parseEmailAddress (and any analogous
parsing used for defaultFrom/defaultReplyTo) to run value = value?.trim() ||
undefined before matching, use trimmed captures for name and email, and if the
parsed email after trimming is falsy, return fallback instead of { email: '' }.

In `@src/lib/server/autosend.ts`:
- Around line 93-99: The current batch send uses Promise.all which rejects on
the first failed send and hides partial successes; replace
Promise.all([...this.sendTransactionalEmail...]) with Promise.allSettled so each
email's outcome is captured, then map the settled results to a per-email status
object (include original email id/address, status: "fulfilled" or "rejected",
value or reason) and return that array instead of throwing; update operation.end
to record succeeded count (count of fulfilled results) and ensure callers can
inspect per-email results to implement idempotent retries; reference
this.sendTransactionalEmail and the batch-sending block in autosend.ts when
making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47603c44-749f-4186-aba0-2407b7e736fa

📥 Commits

Reviewing files that changed from the base of the PR and between 172b6f6 and 4f4ec13.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • .env.example
  • package.json
  • src/lib/features/email/server/email.service.ts
  • src/lib/features/email/server/index.ts
  • src/lib/server/autosend.ts
  • src/lib/server/bento.ts
  • src/routes/api/footprint/ingest/+server.ts
  • src/routes/email-preview/[...email]/+page.server.ts
💤 Files with no reviewable changes (1)
  • src/lib/server/bento.ts

Comment on lines +65 to 71
if (!this.autosendClient.isEnabled()) {
this.logger.warn('Autosend disabled; skipping sendEmail', {
subject: options.subject,
to: options.to
});
operation.end({ skipped: true });
return { skipped: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't silently skip sends when Autosend is misconfigured outside local preview/dev flows.

These branches turn a required provider misconfiguration into { skipped: true }. src/lib/server/auth.ts:101-128 awaits emailService.sendEmail() but never checks skipped, so password-reset mail can be dropped while the auth operation still records success. Throw here in non-dev (or fail fast during startup) and keep the skip path only for explicitly local preview scenarios.

Also applies to: 116-122

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/features/email/server/email.service.ts` around lines 65 - 71, The
current sendEmail path silently returns { skipped: true } when
autosendClient.isEnabled() is false; change it to fail fast outside explicit
local-preview/dev flows by throwing an error (e.g., throw new Error('Autosend
provider not configured') ) instead of returning skipped when not in a local
preview mode; keep the existing skip behavior only when an explicit
local-preview flag or NODE_ENV==='development' (or an isLocalPreview config) is
present. Update the branch that checks autosendClient.isEnabled() (and the
analogous branch at lines ~116-122) to throw in production/non-local modes so
callers of sendEmail() (such as auth.ts awaiting sendEmail()) cannot silently
drop emails, and additionally add a startup validation that verifies
autosendClient is configured to fail-fast during init if the app is expected to
send real emails.

Comment on lines +15 to +23
function parseEmailAddress(value: string | undefined, fallback: EmailAddress): EmailAddress {
if (!value) return fallback;
const match = value.match(/^\s*(.*?)\s*<\s*([^>]+)\s*>\s*$/);
if (match) {
const [, name, email] = match;
return name ? { email, name } : { email };
}
return { email: value.trim() };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden env email parsing to avoid invalid defaults reaching Autosend

At Line 16 and Line 22, whitespace-only env values can produce { email: '' } instead of using fallback. This can break all sends when defaultFrom/defaultReplyTo are built from misconfigured env vars. Normalize first, trim captures, and fall back if parsed email is empty.

Suggested patch
 function parseEmailAddress(value: string | undefined, fallback: EmailAddress): EmailAddress {
-	if (!value) return fallback;
-	const match = value.match(/^\s*(.*?)\s*<\s*([^>]+)\s*>\s*$/);
+	const normalized = value?.trim();
+	if (!normalized) return fallback;
+	const match = normalized.match(/^\s*(.*?)\s*<\s*([^>]+)\s*>\s*$/);
 	if (match) {
-		const [, name, email] = match;
-		return name ? { email, name } : { email };
+		const [, rawName, rawEmail] = match;
+		const email = rawEmail.trim();
+		if (!email) return fallback;
+		const name = rawName.trim();
+		return name ? { email, name } : { email };
 	}
-	return { email: value.trim() };
+	return normalized ? { email: normalized } : fallback;
 }

Also applies to: 25-29

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/features/email/server/index.ts` around lines 15 - 23, The
parseEmailAddress function may return { email: '' } for whitespace-only inputs;
normalize and trim the incoming value first and also trim the regex capture
groups, then if the resulting email string is empty return the provided
fallback. Update parseEmailAddress (and any analogous parsing used for
defaultFrom/defaultReplyTo) to run value = value?.trim() || undefined before
matching, use trimmed captures for name and email, and if the parsed email after
trimming is falsy, return fallback instead of { email: '' }.

Comment on lines +93 to +99
try {
// Autosend has no multi-template batch endpoint; send each individually.
const responses = await Promise.all(
emails.map((email) => this.sendTransactionalEmail(email))
);
operation.end({ count: responses.length });
return responses;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Return per-email batch results instead of failing the whole batch on first rejection.

Promise.all() makes this non-atomic: some emails can already be delivered before one send rejects, but callers only get a thrown batch error. Retrying the batch can then double-send the subset that succeeded on the first attempt. Please surface partial successes/failures (Promise.allSettled + per-email status) or add an idempotent retry strategy here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/autosend.ts` around lines 93 - 99, The current batch send uses
Promise.all which rejects on the first failed send and hides partial successes;
replace Promise.all([...this.sendTransactionalEmail...]) with Promise.allSettled
so each email's outcome is captured, then map the settled results to a per-email
status object (include original email id/address, status: "fulfilled" or
"rejected", value or reason) and return that array instead of throwing; update
operation.end to record succeeded count (count of fulfilled results) and ensure
callers can inspect per-email results to implement idempotent retries; reference
this.sendTransactionalEmail and the batch-sending block in autosend.ts when
making the change.

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