Skip to content

feat(auth): better-auth 1.5.5 -> 1.6.x + resendOTP via @rafters/better-auth-resend (#122)#130

Merged
ssilvius merged 1 commit into
mainfrom
feat/122-better-auth-1.6-otp-resend
May 16, 2026
Merged

feat(auth): better-auth 1.5.5 -> 1.6.x + resendOTP via @rafters/better-auth-resend (#122)#130
ssilvius merged 1 commit into
mainfrom
feat/122-better-auth-1.6-otp-resend

Conversation

@ssilvius

Copy link
Copy Markdown
Contributor

Closes #122. Stacked on #128 (drops Astro from apps/web). Will rebase once #128 merges.

What

Drops the console.log OTP placeholder. OTP now delivers via Resend through @rafters/better-auth-resend (renders the OtpEmail template from @rafters/mail-react-email, posts to Resend's API).

Workspace plumbing

Extends the cross-repo pattern from #107 (rafters packages) to mail packages.

  • pnpm-workspace.yaml: glob now includes ../mail/packages/{core,resend,react-email,better-auth-resend} as workspace:* source. Catalog backfilled with mail's entries (drizzle-orm, tsup, @cloudflare/workers-types). Sanctioned pattern per 019e2840.
  • apps/web/package.json: @rafters/better-auth-resend, @rafters/mail, @rafters/mail-react-email, @rafters/mail-resend as workspace:*. better-auth + @better-auth/passkey bumped 1.5.5 -> ^1.6.0. drizzle-orm ^0.45.1 -> ^0.45.2 to satisfy better-auth 1.6.11's drizzle-adapter peer.
  • root package.json: @rafters/better-auth-resend added as devDep so root vitest can resolve it from tests/.

Wiring

apps/web/src/auth.ts:

emailOTP({
  sendVerificationOTP: (() => {
    const send = resendOTP({
      apiKey: env.RESEND_API_KEY,
      fromEmail: env.FROM_EMAIL,
      brandName: \"Rafters Studio\",
      expiryMinutes: 10,
      baseUrl: \"https://api.resend.com\",
    });
    return ({ email, otp }) => send(email, otp);
  })(),
}),

Why the IIFE: resendOTP returns (email, otp) => Promise<void> but better-auth 1.6's sendVerificationOTP is ({email, otp, type}, ctx?) => Promise<void>. The IIFE constructs the Resend sender once (Zod parse + React Email renderer setup is non-trivial), then wraps to bridge signatures. buildAuth is itself memoized per D1Database via the existing authCache WeakMap.

wrangler.jsonc: vars.FROM_EMAIL = \"noreply@rafters.studio\". RESEND_API_KEY is provisioned via wrangler secret put (#125 covers prod secrets).

Tests

tests/api/auth.test.ts (new): asserts resendOTP returns a 2-arity function from valid config, and rejects empty apiKey + invalid fromEmail (Zod boundary contract). Workers-pool integration test for the auth handler is deferred to #126's e2e suite (would need to extend test-worker.ts to mount auth + provision a test better-auth schema; too much for this PR).

Verification

  • pnpm typecheck clean
  • pnpm test: 27 passed (was 24, +3 auth wiring), 2 skipped
  • pnpm build (wrangler deploy --dry-run) clean; FROM_EMAIL appears in the binding inventory
  • End-to-end OTP delivery: verifies at deploy time with a real RESEND_API_KEY (operator step, not in CI)

Out of scope

Test plan

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

…r-auth-resend (#122)

Drops the console.log OTP placeholder. OTP now delivers via Resend
through @rafters/better-auth-resend (renders @rafters/mail-react-email
OtpEmail template, posts to https://api.resend.com/emails).

Workspace plumbing (extends the rafters pattern from #107)
- pnpm-workspace.yaml: glob now includes ../mail/packages/{core,resend,
  react-email,better-auth-resend} as workspace:* source. Catalog
  backfilled with mail's entries (drizzle-orm, tsup, @cloudflare/
  workers-types). Sanctioned pattern per 019e2840.
- apps/web/package.json: @rafters/better-auth-resend, @rafters/mail,
  @rafters/mail-react-email, @rafters/mail-resend added as workspace:*
  deps. better-auth and @better-auth/passkey bumped 1.5.5 -> ^1.6.0.
  drizzle-orm bumped ^0.45.1 -> ^0.45.2 to satisfy better-auth 1.6.11's
  drizzle-adapter peer.
- root package.json: @rafters/better-auth-resend added as devDep so
  root vitest can resolve it from tests/.

Wiring
- apps/web/src/auth.ts: import resendOTP, replace console.log stub.
  IIFE constructs the Resend sender once per buildAuth call (Zod
  parse + React Email renderer setup is non-trivial); wraps to bridge
  resendOTP's (email, otp) signature to better-auth 1.6's ({email,
  otp, type}, ctx?) shape. buildAuth is itself memoized per
  D1Database via the existing authCache WeakMap.
- wrangler.jsonc: vars.FROM_EMAIL = "noreply@rafters.studio".
  RESEND_API_KEY documented as wrangler secret put provisioning.
- .dev.vars: RESEND_API_KEY placeholder for local dev.
- worker-configuration.d.ts: regen picks up FROM_EMAIL + RESEND_API_KEY.

Tests
- tests/api/auth.test.ts (new): asserts resendOTP returns a 2-arity
  function from a valid config, and rejects empty apiKey + invalid
  fromEmail (Zod boundary contract).

Verification
- pnpm typecheck: clean.
- pnpm test: 27 passed (was 24, +3 auth wiring), 2 skipped.
- pnpm build (wrangler dry-run): clean; FROM_EMAIL appears in the
  binding inventory.

Out of scope
- Polar webhook verification + audit (#124)
- Inbound email via @rafters/mail-cloudflare (#123)
- Production wrangler config + secrets provisioning (#125)
- Workers-pool integration test for the auth handler (defer to #126
  e2e suite which can hit a deployed worker)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ssilvius ssilvius force-pushed the feat/122-better-auth-1.6-otp-resend branch from 257a2ab to 0842c16 Compare May 16, 2026 06:17
@ssilvius ssilvius merged commit 525a486 into main May 16, 2026
1 check passed
@ssilvius ssilvius deleted the feat/122-better-auth-1.6-otp-resend branch May 16, 2026 06:25
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.

feat(auth): upgrade better-auth 1.5.5 -> 1.6.x + wire @rafters/better-auth-resend for OTP

1 participant