Skip to content

Add transactional email catalog and magic-link sign-in - #2642

Open
timmilazzo wants to merge 11 commits into
mainfrom
ai_main_381e5005a3dc41bcb927
Open

Add transactional email catalog and magic-link sign-in#2642
timmilazzo wants to merge 11 commits into
mainfrom
ai_main_381e5005a3dc41bcb927

Conversation

@timmilazzo

@timmilazzo timmilazzo commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a shared transactional email catalog so every app can declare the emails it sends, and introduces email magic-link sign-in as the default authentication method alongside password login.

Problem

There was no central way to see what transactional emails existed across apps, how they were triggered, who received them, who sent them, or how they performed (sends, open rate, last sent). Additionally, sign-in only supported passwords, with no lighter-weight magic-link option.

Solution

Introduced defineTransactionalEmail in @agent-native/core/email-catalog so apps register each email with an id, trigger description, recipient/sender logic, and a dummy-data preview renderer. sendEmail now accepts a templateId to tag messages at the provider and record every attempt (success or failure) to a new email_log table, decoupling metrics from the provider's short activity retention. Dispatch adds a Transactional email screen built on top of this catalog. Separately, Better Auth's magicLink plugin is wired into the shared auth instance and exposed as a default sign-in option below Google, backed by a new core.magic-link-sign-in catalog entry.

Key Changes

  • New changeset describing the transactional email catalog, sendEmail templateId support, email_log table, Dispatch's new screens, and default magic-link sign-in.
  • Added design/implementation plan docs for magic-link sign-in (docs/plans/2026-08-04-magic-link-sign-in-design.md, docs/plans/2026-08-04-magic-link-sign-in.md).
  • Added renderMagicLinkEmail and registered core.magic-link-sign-in in the core email templates/catalog, with corresponding tests.
  • Configured Better Auth's magicLink plugin in better-auth-instance.ts, wiring sendMagicLink to render and send the magic-link email via the core sendEmail transport.
  • Plan template: extracted renderPlanCommentEmail, added templates/plan/server/lib/emails.ts registering plan.plan-comment and plan.access-request catalog entries, and a transactional-emails.ts plugin to register them.
  • Slides template: extracted renderDeckCommentEmail, added templates/slides/server/lib/emails.ts registering slides.deck-comment, updated comment-notifications.ts to use the renderer and pass templateId, and added a transactional-emails.ts plugin to register it.

Edit in Builder  Preview


To clone this PR locally use the Github CLI with command gh pr checkout 2642

You can tag me at @BuilderIO for anything you want me to fix or change

@builder-io-integration builder-io-integration Bot changed the title Update from the Builder.io agent Add transactional email catalog and magic-link sign-in Aug 5, 2026
@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

builder-io-integration[bot]

This comment was marked as outdated.

@timmilazzo

Copy link
Copy Markdown
Contributor Author

@builderio-bot - review the latest PR feedback, fix what you agree with, comment on each item as you make changes, and try to make this merge-able.

@builder-io-integration

Copy link
Copy Markdown
Contributor

There was a problem with your request, please try again later. Error id: 0744b8fdd847455aac828f9c63cc246b

@timmilazzo

Copy link
Copy Markdown
Contributor Author

@builderio-bot - what human input do you truly need to get this to a merge-able state? Looks like you've made changes and comments but not given us a fresh status update.

@builder-io-integration

Copy link
Copy Markdown
Contributor

Fresh status: I do not need any further product or implementation input to finish this.

  • All 5 review findings have been addressed and all review threads are resolved.
  • The follow-on CI regressions found locally (Core mock, sign-in smoke, Clips/Plan import cycles and expectations) are fixed.
  • The remaining Scaffold E2E failure was the published-Core mismatch: the generated apps consume the new @agent-native/core/email-catalog export, so that workflow step now runs with AGENT_NATIVE_CREATE_USE_LOCAL_CORE=1, matching the existing local-Core onboarding test.
  • Fresh verification: 42/42 repository guards pass; workflow formatting and git diff --check pass; the exact scaffold create → pnpm install → Calendar build path exits 0.
  • GitHub currently shows the old failing CI run because these local branch changes have not been pushed yet.

Human actions remaining are mechanical only: push the branch changes, let CI rerun, and provide the required reviewer approval. No additional design decision is blocking mergeability.

@builder-io-integration

Copy link
Copy Markdown
Contributor

No further product or implementation input is needed.

I fixed the remaining scaffold CI configuration and posted a fresh status on PR #2642. Fresh verification passes: 42/42 guards, formatting, git diff --check, and the exact scaffold → install → Calendar build flow.

Remaining human actions:

  1. Push the local branch changes using the top-right Push button.
  2. Let CI rerun.
  3. Obtain the required reviewer approval.

@builder-io-integration builder-io-integration Bot left a comment

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.

Builder reviewed your changes and found 5 potential issues 🔴

Review Details

Incremental Code Review Summary

The latest commits address all five findings from the prior review: email logs now filter by app, SendGrid activity is no longer account-wide, Google-only deployments omit the magic-link plugin, verification tokens are hashed, and metrics follow the active transport. I resolved those previous review threads.

The remaining changes still carry high risk because they affect authentication links and privileged email-provider reporting.

