Skip to content

Cloudflare-native auth + email (drop Clerk + Resend) + account email change - #47

Open
unforced wants to merge 1 commit into
mainfrom
cloudflare-auth-email-migration
Open

Cloudflare-native auth + email (drop Clerk + Resend) + account email change#47
unforced wants to merge 1 commit into
mainfrom
cloudflare-auth-email-migration

Conversation

@unforced

@unforced unforced commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Migrates LVB's auth and email layers onto Cloudflare primitives, adds a self-serve email-change flow, and removes the Clerk + Resend dependencies. All of this is already deployed to production and verified end-to-end (this PR records the working tree). Also bundles the in-flight Summer 2026 cohort funnel work that shared the tree.

Auth: Clerk → magic-link

  • Stateless HMAC-signed session cookies (lib/session.ts) — no sessions table, one users-by-id read per request.
  • Single-use, hashed, 20-min magic tokens (lib/magic-link.ts, migration 0025). Atomic single-use claim (UPDATE … WHERE used=0 … RETURNING).
  • getUser reads the session; findOrCreateUser keys on email and stores a synthetic clerk_id so the NOT NULL UNIQUE column survives without a table rebuild.
  • Removed clerkMiddleware, the Clerk webhook, the Clerk JS embed, middleware/auth.ts.
  • LVB's own OAuth 2.1 server (MCP connector) is unaffected — it's session-backed; verified the live connector still authenticates.

Email: Resend → Cloudflare Email Service

  • env.EMAIL send_email binding; sender hello@learnvibe.build (onboarded in CF dashboard, DKIM/SPF/DMARC).
  • Always sends a text/plain part; parseFromAddress + htmlToText helpers; List-Unsubscribe (mailto) + visible unsubscribe line on broadcasts.
  • Magic-link email restyled (centered button, mono fallback-link box, accent rule).
  • Removed the resend SDK, the audience mirror (resend-audience.ts), and the now-defunct "audience sync" UI on /admin/interests.

Account email change (new)

  • POST /settings/email → confirmation link to the new address. GET /settings/email/verify requires both the link and a matching session (prevents a mistyped/hostile address from taking over an account), re-checks uniqueness at confirm, updates users.email, and notifies the old address (migration 0026, lib/email-change.ts).

Hardening (adversarial review — 11 findings, all fixed or tracked)

  • HIGH: magic-link send is now awaited + binding-guarded — a broken email config surfaces an error instead of a silent "check your email" dead-end (this is the sole auth path).
  • Atomic single-use tokens; SESSION_SECRET guarded before the token is consumed; session signature decode fails closed; tightened sendEmail to a single recipient.

Removed deps

resend, @clerk/backend, @hono/clerk-auth. Deleted unused CLERK_SECRET_KEY / CLERK_WEBHOOK_SECRET secrets.

Verification

tsc clean. Live-tested on prod: magic-link sign-up/sign-in (email delivered via CF, inbox not spam), session persistence, single-use enforcement, OAuth/MCP connector, and the full email-change matrix (no-session → sign-in, wrong-account → 403, happy path, replay → 410, all POST validations).

Follow-ups (tracked, low/future)

  • Broadcast retry/backoff + daily-limit hard stop.
  • Gate the localhost admin dev-bypass on an explicit env flag.
  • One-Click HTTPS unsubscribe + suppression list (only needed near 5k/day).
  • RESEND_API_KEY secret is now dead — can be deleted on request.

🤖 Generated with Claude Code

…email change

Migrate the auth and email layers onto Cloudflare primitives and add a
self-serve email-change flow. Also bundles the in-flight Summer 2026 cohort
funnel work that shared the working tree.

Auth (Clerk -> magic-link):
- Stateless HMAC-signed session cookies (lib/session.ts); single-use, hashed,
  20-min magic tokens (lib/magic-link.ts, migration 0025).
- getUser via session; findOrCreateUser keyed on email with a synthetic
  clerk_id (no users-table rebuild). Removed clerkMiddleware, Clerk webhook,
  Clerk JS embed, middleware/auth.ts.
- LVB's own OAuth 2.1 server (MCP connector) unaffected -- session-backed.

Email (Resend -> Cloudflare Email Service):
- env.EMAIL send_email binding; from hello@learnvibe.build; text/plain part;
  parseFromAddress + htmlToText; List-Unsubscribe on broadcasts.
- Magic-link email restyled. Removed resend SDK + audience mirror + the
  defunct /admin/interests audience UI.

Account email change (new):
- /settings/email request -> confirm link to the NEW address. Confirming
  requires BOTH the link AND a matching session (prevents typo/hostile-address
  account takeover); single-use, uniqueness re-checked at confirm, old address
  notified (migration 0026, lib/email-change.ts).

Hardening from an adversarial review: awaited + binding-guarded magic-link send
(no silent auth dead-end), atomic single-use tokens, SESSION_SECRET guarded
before token burn, session signature decode fails closed.

Removed deps: resend, @clerk/backend, @hono/clerk-auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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