Skip to content

Commit 59e61aa

Browse files
authored
feat: add email templates and email OTP login flow (#2110)
1 parent e128d60 commit 59e61aa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2787
-509
lines changed

.env

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ CLOUDFLARE_API_TOKEN=
5252
# https://firebase.google.com/docs/analytics/get-started?platform=web
5353
GA_MEASUREMENT_ID=G-XXXXXXXX
5454

55-
# SendGrid
56-
# https://app.sendgrid.com/settings/api_keys
57-
SENDGRID_API_KEY=xxxxx
58-
FROM_EMAIL=hello@example.com
55+
# Resend
56+
# https://resend.com/api-keys
57+
RESEND_API_KEY=xxxxx
58+
RESEND_EMAIL_FROM=onboarding@resend.dev
5959

6060
# Algolia Search
6161
# https://dashboard.algolia.com/account/api-keys/all

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ jobs:
4848
if: ${{ github.event_name == 'pull_request' }}
4949

5050
# Type checking
51+
- run: bun email:build # Build email templates first
5152
- run: bun tsc --build
5253

5354
# Testing (commented out - enable as needed)

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ terraform.rc
6868
docs/.vitepress/cache
6969
docs/.vitepress/dist
7070

71+
# React Email
72+
# Generated preview build and runtime files
73+
.react-email/
74+
7175
# Local development files
7276
*.local.md
7377
*.local.json

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
},
5656
"cSpell.ignoreWords": [
5757
"browserslist",
58+
"bunx",
5859
"cloudfunctions",
5960
"corejs",
6061
"corepack",
@@ -89,7 +90,6 @@
8990
"refetch",
9091
"refetchable",
9192
"relyingparty",
92-
"sendgrid",
9393
"signup",
9494
"sourcemap",
9595
"spdx",

CLAUDE.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Full-stack React application template optimized for Cloudflare Workers deploymen
77
- `apps/web/` - Marketing static website
88
- `apps/app/` - Main application SPA
99
- `apps/api/` - tRPC API server
10+
- `apps/email/` - React Email templates for authentication emails
1011
- `packages/core/` - Shared core utilities and WebSocket functionality
1112
- `packages/ui/` - Shared UI components and shadcn/ui management scripts
1213
- `db/` - Drizzle ORM schemas and migrations
@@ -50,6 +51,11 @@ bun ui:list # List installed components
5051
bun ui:update # Update all components
5152
bun ui:essentials # Install essential components
5253

54+
# Email Templates
55+
bun email:dev # Start email preview server
56+
bun email:build # Build email templates
57+
bun email:export # Export static email templates
58+
5359
# Other
5460
bun lint # Lint all code
5561
bun --filter @repo/db push # Apply DB schema changes
@@ -60,9 +66,15 @@ bun --filter @repo/db studio # Open DB GUI
6066
bun --filter @repo/db seed # Seed sample data
6167

6268
# Deployment
63-
bun wrangler deploy --config apps/web/wrangler.jsonc --env=production
64-
bun wrangler deploy --config apps/app/wrangler.jsonc --env=production
65-
bun wrangler deploy --config apps/api/wrangler.jsonc --env=production
69+
# Build required packages first
70+
bun email:build # Build email templates
71+
bun web:build # Build marketing site
72+
bun app:build # Build main React app
73+
74+
# Deploy all applications
75+
bun web:deploy # Deploy marketing site
76+
bun api:deploy # Deploy API server
77+
bun app:deploy # Deploy main React app
6678
```
6779

6880
## Code Conventions

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This starter kit uses a thoughtfully organized monorepo structure that promotes
3232
- [`apps/app/`](./apps/app) — React 19 application with TanStack Router, Jotai, and Tailwind CSS v4
3333
- [`apps/web/`](./apps/web) — Astro marketing website for static site generation
3434
- [`apps/api/`](./apps/api) — tRPC API server powered by Hono framework for Cloudflare Workers
35+
- [`apps/email/`](./apps/email) — React Email templates for authentication and transactional emails
3536
- [`packages/core/`](./packages/core) — Shared TypeScript types and utilities
3637
- [`packages/ui/`](./packages/ui) — Shared UI components with shadcn/ui management utilities
3738
- [`packages/ws-protocol/`](./packages/ws-protocol) — WebSocket protocol template with type-safe messaging
@@ -112,7 +113,7 @@ bun install
112113

113114
### 3. Configure Environment
114115

115-
Update environment variables in [`.env`](./.env) and `.env.local` files as well as Wrangler configuration in [`wrangler.jsonc`](./apps/edge/wrangler.jsonc).
116+
Update environment variables in [`.env`](./.env) and `.env.local` files as well as Wrangler configuration in [`wrangler.jsonc`](./apps/api/wrangler.jsonc).
116117

117118
### 4. Start Development
118119

@@ -124,11 +125,10 @@ Open two terminals and run these commands:
124125
bun --filter @repo/app dev
125126
```
126127

127-
**Terminal 2 - Backend:**
128+
**Terminal 2 - API Server:**
128129

129130
```bash
130-
bun --filter @repo/edge build --watch
131-
bun wrangler dev
131+
bun --filter @repo/api dev
132132
```
133133

134134
For the marketing website:
@@ -162,13 +162,15 @@ bun wrangler secret put OPENAI_API_KEY --env=production
162162
### 2. Build and Deploy
163163

164164
```bash
165-
# Build all packages
166-
bun --filter @repo/app build
167-
bun --filter @repo/web build
168-
bun --filter @repo/edge build
169-
170-
# Deploy to Cloudflare Workers
171-
bun wrangler deploy --env=production
165+
# Build packages that require compilation (order matters!)
166+
bun email:build # Build email templates first
167+
bun web:build # Build marketing site
168+
bun app:build # Build main React app
169+
170+
# Deploy all applications
171+
bun web:deploy # Deploy marketing site
172+
bun api:deploy # Deploy API server
173+
bun app:deploy # Deploy main React app
172174
```
173175

174176
Your application will be live on your Cloudflare Workers domain within seconds. The edge-first architecture ensures optimal performance regardless of user location.

apps/api/dev.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,21 @@ app.use("*", async (c, next) => {
6161
"GOOGLE_CLIENT_ID",
6262
"GOOGLE_CLIENT_SECRET",
6363
"OPENAI_API_KEY",
64+
"RESEND_API_KEY",
65+
"RESEND_EMAIL_FROM",
6466
] as const;
6567

6668
const env = {
6769
...cf.env,
6870
...Object.fromEntries(
69-
secretKeys.map((key) => [key, (cf.env[key] || process.env[key]) ?? ""]),
71+
secretKeys.map((key) => [key, (process.env[key] || cf.env[key]) ?? ""]),
7072
),
71-
APP_NAME: cf.env.APP_NAME || process.env.APP_NAME || "Example",
73+
APP_NAME: process.env.APP_NAME || cf.env.APP_NAME || "Example",
7274
APP_ORIGIN:
7375
// Prefer origin set by `apps/app` at runtime
7476
c.req.header("x-forwarded-origin") ||
75-
c.env.APP_ORIGIN ||
7677
process.env.APP_ORIGIN ||
78+
c.env.APP_ORIGIN ||
7779
"http://localhost:5173",
7880
};
7981

apps/api/lib/auth.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { betterAuth } from "better-auth";
66
import type { DB } from "better-auth/adapters/drizzle";
77
import { drizzleAdapter } from "better-auth/adapters/drizzle";
88
import { anonymous, organization } from "better-auth/plugins";
9+
import { emailOTP } from "better-auth/plugins/email-otp";
910
import { passkey } from "better-auth/plugins/passkey";
11+
import { sendOTP, sendPasswordReset, sendVerificationEmail } from "./email";
1012
import type { Env } from "./env";
1113

1214
/**
@@ -20,6 +22,8 @@ type AuthEnv = Pick<
2022
| "BETTER_AUTH_SECRET"
2123
| "GOOGLE_CLIENT_ID"
2224
| "GOOGLE_CLIENT_SECRET"
25+
| "RESEND_API_KEY"
26+
| "RESEND_EMAIL_FROM"
2327
>;
2428

2529
/**
@@ -79,6 +83,16 @@ export function createAuth(
7983
// Email and password authentication
8084
emailAndPassword: {
8185
enabled: true,
86+
sendResetPassword: async ({ user, url }) => {
87+
await sendPasswordReset(env, { user, url });
88+
},
89+
},
90+
91+
// Email verification
92+
emailVerification: {
93+
sendVerificationEmail: async ({ user, url }) => {
94+
await sendVerificationEmail(env, { user, url });
95+
},
8296
},
8397

8498
// OAuth providers
@@ -104,6 +118,14 @@ export function createAuth(
104118
// origin: URL where auth occurs (no trailing slash)
105119
origin: env.APP_ORIGIN,
106120
}),
121+
emailOTP({
122+
async sendVerificationOTP({ email, otp, type }) {
123+
await sendOTP(env, { email, otp, type });
124+
},
125+
otpLength: 6,
126+
expiresIn: 300, // 5 minutes
127+
allowedAttempts: 3,
128+
}),
107129
],
108130

109131
advanced: {

apps/api/lib/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { DbSchema } from "@repo/db";
55
import type { CreateHTTPContextOptions } from "@trpc/server/adapters/standalone";
66
import type { Session, User } from "better-auth/types";
77
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
8+
import type { Resend } from "resend";
89
import type { Auth } from "./auth.js";
910
import type { Env } from "./env.js";
1011

@@ -80,6 +81,7 @@ export type AppContext = {
8081
db: PostgresJsDatabase<DbSchema>;
8182
dbDirect: PostgresJsDatabase<DbSchema>;
8283
auth: Auth;
84+
resend?: Resend;
8385
session: Session | null;
8486
user: User | null;
8587
};

0 commit comments

Comments
 (0)