Brand transactional auth emails per app - #2439
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@builderio-bot look at latest PR feedback and fix anything you agree with. Be skeptical. Reply to every comment (directly on the comment thread of each comment) if you fixed it or not and why. then check back every 2 minutes on a loop and see if any new feedback posted, until at least 20 minutes go by without any new feedback posted we want to address, including making sure CI passes too and no merge conflicts (make sure code is mergeable) |
…a22984fd7a074c0f90c5
…ents Applying the branded sender unconditionally overrode a self-hosted deployment's verified EMAIL_FROM, which providers can reject and would break signup verification and password reset. Route branding through a new appSender arg that sendEmail only honors when the configured sender is already on agent-native.com; the same gate covers the reply-to so custom deployments keep their own support mailbox.
There was a problem hiding this comment.
Builder reviewed your changes and found 1 potential issue 🟡
Review Details
Incremental Code Review Summary
The latest commit addresses both previously reported custom-deployment issues: branding is now gated on the configured sender domain, and the branded reply-to is only applied alongside that first-party sender. I verified those fixes and resolved both stale review threads.
The new appSender plumbing and SendGrid coverage are sound, including explicit sender/reply-to precedence and preservation of self-hosted configuration. However, the first-party detection gate appears narrower than the deployment described by this PR: it recognizes only @agent-native.com, while the PR's stated existing first-party sender is noreply@builder.io. If that remains the actual production configuration, the feature is silently disabled for the main first-party deployment and auth emails retain generic branding.
Key Finding
- 🟡 MEDIUM — Include the current verified first-party sender/domain (or use an explicit first-party deployment identity) in the gate and add a regression test for it, while continuing to exclude arbitrary self-hosted domains.
🧪 Browser testing: Skipped — PR only modifies backend authentication email rendering and delivery, with no user-facing browser UI impact.
| const address = parseSendGridFrom(configuredFrom).email.toLowerCase(); | ||
| if (!address.endsWith(`@${AGENT_NATIVE_SENDER_DOMAIN}`)) return undefined; |
There was a problem hiding this comment.
🟡 Recognize the current first-party sender configuration
resolveAppSender only enables branding when EMAIL_FROM ends in @agent-native.com, but this PR identifies the existing first-party sender as noreply@builder.io. With that production configuration, branded remains undefined and both Better Auth callbacks continue sending the generic configured sender, so the feature is disabled for the first-party deployment it targets. Use an explicit first-party deployment identity or include the currently verified first-party domain, and add coverage for that configuration while still excluding arbitrary self-hosted domains.
Additional Info
New issue found by 1 of 3 incremental review agents; previous sender/reply-to issues were verified fixed and their threads resolved.
There was a problem hiding this comment.
Not changing this — the premise does not hold up, though the failure mode you describe is a real one worth stating explicitly.
noreply@builder.io comes from the PR description, which is a paraphrase of the reported symptom, not a configured value. It is not the sender configuration anywhere in this repo:
grep -rn "noreply@builder\.io"across the repo returns nothing. The only@builder.iostrings are unrelated fixtures intemplates/mail/server/lib/sender-identity.spec.ts.EMAIL_FROMis a per-deployment secret (synced viascripts/sync-template-netlify-env.ts), not a repo constant, so the PR cannot "identify" it.- The value actually configured for this workspace is
Agent Native <noreply@agent-native.com>, which ends in@agent-native.com. Under that configresolveAppSenderreturns branding and both callbacks send as<slug>@agent-native.com— the intended behavior, not a no-op.
On the residual risk: you are right that if some deployment sets EMAIL_FROM to a non-agent-native.com address, branding silently does not apply there. I deliberately did not widen the allowlist on a guess. Adding builder.io would mean sending as <slug>@agent-native.com from an account that may only have verified builder.io — reintroducing exactly the unverified-sender rejection that the 🔴 comment on this PR just flagged. Trading a cosmetic no-op for a hard delivery failure is the wrong direction.
If someone confirms the production EMAIL_FROM really is on builder.io and that agent-native.com is a verified sending domain on that provider account, this becomes a one-line addition to AGENT_NATIVE_SENDER_DOMAIN. That is a config fact I cannot verify from the repo, so I have flagged it for @steve8708 / @tim rather than guessing.
|
Here's a visual recap of what changed: Open the full interactive recap |
|
There was a problem with your request, please try again later. Error id: |
|
@steve8708 Triage complete. Head Fixed (1 commit, The two original findings were both valid and shared one root cause:
Templates no longer emit a raw Not fixed (1)
Needs a human
|
|
@builderio-bot look at latest PR feedback and fix anything you agree with. Be skeptical. Reply to every comment (directly on the comment thread of each comment) if you fixed it or not and why. then check back every 2 minutes on a loop and see if any new feedback posted, until at least 20 minutes go by without any new feedback posted we want to address, including making sure CI passes too and no merge conflicts (make sure code is mergeable) |
…a22984fd7a074c0f90c5
APP_NAME flows into the branded From display name, and angle brackets there made parseSendGridFrom read the address as part of the name, producing a malformed sender the provider rejects. Strip header-significant characters before interpolating, and fall back to the bare address when nothing is left.
|
@steve8708 Second triage pass complete. Head Fixed since last pass (
I scoped that as a deliverability bug rather than a security one — Also in this branch
Still open, needs a human
Full ledger: 4 review threads — 3 fixed and replied, 1 rejected with evidence and replied. No unanswered feedback. |
|
Babysitting complete on PR #2439. Final state — head Fixed this pass ( One new finding, and it was real — I reproduced it before changing anything. Fixed with Two judgment calls worth surfacing:
Cumulative ledger — 4 review threads: 3 fixed and replied, 1 rejected with evidence and replied. No feedback left unanswered. Two things I did not do, deliberately:
Note the reply-to change you asked for ( ⠀ |
|
@builderio-bot resolve hte merge conflict plz |
…a22984fd7a074c0f90c5 # Conflicts: # packages/core/src/server/email.ts
|
Conflict resolved and pushed as What the conflict actually wasNot a trivial textual clash. While this PR was open, That's a better-scoped fix than mine, so I preserved main's version and deleted my How I resolved itTook main's side wholesale, then re-added only the piece main doesn't cover — the per-app address swap ( const branded = resolveAppSender(config.from, args.appSender);
const from =
branded && !args.from
? withDisplayName(branded.address, args.fromName ?? branded.name)
: getFromAddress(config, args.from, args.fromName);
Verification: One test expectation changed: my malformed-name regression test now asserts main's quoted output ( CI is re-running on the new head. Merge is still ⠀ |
|
@builderio-bot wdyt of the latest feedback? also resolve the merge conflict |
…a22984fd7a074c0f90c5 # Conflicts: # packages/core/src/server/email.spec.ts
|
@steve8708 Second conflict resolved ( The merge conflict
One thing worth flagging separately: the merge pulled in a new The latest review Its a pass. Quoting the relevant part: it says the refactor "fixes the previously reported display-name parsing issue without regressing The only item it still lists is the sender-domain one it has now raised three times, and I still disagree with it for the same reason: it rests on Id actually go further now — theres positive evidence the current gate is correct. This workspace has So Im leaving it as-is. If you or @tim know of a deployment that sets CI is re-running on |
|
Both done. CI is fully terminal and green on The merge conflict
One thing that wasn't obvious: the merge pulled in a new What I think of the latest feedbackIt's a pass. The review states the refactor "fixes the previously reported display-name parsing issue without regressing The only item it still lists is the sender-domain one, now raised three times. I still disagree, and I found evidence that strengthens the case rather than weakens it:
The bot's premise ( Still needs you: if you or Tim know of a deployment that actually sets ⠀ |
This comment has been minimized.
This comment has been minimized.
|
@builderio-bot look at latest PR feedback and fix anything you agree with. Be skeptical. Reply to every comment (directly on the comment thread of each comment) if you fixed it or not and why. then check back every 2 minutes on a loop and see if any new feedback posted, until at least 20 minutes go by without any new feedback posted we want to address, including making sure CI passes too and no merge conflicts (make sure code is mergeable) |
…a22984fd7a074c0f90c5
The Resend branch serializes from/reply_to differently from SendGrid, and its reply_to line was only exercised indirectly.
|
Replying to the latest review ( Accepted: missing Resend I agreed with this one after checking rather than on the reviewers word. The Added Everything else in the review is a pass — structured sender path, provider handling, reply-to precedence, and auth integration all confirmed correct, no new reportable bugs. Sender-domain item stays rejected, unchanged from my previous reply. Short version: it rests on Branch is also up to date with |
|
There was a problem with your request, please try again later. Error id: |
There was a problem hiding this comment.
Builder reviewed your changes and has a few items to flag 🟡
Review Details
Incremental Code Review Summary
The latest commit adds the missing Resend appSender regression coverage. I reviewed the updated Resend and SendGrid payloads, explicit from/fromName/replyTo precedence, Better Auth call sites, and deployment fallback behavior. The implementation remains consistent across providers, and validation reports 13/13 focused email tests passing, typecheck passing, and no diff whitespace errors.
The existing first-party sender-domain issue remains open and unchanged, so it was intentionally not reposted. No new actionable issues were found in this increment.
🧪 Browser testing: Skipped — PR only modifies backend authentication email rendering and delivery, with no user-facing browser UI impact.

Summary
Signup verification and password reset emails are now branded per app instead of using a generic "Agent Native" sender, subject, and body.
Problem
Every agent-native app sent the same generic "Verify your email for Agent Native" email from
noreply@builder.io, with no indication of which specific app the user had signed up for.Solution
Resolve the app's slug and description from template metadata, then use them to customize the sender address, reply-to, subject, heading, and body copy for both the signup verification and password reset emails. Unknown apps still fall back to the generic "Agent Native" branding and configured
EMAIL_FROMsender.Key Changes
getAppSlug()andgetAppDescription()toapp-name.ts, resolving from first-party template metadata (with a slugified fallback for customAPP_NAMEoverrides).RenderedEmailMessagenow includes optionalfromandreplyTofields, andAGENT_NATIVE_REPLY_TO(hello@agent-native.com) is exported as the shared reply-to.renderVerifySignupEmailbuilds an "Agent-Native<App>" brand, setsfromto<slug>@agent-native.com, and appends a per-app one-line description (competitor names reframed as "replacement") to the email body.renderResetPasswordEmailuses the same per-app branding and sender/reply-to but omits the value-pitch description since it's a security email.better-auth-instance.tspasses the newfrom/replyTovalues through tosendEmailfor both verification and reset flows.To clone this PR locally use the Github CLI with command
gh pr checkout 2439You can tag me at @BuilderIO for anything you want me to fix or change