Skip to content

Latest commit

 

History

History
116 lines (82 loc) · 7.1 KB

File metadata and controls

116 lines (82 loc) · 7.1 KB
status done
depends
specs
specs/api/auth.md
issues
43
pr 99

Plan: Welcome notification on fresh OAuth signup

Scope

apps/api/src/auth/github-oauth.ts handles the GitHub OAuth callback. The create-fresh outcome — a brand-new user with no laddr-account-claim to do — writes a new Person + PrivateProfile, mints a session, and redirects. No welcome notification today.

This plan adds the third method to the Notifier interface (notifyWelcomeOnSignup) and fires it from the create-fresh path. With the EmailNotifier from #82 already in place, the welcome email lands as soon as RESEND_API_KEY is sealed; until then, it logs via LoggingNotifier.

Closes #43.

Implements

  • api/auth.md — the GitHub OAuth flow's create-fresh user shape gets a notification side-effect.

Approach

1. Extend the Notifier interface

In apps/api/src/notify/index.ts:

export interface WelcomeNotification {
  readonly email: string;       // PrivateProfile.email
  readonly fullName: string;    // Person.fullName
  readonly slug: string;        // Person.slug — used in the profile link
}

export interface Notifier {
  notifyHelpWantedInterest(n: HelpWantedInterestNotification): Promise<{ delivered: boolean }>;
  notifyHelpWantedFilled(n: HelpWantedFillNotification): Promise<{ delivered: boolean }>;
  notifyWelcomeOnSignup(n: WelcomeNotification): Promise<{ delivered: boolean }>;
}

Add the no-op LoggingNotifier.notifyWelcomeOnSignup and the real EmailNotifier.notifyWelcomeOnSignup — same shape as the existing two methods.

2. Welcome email template

apps/api/src/notify/templates.ts gains renderWelcomeEmail(n, siteHost). Short, warm, single-CTA body:

  • Subject: Welcome to Code for Philly, <fullName>
  • Text body: 2-3 sentence intro pointing at the projects directory + Slack workspace
  • HTML body: same content + styled link buttons
  • Like the existing templates, HTML-escapes user-supplied fields (fullName)

3. Wire into the OAuth callback

In apps/api/src/auth/github-oauth.ts's create-fresh branch, after createFresh resolves:

// Fire-and-forget the welcome notification — never block the redirect on
// notifier latency or failures. The spec for express-interest applies here
// too: returning 202/302 to the caller regardless of notification outcome.
void fastify.notifier
  .notifyWelcomeOnSignup({
    email: result.value.profile.email,
    fullName: result.value.person.fullName,
    slug: result.value.person.slug,
  })
  .catch((err) => {
    fastify.log.error({ err }, 'welcome notification threw (fire-and-forget)');
  });

Fire-and-forget — Notifier.notifyXxx already returns { delivered } and swallows errors internally, but the outer .catch covers any unforeseen sync-throw before the SDK is reached. The OAuth callback's redirect happens on the next line, unblocked.

4. Tests

apps/api/tests/welcome-notification.test.ts:

  • Template renderers — interest-style assertions for subject + body interpolation, HTML escape of fullName
  • EmailNotifier.notifyWelcomeOnSignup — Resend success, Resend-error, SDK-throw, missing email (defensive — shouldn't happen on the create-fresh path since OAuth requires a primary email, but the interface accepts any string)
  • LoggingNotifier.notifyWelcomeOnSignup — logs + returns delivered: true

Wiring-level test: the existing github-oauth.test.ts covers the create-fresh outcome end-to-end. Extend it with one case that asserts fastify.notifier.notifyWelcomeOnSignup was called with the right payload (vi.spyOn on the notifier). Don't test that the email delivered — that's a notifier-unit concern.

Validation

  • Three Notifier impls (interface + LoggingNotifier + EmailNotifier) all have notifyWelcomeOnSignup.
  • EmailNotifier sends with the right Resend payload (subject + text + html + to).
  • Welcome template HTML-escapes fullName (test asserts <script> becomes &lt;script&gt;).
  • OAuth create-fresh path fires the notifier via spy assertion in github-oauth.test.ts — payload matches { email, fullName, slug }.
  • Existing OAuth tests pass (15 pre-existing + 1 new = 16 in that file).
  • 310/310 API tests pass; npm run type-check && npm run lint clean.

Risks / unknowns

  • Fire-and-forget vs await. If we await, OAuth callback latency includes one Resend HTTPS round-trip — slow on a cold path, and a Resend outage would stall logins. Fire-and-forget trades email guarantees (a crash before the SDK enqueues drops the email silently) for response latency. Trade is right for v1; if delivery guarantees become important, a small outbox table is the canonical answer.
  • No retry on Resend failures. Same trade-off — one shot, log the error, move on. Resend's own retry is good enough for transient blips.
  • No double-fire on duplicate signups. OAuth create-fresh only fires when a Person was actually created (the kind === 'create-fresh' branch), so re-running the callback for an existing user doesn't re-send. Safe.
  • First-time-Login race with not-yet-sealed Resend key. If the sandbox flips RESEND_API_KEY mid-signup, the in-flight request uses whatever notifier was installed at boot. Negligible — sealing the secret is a deploy event, the pod restart resets everything.

Notes

One-shot implementation across two commits (plan + impl/tests).

Surprises:

  • encodeURIComponent for the profile-link slug. Slugs are [a-z0-9-] per the spec so they never need encoding today, but the template uses encodeURIComponent defensively. Test asserts that a hypothetical slug with spaces produces name%20with%20spaces in the href, which gives the template the same robustness as the existing interest/filled renderers.
  • Spy timing in the OAuth test. Fire-and-forget worried me — would the spy assertion race the route handler's return? But void notifier.notifyWelcomeOnSignup(...) synchronously invokes the function (queuing the promise) before the next statement runs. By the time the route handler resolves and the test awaits the response, the spy has been called at least once. The Resend HTTPS send happens later in the promise chain; we don't await it.
  • No new env needed. The notifier already had a siteHost from CFP_SITE_HOST (added in #82's email-notifier wiring); the welcome template reuses it for the profile/projects/help-wanted links. Zero new config.

Follow-ups

  • Slack #welcome bump. Once Slack DM lands (#95), the welcome path could DM the new user with a #welcome channel invite link. Out of scope here; tracked alongside the Slack notifier work.
  • Email verification reminder. If the GitHub primary email later fails delivery (Resend bounces it), today the user has no recovery path beyond editing their profile. Tied to the bounce-webhook follow-up from #82.
  • A/B-able copy. The body is currently hardcoded. If marketing ever wants to experiment, lift into a small key/value content table in a future plan. Deferred — premature for v1.