New Findings

  • 🔴 High: Magic-link URLs are sent without inserting the mounted app path, so links generated by path-mounted apps such as Calendar or Mail can land at the wrong auth handler and fail to establish a session.
  • 🔴 High: Dispatch activity requests accept arbitrary SendGrid category IDs without catalog or app authorization, exposing recipient addresses, subjects, and delivery activity.
  • 🟡 Medium: Engagement requests likewise accept arbitrary category IDs and expose aggregate delivery/open/click data outside the authorized catalog.
  • 🟡 Medium: The Dispatch shared-core panel calculates core send counts from the Dispatch database only, so it misses core emails sent by other apps and can duplicate core definitions in per-app cards.
  • 🟡 Medium: Daily SendGrid unique_opens values are summed into a window-level unique-open rate, which can double-count recipients who open on multiple days and produce inflated rates.

The fixes from the previous review are directionally sound, but these new issues should be addressed before merge. Browser testing will be attempted again for the updated UI/auth flows.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +1157 to +1162
magicLink({
storeToken: "hashed",
sendMagicLink: async ({ email, url }) => {
const { subject, html, text } = renderMagicLinkEmail({
email,
magicLinkUrl: url,

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.

🔴 Prefix generated magic-link URLs with the mounted app path

The magic-link callback sends Better Auth's generated url unchanged. In workspace deployments the app is mounted under APP_BASE_PATH (for example /calendar), while this instance uses the unprefixed /_agent-native/auth/ba path. Unlike the existing reset and verification senders, this flow does not insert the mounted prefix, so recipients can land at the wrong auth handler and the token cannot establish a session. Apply the same normalized app-base-path rewrite used by the adjacent email flows and add a mounted-app regression test.

Additional Info
Reported by 2/4 review agents; confirmed against adjacent reset/verification URL handling.

Fix in Builder

limit: z.coerce.number().int().min(1).max(1000).default(50),
}),
http: { method: "GET" },
run: async ({ templateId, limit }) =>

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.

🔴 Authorize activity categories before querying SendGrid

The action now prevents an account-wide feed, but it still accepts any caller-provided templateId and sends it directly to SendGrid. There is no catalog lookup or app/workspace authorization, so an authenticated Dispatch caller can query known or arbitrary categories and receive recipient addresses, subjects, and delivery activity for messages outside the authorized transactional-email catalog. Resolve the ID against authorized catalog entries and reject unknown or inaccessible categories before calling the provider.

Additional Info
Reported by 3/4 review agents; this is distinct from the previously fixed account-wide-feed issue.

Fix in Builder

windowDays: z.coerce.number().int().min(1).max(365).default(30),
}),
http: { method: "POST" },
run: async ({ templateIds, windowDays }) =>

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.

🟡 Authorize categories before returning engagement metrics

This endpoint accepts arbitrary templateIds and forwards them directly to SendGrid category statistics without checking that each ID is registered and accessible to the caller. An authenticated Dispatch user can therefore retrieve delivery/open/click aggregates for categories belonging to other products or workloads. Validate IDs against the authorized catalog and reject or omit unknown/inaccessible categories before querying the provider.

Additional Info
Reported by 3/4 review agents; distinct from the previously fixed transport-selection issue.

Fix in Builder

> | null = null;
let statsError: string | null = null;
try {
const stats = await getEmailSendStats(since, app);

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.

🟡 Aggregate shared core-email counts across sending apps

The action computes stats with getEmailSendStats(since, app), where app is the currently running app, but returns definitions whose definition.app is core. Dispatch uses this local result for its shared system-email section, so core emails sent by Calendar, Forms, or other mounted apps are omitted and the same core definitions can also appear in per-app cards. Aggregate core-template stats across the relevant apps or separate core definitions from per-app results so the shared section represents all senders.

Additional Info
Reported by 1/4 review agents; confirmed by the Dispatch shared section filtering core definitions from the local action result.

Fix in Builder

Comment on lines +159 to +171
for (const day of payload ?? []) {
for (const entry of day.stats ?? []) {
const name = entry.name;
if (!name) continue;
const metrics = entry.metrics ?? {};
const current = totals.get(name) ?? {
templateId: name,
delivered: 0,
uniqueOpens: 0,
uniqueClicks: 0,
openRate: null,
};
current.delivered += Number(metrics.delivered ?? 0);

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.

🟡 Avoid summing daily unique opens as a window-wide unique count

The request uses aggregated_by=day, then sums each day's unique_opens while presenting the result as a window-level unique-open rate. A recipient who opens on multiple days is counted once per day, while delivered remains unchanged, inflating the rate and potentially producing values over 100%. Use a range-aggregated response if supported, or label/report the daily total rather than treating it as distinct recipients for the whole window.

Additional Info
Reported by 1/4 review agents; confirmed from the daily aggregation request and summation logic.

Fix in Builder

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Visual recap — screenshot failed

A recap was published, but the PR-comment screenshot could not be captured or uploaded. Open the interactive recap directly:

Open the full interactive recap

Diagnostic:

light: page.goto: Timeout 45000ms exceeded. Call log: - navigating to "https://plan.agent-native.com/recaps/recap-d5daa86e7a374d04?recapScreenshot=1&recapScreenshotTheme=light", waiting until "domcontentloaded"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants