@rebasepro/cli 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/commands/api-keys.d.ts +1 -0
- package/dist/commands/generate_sdk.d.ts +1 -1
- package/dist/index.es.js +399 -57
- package/dist/index.es.js.map +1 -1
- package/package.json +20 -15
- package/templates/template/backend/package.json +0 -1
- package/templates/template/backend/src/index.ts +1 -1
- package/templates/template/config/collections/posts.ts +1 -3
- package/templates/template/config/collections/tags.ts +1 -3
- package/templates/template/config/collections/users.ts +1 -4
- package/templates/template/package.json +0 -1
- package/templates/template/scripts/example.ts +4 -1
- package/skills/rebase-admin/SKILL.md +0 -710
- package/skills/rebase-api/SKILL.md +0 -662
- package/skills/rebase-api/references/.gitkeep +0 -3
- package/skills/rebase-auth/SKILL.md +0 -1143
- package/skills/rebase-auth/references/.gitkeep +0 -3
- package/skills/rebase-backend-postgres/SKILL.md +0 -633
- package/skills/rebase-backend-postgres/references/.gitkeep +0 -3
- package/skills/rebase-basics/SKILL.md +0 -749
- package/skills/rebase-basics/references/.gitkeep +0 -3
- package/skills/rebase-collections/SKILL.md +0 -1328
- package/skills/rebase-collections/references/.gitkeep +0 -3
- package/skills/rebase-cron-jobs/SKILL.md +0 -699
- package/skills/rebase-cron-jobs/references/.gitkeep +0 -1
- package/skills/rebase-custom-functions/SKILL.md +0 -233
- package/skills/rebase-deployment/SKILL.md +0 -885
- package/skills/rebase-deployment/references/.gitkeep +0 -3
- package/skills/rebase-design-language/SKILL.md +0 -692
- package/skills/rebase-email/SKILL.md +0 -701
- package/skills/rebase-email/references/.gitkeep +0 -1
- package/skills/rebase-entity-history/SKILL.md +0 -485
- package/skills/rebase-entity-history/references/.gitkeep +0 -1
- package/skills/rebase-local-env-setup/SKILL.md +0 -189
- package/skills/rebase-local-env-setup/references/.gitkeep +0 -3
- package/skills/rebase-realtime/SKILL.md +0 -755
- package/skills/rebase-realtime/references/.gitkeep +0 -3
- package/skills/rebase-sdk/SKILL.md +0 -594
- package/skills/rebase-sdk/references/.gitkeep +0 -0
- package/skills/rebase-storage/SKILL.md +0 -765
- package/skills/rebase-storage/references/.gitkeep +0 -3
- package/skills/rebase-studio/SKILL.md +0 -746
- package/skills/rebase-studio/references/.gitkeep +0 -3
- package/skills/rebase-ui-components/SKILL.md +0 -1488
- package/skills/rebase-ui-components/references/.gitkeep +0 -3
- package/skills/rebase-webhooks/SKILL.md +0 -623
- package/skills/rebase-webhooks/references/.gitkeep +0 -1
- package/templates/template/backend/drizzle.config.ts +0 -51
|
@@ -1,701 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: rebase-email
|
|
3
|
-
description: Guide for configuring and using the Rebase email system. Use this skill when the user needs to set up SMTP, customize email templates, use a custom email provider (AWS SES, Resend, Postmark), send emails programmatically via the `rebase.email` singleton, or understand how email integrates with password reset, email verification, user invitation, and welcome email flows.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Rebase Email
|
|
7
|
-
|
|
8
|
-
Rebase includes a built-in email system that powers password reset, email verification, user invitation, and welcome email flows. It supports SMTP (via Nodemailer), custom send functions for third-party providers (AWS SES, Resend, Postmark, etc.), and fully customizable HTML/text templates.
|
|
9
|
-
|
|
10
|
-
> **IMPORTANT FOR AGENTS:** Always read the `rebase-basics` skill first. Email requires a running Rebase backend with `initializeRebaseBackend()` and is configured through the `auth.email` property.
|
|
11
|
-
|
|
12
|
-
## EmailConfig Interface
|
|
13
|
-
|
|
14
|
-
Email is configured via the `email` property inside `auth` in `initializeRebaseBackend()`:
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
import { initializeRebaseBackend } from "@rebasepro/server-core";
|
|
18
|
-
|
|
19
|
-
const backend = await initializeRebaseBackend({
|
|
20
|
-
server, app,
|
|
21
|
-
database: postgresAdapter,
|
|
22
|
-
auth: {
|
|
23
|
-
collection: usersCollection,
|
|
24
|
-
jwtSecret: env.JWT_SECRET,
|
|
25
|
-
email: {
|
|
26
|
-
from: "MyApp <noreply@myapp.com>",
|
|
27
|
-
smtp: {
|
|
28
|
-
host: "smtp.gmail.com",
|
|
29
|
-
port: 587,
|
|
30
|
-
auth: { user: "user@gmail.com", pass: "app-password" },
|
|
31
|
-
},
|
|
32
|
-
appName: "MyApp",
|
|
33
|
-
resetPasswordUrl: "https://myapp.com",
|
|
34
|
-
verifyEmailUrl: "https://myapp.com",
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### EmailConfig Properties
|
|
41
|
-
|
|
42
|
-
| Property | Type | Default | Description |
|
|
43
|
-
|----------|------|---------|-------------|
|
|
44
|
-
| `from` | `string` | — | **Required.** Sender address. Format: `"noreply@example.com"` or `"MyApp <noreply@example.com>"` |
|
|
45
|
-
| `smtp` | `SMTPConfig` | `undefined` | SMTP server configuration (see below) |
|
|
46
|
-
| `sendEmail` | `(options: EmailSendOptions) => Promise<void>` | `undefined` | Custom send function — replaces SMTP entirely |
|
|
47
|
-
| `resetPasswordUrl` | `string` | `""` | Base URL for password reset links. The reset link becomes `{resetPasswordUrl}/reset-password?token={token}` |
|
|
48
|
-
| `verifyEmailUrl` | `string` | `""` | Base URL for email verification links. The link becomes `{verifyEmailUrl}/verify-email?token={token}` |
|
|
49
|
-
| `appName` | `string` | `"Rebase"` | Application name used in email subjects and body text |
|
|
50
|
-
| `templates` | `object` | `undefined` | Custom template functions to override built-in templates (see [Custom Templates](#custom-templates)) |
|
|
51
|
-
|
|
52
|
-
> **IMPORTANT FOR AGENTS:** You must provide **either** `smtp` or `sendEmail` (not both). If neither is provided, the email service reports `isConfigured() === false` and all email-dependent auth flows (forgot-password, send-verification, user invitation) will fail with a `503 EMAIL_NOT_CONFIGURED` error.
|
|
53
|
-
|
|
54
|
-
### SMTPConfig Properties
|
|
55
|
-
|
|
56
|
-
| Property | Type | Default | Description |
|
|
57
|
-
|----------|------|---------|-------------|
|
|
58
|
-
| `host` | `string` | — | **Required.** SMTP server hostname (e.g., `"smtp.gmail.com"`) |
|
|
59
|
-
| `port` | `number` | — | **Required.** SMTP server port (typically `587` for STARTTLS, `465` for SSL) |
|
|
60
|
-
| `secure` | `boolean` | `true` when `port === 465`, otherwise `false` | Use SSL/TLS. Auto-inferred from port when not set |
|
|
61
|
-
| `auth` | `{ user: string; pass: string }` | `undefined` | SMTP credentials. Omit for unauthenticated SMTP relays |
|
|
62
|
-
| `name` | `string` | Auto-detected from `FRONTEND_URL` | Client hostname sent in SMTP `EHLO`/`HELO` greeting (see [SMTP Name Resolution](#smtp-name-resolution)) |
|
|
63
|
-
|
|
64
|
-
### EmailSendOptions
|
|
65
|
-
|
|
66
|
-
The `EmailSendOptions` interface is used by both `emailService.send()` and custom `sendEmail` functions:
|
|
67
|
-
|
|
68
|
-
| Property | Type | Required | Description |
|
|
69
|
-
|----------|------|----------|-------------|
|
|
70
|
-
| `to` | `string \| string[]` | Yes | Recipient email address(es) |
|
|
71
|
-
| `subject` | `string` | Yes | Email subject line |
|
|
72
|
-
| `html` | `string` | Yes | HTML body content |
|
|
73
|
-
| `text` | `string` | No | Plain-text fallback body |
|
|
74
|
-
| `replyTo` | `string` | No | Reply-to email address |
|
|
75
|
-
|
|
76
|
-
### EmailService Interface
|
|
77
|
-
|
|
78
|
-
The `EmailService` interface is exposed on the `rebase.email` singleton:
|
|
79
|
-
|
|
80
|
-
| Method | Signature | Description |
|
|
81
|
-
|--------|-----------|-------------|
|
|
82
|
-
| `send` | `(options: EmailSendOptions) => Promise<void>` | Send a single email |
|
|
83
|
-
| `isConfigured` | `() => boolean` | Returns `true` when SMTP or a custom `sendEmail` function is configured |
|
|
84
|
-
| `verifyConnection` | `() => Promise<boolean>` | *(Optional)* Test SMTP connectivity. Returns `true` on success, `false` on failure. For custom `sendEmail`, returns `true` if the function is set |
|
|
85
|
-
|
|
86
|
-
## SMTP Setup
|
|
87
|
-
|
|
88
|
-
### Basic SMTP Configuration
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
auth: {
|
|
92
|
-
// ...
|
|
93
|
-
email: {
|
|
94
|
-
from: "noreply@myapp.com",
|
|
95
|
-
appName: "MyApp",
|
|
96
|
-
smtp: {
|
|
97
|
-
host: "smtp.gmail.com",
|
|
98
|
-
port: 587,
|
|
99
|
-
auth: {
|
|
100
|
-
user: "user@gmail.com",
|
|
101
|
-
pass: "your-app-password",
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
resetPasswordUrl: "https://myapp.com",
|
|
105
|
-
verifyEmailUrl: "https://myapp.com",
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Common SMTP Providers
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
// Gmail (requires App Password with 2FA enabled)
|
|
114
|
-
smtp: { host: "smtp.gmail.com", port: 587, auth: { user: "you@gmail.com", pass: "xxxx xxxx xxxx xxxx" } }
|
|
115
|
-
|
|
116
|
-
// Amazon SES
|
|
117
|
-
smtp: { host: "email-smtp.us-east-1.amazonaws.com", port: 587, auth: { user: "AKIAIOSFODNN7EXAMPLE", pass: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" } }
|
|
118
|
-
|
|
119
|
-
// Resend SMTP
|
|
120
|
-
smtp: { host: "smtp.resend.com", port: 465, secure: true, auth: { user: "resend", pass: "re_xxxxxxxxxxxx" } }
|
|
121
|
-
|
|
122
|
-
// Postmark
|
|
123
|
-
smtp: { host: "smtp.postmarkapp.com", port: 587, auth: { user: "your-server-api-token", pass: "your-server-api-token" } }
|
|
124
|
-
|
|
125
|
-
// Mailtrap (testing)
|
|
126
|
-
smtp: { host: "sandbox.smtp.mailtrap.io", port: 2525, auth: { user: "your-mailtrap-user", pass: "your-mailtrap-pass" } }
|
|
127
|
-
|
|
128
|
-
// SendGrid
|
|
129
|
-
smtp: { host: "smtp.sendgrid.net", port: 587, auth: { user: "apikey", pass: "SG.xxxxxxxxxxxxx" } }
|
|
130
|
-
|
|
131
|
-
// Mailgun
|
|
132
|
-
smtp: { host: "smtp.mailgun.org", port: 587, auth: { user: "postmaster@your-domain.mailgun.org", pass: "your-mailgun-key" } }
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Using Environment Variables for SMTP
|
|
136
|
-
|
|
137
|
-
A common pattern is to use `loadEnv` with extended variables:
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
import { loadEnv } from "@rebasepro/server-core";
|
|
141
|
-
import { z } from "zod";
|
|
142
|
-
|
|
143
|
-
export const env = loadEnv({
|
|
144
|
-
extend: z.object({
|
|
145
|
-
SMTP_HOST: z.string().optional(),
|
|
146
|
-
SMTP_PORT: z.string().default("587").transform(Number),
|
|
147
|
-
SMTP_SECURE: z.enum(["true", "false", ""]).default("false").transform(v => v === "true"),
|
|
148
|
-
SMTP_USER: z.string().optional(),
|
|
149
|
-
SMTP_PASS: z.string().optional(),
|
|
150
|
-
SMTP_FROM: z.string().default("noreply@example.com"),
|
|
151
|
-
}),
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Then in your backend config:
|
|
155
|
-
auth: {
|
|
156
|
-
// ...
|
|
157
|
-
email: {
|
|
158
|
-
from: env.SMTP_FROM,
|
|
159
|
-
appName: "MyApp",
|
|
160
|
-
smtp: env.SMTP_HOST ? {
|
|
161
|
-
host: env.SMTP_HOST,
|
|
162
|
-
port: env.SMTP_PORT,
|
|
163
|
-
secure: env.SMTP_SECURE,
|
|
164
|
-
auth: env.SMTP_USER ? {
|
|
165
|
-
user: env.SMTP_USER,
|
|
166
|
-
pass: env.SMTP_PASS!,
|
|
167
|
-
} : undefined,
|
|
168
|
-
} : undefined,
|
|
169
|
-
resetPasswordUrl: env.FRONTEND_URL || "http://localhost:3000",
|
|
170
|
-
verifyEmailUrl: env.FRONTEND_URL || "http://localhost:3000",
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
> **IMPORTANT FOR AGENTS:** SMTP-related variables (`SMTP_HOST`, `SMTP_PORT`, etc.) are **NOT** part of the base Rebase environment schema. They must be added via `loadEnv({ extend: ... })`. The base schema does include `FRONTEND_URL` which is used for SMTP name resolution.
|
|
176
|
-
|
|
177
|
-
### `.env` File Example
|
|
178
|
-
|
|
179
|
-
```env
|
|
180
|
-
# SMTP Configuration
|
|
181
|
-
SMTP_HOST=smtp.gmail.com
|
|
182
|
-
SMTP_PORT=587
|
|
183
|
-
SMTP_SECURE=false
|
|
184
|
-
SMTP_USER=noreply@myapp.com
|
|
185
|
-
SMTP_PASS=your-app-password
|
|
186
|
-
SMTP_FROM=MyApp <noreply@myapp.com>
|
|
187
|
-
|
|
188
|
-
# Used for password reset / verification links AND SMTP EHLO name
|
|
189
|
-
FRONTEND_URL=https://myapp.com
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## Built-in Email Templates
|
|
193
|
-
|
|
194
|
-
Rebase ships four built-in HTML email templates. All templates include both HTML and plain-text versions, use responsive inline CSS, and follow a consistent card-based design with a blue (#3b82f6) call-to-action button.
|
|
195
|
-
|
|
196
|
-
### Template Overview
|
|
197
|
-
|
|
198
|
-
| Template | Trigger | Subject | Token Expiry |
|
|
199
|
-
|----------|---------|---------|-------------|
|
|
200
|
-
| Password Reset | `POST /api/auth/forgot-password` | `"Reset your {appName} password"` | 1 hour |
|
|
201
|
-
| Email Verification | `POST /api/auth/send-verification` | `"Verify your {appName} email address"` | No expiry (stored as user field) |
|
|
202
|
-
| User Invitation | `POST /api/admin/users` (admin-created user, no explicit password) | `"You've been invited to {appName}"` | 1 hour (uses password reset token) |
|
|
203
|
-
| Welcome Email | `POST /api/auth/register` and OAuth first-login | `"¡Bienvenido/a a {appName}!"` | N/A (informational) |
|
|
204
|
-
|
|
205
|
-
### Template Functions
|
|
206
|
-
|
|
207
|
-
Each built-in template is a standalone function exported from `@rebasepro/server-core`:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
import {
|
|
211
|
-
getPasswordResetTemplate,
|
|
212
|
-
getEmailVerificationTemplate,
|
|
213
|
-
getUserInvitationTemplate,
|
|
214
|
-
getWelcomeEmailTemplate,
|
|
215
|
-
} from "@rebasepro/server-core";
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
#### getPasswordResetTemplate
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
function getPasswordResetTemplate(
|
|
222
|
-
resetUrl: string,
|
|
223
|
-
user: { email: string; displayName?: string | null },
|
|
224
|
-
appName?: string // default: "Rebase"
|
|
225
|
-
): { subject: string; html: string; text: string }
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
The template includes a "Reset Password" button linking to `resetUrl`, a plain-text fallback with the URL, and a warning that the link expires in 1 hour.
|
|
229
|
-
|
|
230
|
-
#### getEmailVerificationTemplate
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
function getEmailVerificationTemplate(
|
|
234
|
-
verifyUrl: string,
|
|
235
|
-
user: { email: string; displayName?: string | null },
|
|
236
|
-
appName?: string // default: "Rebase"
|
|
237
|
-
): { subject: string; html: string; text: string }
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
The template includes a "Verify Email Address" button linking to `verifyUrl`.
|
|
241
|
-
|
|
242
|
-
#### getUserInvitationTemplate
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
function getUserInvitationTemplate(
|
|
246
|
-
setPasswordUrl: string,
|
|
247
|
-
user: { email: string; displayName?: string | null },
|
|
248
|
-
appName?: string // default: "Rebase"
|
|
249
|
-
): { subject: string; html: string; text: string }
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
Sent when an admin creates a user via `POST /api/admin/users` without specifying a password. The template includes a "Set Your Password" button and a 1-hour expiry warning.
|
|
253
|
-
|
|
254
|
-
#### getWelcomeEmailTemplate
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
function getWelcomeEmailTemplate(
|
|
258
|
-
user: { email: string; displayName?: string | null },
|
|
259
|
-
appName?: string, // default: "Rebase"
|
|
260
|
-
loginUrl?: string
|
|
261
|
-
): { subject: string; html: string; text: string }
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
> **IMPORTANT FOR AGENTS:** The default welcome email template is in Spanish. The subject is `"¡Bienvenido/a a {appName}!"` and the body is Spanish text. Override this template if you need a different language.
|
|
265
|
-
|
|
266
|
-
### Template User Greeting
|
|
267
|
-
|
|
268
|
-
All templates greet the user by `displayName` if available, otherwise by the email username (the part before `@`). For example, `user@example.com` is greeted as `"Hi user"`.
|
|
269
|
-
|
|
270
|
-
## Custom Templates
|
|
271
|
-
|
|
272
|
-
Override any built-in template by providing a function in `emailConfig.templates`:
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
auth: {
|
|
276
|
-
email: {
|
|
277
|
-
from: "noreply@myapp.com",
|
|
278
|
-
appName: "MyApp",
|
|
279
|
-
smtp: { /* ... */ },
|
|
280
|
-
resetPasswordUrl: "https://myapp.com",
|
|
281
|
-
verifyEmailUrl: "https://myapp.com",
|
|
282
|
-
templates: {
|
|
283
|
-
passwordReset: (resetUrl, user) => ({
|
|
284
|
-
subject: `Reset your password, ${user.displayName || user.email}`,
|
|
285
|
-
html: `<h1>Password Reset</h1><p>Click <a href="${resetUrl}">here</a> to reset.</p>`,
|
|
286
|
-
text: `Reset your password: ${resetUrl}`,
|
|
287
|
-
}),
|
|
288
|
-
emailVerification: (verifyUrl, user) => ({
|
|
289
|
-
subject: "Please verify your email",
|
|
290
|
-
html: `<p>Hi ${user.displayName || user.email}, <a href="${verifyUrl}">verify here</a></p>`,
|
|
291
|
-
}),
|
|
292
|
-
userInvitation: (setPasswordUrl, user) => ({
|
|
293
|
-
subject: "You've been invited!",
|
|
294
|
-
html: `<p>Welcome! Set your password: <a href="${setPasswordUrl}">${setPasswordUrl}</a></p>`,
|
|
295
|
-
}),
|
|
296
|
-
welcomeEmail: (user, appName) => ({
|
|
297
|
-
subject: `Welcome to ${appName}!`,
|
|
298
|
-
html: `<h1>Welcome, ${user.displayName || user.email}!</h1><p>Thanks for joining ${appName}.</p>`,
|
|
299
|
-
text: `Welcome to ${appName}!`,
|
|
300
|
-
}),
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Template Function Signatures
|
|
307
|
-
|
|
308
|
-
| Template Key | Signature |
|
|
309
|
-
|-------------|-----------|
|
|
310
|
-
| `passwordReset` | `(resetUrl: string, user: { email: string; displayName?: string \| null }) => { subject: string; html: string; text?: string }` |
|
|
311
|
-
| `emailVerification` | `(verifyUrl: string, user: { email: string; displayName?: string \| null }) => { subject: string; html: string; text?: string }` |
|
|
312
|
-
| `userInvitation` | `(setPasswordUrl: string, user: { email: string; displayName?: string \| null }) => { subject: string; html: string; text?: string }` |
|
|
313
|
-
| `welcomeEmail` | `(user: { email: string; displayName?: string \| null }, appName: string) => { subject: string; html: string; text?: string }` |
|
|
314
|
-
|
|
315
|
-
> **IMPORTANT FOR AGENTS:** The `text` field is optional in custom templates but recommended for deliverability. Many spam filters penalize emails without a plain-text alternative.
|
|
316
|
-
|
|
317
|
-
## Custom Send Functions
|
|
318
|
-
|
|
319
|
-
Replace SMTP entirely with a custom `sendEmail` function for third-party email APIs:
|
|
320
|
-
|
|
321
|
-
### AWS SES Example
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
|
|
325
|
-
|
|
326
|
-
const ses = new SESClient({ region: "us-east-1" });
|
|
327
|
-
|
|
328
|
-
auth: {
|
|
329
|
-
email: {
|
|
330
|
-
from: "noreply@myapp.com",
|
|
331
|
-
appName: "MyApp",
|
|
332
|
-
resetPasswordUrl: "https://myapp.com",
|
|
333
|
-
verifyEmailUrl: "https://myapp.com",
|
|
334
|
-
sendEmail: async (options) => {
|
|
335
|
-
const to = Array.isArray(options.to) ? options.to : [options.to];
|
|
336
|
-
await ses.send(new SendEmailCommand({
|
|
337
|
-
Source: "noreply@myapp.com",
|
|
338
|
-
Destination: { ToAddresses: to },
|
|
339
|
-
Message: {
|
|
340
|
-
Subject: { Data: options.subject },
|
|
341
|
-
Body: {
|
|
342
|
-
Html: { Data: options.html },
|
|
343
|
-
...(options.text ? { Text: { Data: options.text } } : {}),
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
...(options.replyTo ? { ReplyToAddresses: [options.replyTo] } : {}),
|
|
347
|
-
}));
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
}
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
### Resend API Example
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
import { Resend } from "resend";
|
|
357
|
-
|
|
358
|
-
const resend = new Resend("re_xxxxxxxxxxxx");
|
|
359
|
-
|
|
360
|
-
auth: {
|
|
361
|
-
email: {
|
|
362
|
-
from: "MyApp <noreply@myapp.com>",
|
|
363
|
-
appName: "MyApp",
|
|
364
|
-
resetPasswordUrl: "https://myapp.com",
|
|
365
|
-
verifyEmailUrl: "https://myapp.com",
|
|
366
|
-
sendEmail: async (options) => {
|
|
367
|
-
await resend.emails.send({
|
|
368
|
-
from: "MyApp <noreply@myapp.com>",
|
|
369
|
-
to: Array.isArray(options.to) ? options.to : [options.to],
|
|
370
|
-
subject: options.subject,
|
|
371
|
-
html: options.html,
|
|
372
|
-
text: options.text,
|
|
373
|
-
reply_to: options.replyTo,
|
|
374
|
-
});
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### Postmark API Example
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
import { ServerClient } from "postmark";
|
|
384
|
-
|
|
385
|
-
const postmark = new ServerClient("your-server-api-token");
|
|
386
|
-
|
|
387
|
-
auth: {
|
|
388
|
-
email: {
|
|
389
|
-
from: "noreply@myapp.com",
|
|
390
|
-
appName: "MyApp",
|
|
391
|
-
resetPasswordUrl: "https://myapp.com",
|
|
392
|
-
verifyEmailUrl: "https://myapp.com",
|
|
393
|
-
sendEmail: async (options) => {
|
|
394
|
-
await postmark.sendEmail({
|
|
395
|
-
From: "noreply@myapp.com",
|
|
396
|
-
To: Array.isArray(options.to) ? options.to.join(",") : options.to,
|
|
397
|
-
Subject: options.subject,
|
|
398
|
-
HtmlBody: options.html,
|
|
399
|
-
TextBody: options.text || "",
|
|
400
|
-
ReplyTo: options.replyTo,
|
|
401
|
-
});
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
}
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
> **IMPORTANT FOR AGENTS:** When using `sendEmail`, the `from` field in `EmailConfig` is **not** automatically applied by the email service. Your custom function receives the `EmailSendOptions` (which has `to`, `subject`, `html`, `text`, `replyTo` but **no** `from`). You must set the sender address inside your function.
|
|
408
|
-
|
|
409
|
-
## `rebase.email` Singleton
|
|
410
|
-
|
|
411
|
-
When email is configured, the email service is automatically attached to the `rebase` server-side singleton as `rebase.email`. This allows sending emails programmatically from custom functions, cron jobs, and entity callbacks.
|
|
412
|
-
|
|
413
|
-
```typescript
|
|
414
|
-
import { rebase } from "@rebasepro/server-core";
|
|
415
|
-
|
|
416
|
-
// Check if email is available
|
|
417
|
-
if (rebase.email) {
|
|
418
|
-
await rebase.email.send({
|
|
419
|
-
to: "user@example.com",
|
|
420
|
-
subject: "Your order has shipped",
|
|
421
|
-
html: "<h1>Order Shipped!</h1><p>Your order #12345 is on its way.</p>",
|
|
422
|
-
text: "Your order #12345 has shipped.",
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### Using in Custom Functions
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
// functions/send-report.ts
|
|
431
|
-
import { defineFunction } from "@rebasepro/server-core";
|
|
432
|
-
|
|
433
|
-
export default defineFunction({
|
|
434
|
-
method: "POST",
|
|
435
|
-
handler: async (ctx) => {
|
|
436
|
-
const { email, reportData } = ctx.body;
|
|
437
|
-
|
|
438
|
-
if (!ctx.client.email) {
|
|
439
|
-
return ctx.json({ error: "Email not configured" }, 503);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
await ctx.client.email.send({
|
|
443
|
-
to: email,
|
|
444
|
-
subject: "Your Monthly Report",
|
|
445
|
-
html: `<h1>Report</h1><pre>${JSON.stringify(reportData, null, 2)}</pre>`,
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
return ctx.json({ success: true, message: "Report sent" });
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Using in Cron Jobs
|
|
454
|
-
|
|
455
|
-
```typescript
|
|
456
|
-
// crons/weekly-digest.ts
|
|
457
|
-
import { defineCron } from "@rebasepro/server-core";
|
|
458
|
-
|
|
459
|
-
export default defineCron({
|
|
460
|
-
schedule: "0 9 * * 1", // Every Monday at 9 AM
|
|
461
|
-
handler: async (ctx) => {
|
|
462
|
-
if (!ctx.client.email) {
|
|
463
|
-
console.warn("Email not configured, skipping digest");
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Fetch subscribers
|
|
468
|
-
const { data: subscribers } = await ctx.client.data
|
|
469
|
-
.collection("subscribers")
|
|
470
|
-
.get({ filter: { active: true } });
|
|
471
|
-
|
|
472
|
-
for (const sub of subscribers) {
|
|
473
|
-
await ctx.client.email.send({
|
|
474
|
-
to: sub.email,
|
|
475
|
-
subject: "Your Weekly Digest",
|
|
476
|
-
html: "<h1>This Week's Updates</h1>...",
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### Checking Email Configuration
|
|
484
|
-
|
|
485
|
-
```typescript
|
|
486
|
-
import { rebase } from "@rebasepro/server-core";
|
|
487
|
-
|
|
488
|
-
// rebase.email is undefined when no email config is provided at all
|
|
489
|
-
if (!rebase.email) {
|
|
490
|
-
console.log("Email not configured");
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// rebase.email exists but may not be functional
|
|
494
|
-
if (rebase.email && !rebase.email.isConfigured()) {
|
|
495
|
-
console.log("Email service exists but has no SMTP or sendEmail function");
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Full check
|
|
499
|
-
if (rebase.email?.isConfigured()) {
|
|
500
|
-
console.log("Email is ready to send");
|
|
501
|
-
}
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
## Integration with Auth Flows
|
|
505
|
-
|
|
506
|
-
The email system is tightly integrated with Rebase authentication. Here is how each flow works:
|
|
507
|
-
|
|
508
|
-
### Password Reset Flow
|
|
509
|
-
|
|
510
|
-
| Step | Endpoint | Description |
|
|
511
|
-
|------|----------|-------------|
|
|
512
|
-
| 1 | `POST /api/auth/forgot-password` | User submits `{ email }`. Server generates a SHA-256 hashed token, stores it with a 1-hour expiry, and sends the password reset email |
|
|
513
|
-
| 2 | User clicks link | User is directed to `{resetPasswordUrl}/reset-password?token={token}` |
|
|
514
|
-
| 3 | `POST /api/auth/reset-password` | Client submits `{ token, password }`. Server verifies the token, updates the password, marks the token as used, and invalidates all refresh tokens (logs out all sessions) |
|
|
515
|
-
|
|
516
|
-
> **WARNING FOR AGENTS:** The `POST /api/auth/forgot-password` endpoint **always** returns `{ success: true }` regardless of whether the email exists. This is a security measure to prevent email enumeration. If the email service is not configured, it throws `503 EMAIL_NOT_CONFIGURED`.
|
|
517
|
-
|
|
518
|
-
### Email Verification Flow
|
|
519
|
-
|
|
520
|
-
| Step | Endpoint | Description |
|
|
521
|
-
|------|----------|-------------|
|
|
522
|
-
| 1 | `POST /api/auth/send-verification` | Authenticated user requests verification. Server generates a token, stores the SHA-256 hash on the user record, and sends the verification email |
|
|
523
|
-
| 2 | User clicks link | User is directed to `{verifyEmailUrl}/verify-email?token={token}` |
|
|
524
|
-
| 3 | `GET /api/auth/verify-email?token={token}` | Server looks up the user by hashed token and sets `emailVerified = true` |
|
|
525
|
-
|
|
526
|
-
The `send-verification` endpoint requires authentication and returns `400 ALREADY_VERIFIED` if the email is already verified.
|
|
527
|
-
|
|
528
|
-
### User Invitation Flow (Admin-Created Users)
|
|
529
|
-
|
|
530
|
-
| Step | Endpoint | Description |
|
|
531
|
-
|------|----------|-------------|
|
|
532
|
-
| 1 | `POST /api/admin/users` | Admin creates user with `{ email, displayName, roles }` (no password). Server auto-generates a password, creates a password reset token, and sends the invitation email |
|
|
533
|
-
| 2 | User clicks link | User is directed to `{resetPasswordUrl}/reset-password?token={token}` (same as password reset) |
|
|
534
|
-
| 3 | `POST /api/auth/reset-password` | User sets their password via the standard reset flow |
|
|
535
|
-
|
|
536
|
-
**Fallback behavior:** If the email service is not configured or the email fails to send, the API response includes a `temporaryPassword` field that the admin can share manually:
|
|
537
|
-
|
|
538
|
-
```json
|
|
539
|
-
{
|
|
540
|
-
"user": { "uid": "...", "email": "new@user.com", "roles": ["editor"] },
|
|
541
|
-
"invitationSent": false,
|
|
542
|
-
"temporaryPassword": "aB3xKm9pQr2sT5wY"
|
|
543
|
-
}
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
When the invitation email is sent successfully:
|
|
547
|
-
|
|
548
|
-
```json
|
|
549
|
-
{
|
|
550
|
-
"user": { "uid": "...", "email": "new@user.com", "roles": ["editor"] },
|
|
551
|
-
"invitationSent": true
|
|
552
|
-
}
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### Admin Password Reset
|
|
556
|
-
|
|
557
|
-
| Step | Endpoint | Description |
|
|
558
|
-
|------|----------|-------------|
|
|
559
|
-
| 1 | `POST /api/admin/users/:userId/reset-password` | Admin triggers a password reset for a specific user. If email is configured, sends a reset email. Otherwise, generates a new password and returns it as `temporaryPassword` |
|
|
560
|
-
|
|
561
|
-
### Welcome Email
|
|
562
|
-
|
|
563
|
-
Sent automatically (fire-and-forget) when:
|
|
564
|
-
- A user registers via `POST /api/auth/register`
|
|
565
|
-
- A new user is created via an OAuth provider (first login)
|
|
566
|
-
|
|
567
|
-
The welcome email is **not sent** when an admin creates a user via `POST /api/admin/users` (the invitation email is sent instead).
|
|
568
|
-
|
|
569
|
-
The welcome email uses `resetPasswordUrl` as the base for a login link, appending `/app` (e.g., `https://myapp.com/app`).
|
|
570
|
-
|
|
571
|
-
## SMTP Name Resolution
|
|
572
|
-
|
|
573
|
-
When creating the Nodemailer transport, Rebase automatically resolves a hostname for the SMTP `EHLO`/`HELO` greeting. This is important because some SMTP servers (e.g., Gmail, Outlook) reject connections from clients that identify as `localhost`.
|
|
574
|
-
|
|
575
|
-
**Resolution order:**
|
|
576
|
-
|
|
577
|
-
| Priority | Source | Description |
|
|
578
|
-
|----------|--------|-------------|
|
|
579
|
-
| 1 | `smtp.name` | Explicitly set in the `SMTPConfig` |
|
|
580
|
-
| 2 | `FRONTEND_URL` env var | Hostname extracted from the URL |
|
|
581
|
-
| 3 | `emailConfig.resetPasswordUrl` | Hostname extracted from the URL |
|
|
582
|
-
| 4 | `emailConfig.verifyEmailUrl` | Hostname extracted from the URL |
|
|
583
|
-
|
|
584
|
-
```typescript
|
|
585
|
-
// If FRONTEND_URL=https://myapp.com, the EHLO name will be "myapp.com"
|
|
586
|
-
// The URL is parsed safely — invalid URLs are skipped
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
> **IMPORTANT FOR AGENTS:** If you encounter SMTP delivery issues (especially with Gmail or corporate SMTP servers), ensure `FRONTEND_URL` is set to a valid URL with a real domain, or set `smtp.name` explicitly. Sending EHLO with `localhost` will cause many SMTP servers to reject the connection.
|
|
590
|
-
|
|
591
|
-
## SMTP Connection Verification
|
|
592
|
-
|
|
593
|
-
At startup, Rebase automatically verifies the SMTP connection when email is configured. This is a non-blocking check that logs the result:
|
|
594
|
-
|
|
595
|
-
- **Success:** `"SMTP connection verified successfully."`
|
|
596
|
-
- **Failure:** `"Warning: SMTP connection verification failed. Email delivery may fail."`
|
|
597
|
-
|
|
598
|
-
The verification calls Nodemailer's `transporter.verify()` which attempts to connect and authenticate with the SMTP server. **Startup is not blocked** — the verification runs asynchronously after the backend is initialized.
|
|
599
|
-
|
|
600
|
-
When using a custom `sendEmail` function instead of SMTP, `verifyConnection()` simply returns `true` (there is no transport to verify).
|
|
601
|
-
|
|
602
|
-
## Complete Configuration Example
|
|
603
|
-
|
|
604
|
-
```typescript
|
|
605
|
-
import { Hono } from "hono";
|
|
606
|
-
import { serve } from "@hono/node-server";
|
|
607
|
-
import { initializeRebaseBackend, loadEnv } from "@rebasepro/server-core";
|
|
608
|
-
import { createPostgresAdapter } from "@rebasepro/server-postgresql";
|
|
609
|
-
import { defaultUsersCollection } from "@rebasepro/common";
|
|
610
|
-
import { z } from "zod";
|
|
611
|
-
|
|
612
|
-
// 1. Load and validate environment
|
|
613
|
-
const env = loadEnv({
|
|
614
|
-
extend: z.object({
|
|
615
|
-
SMTP_HOST: z.string().optional(),
|
|
616
|
-
SMTP_PORT: z.string().default("587").transform(Number),
|
|
617
|
-
SMTP_SECURE: z.enum(["true", "false", ""]).default("false").transform(v => v === "true"),
|
|
618
|
-
SMTP_USER: z.string().optional(),
|
|
619
|
-
SMTP_PASS: z.string().optional(),
|
|
620
|
-
SMTP_FROM: z.string().default("noreply@example.com"),
|
|
621
|
-
APP_NAME: z.string().default("MyApp"),
|
|
622
|
-
}),
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
// 2. Set up Hono and HTTP server
|
|
626
|
-
const app = new Hono();
|
|
627
|
-
const server = serve({ fetch: app.fetch, port: env.PORT });
|
|
628
|
-
|
|
629
|
-
// 3. Create database adapter
|
|
630
|
-
const postgresAdapter = createPostgresAdapter({
|
|
631
|
-
connectionString: env.DATABASE_URL,
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
// 4. Initialize backend with email
|
|
635
|
-
await initializeRebaseBackend({
|
|
636
|
-
server, app,
|
|
637
|
-
database: postgresAdapter,
|
|
638
|
-
auth: {
|
|
639
|
-
collection: defaultUsersCollection,
|
|
640
|
-
jwtSecret: env.JWT_SECRET,
|
|
641
|
-
serviceKey: env.REBASE_SERVICE_KEY,
|
|
642
|
-
allowRegistration: env.ALLOW_REGISTRATION,
|
|
643
|
-
email: {
|
|
644
|
-
from: env.SMTP_FROM,
|
|
645
|
-
appName: env.APP_NAME,
|
|
646
|
-
smtp: env.SMTP_HOST ? {
|
|
647
|
-
host: env.SMTP_HOST,
|
|
648
|
-
port: env.SMTP_PORT,
|
|
649
|
-
secure: env.SMTP_SECURE,
|
|
650
|
-
auth: env.SMTP_USER ? {
|
|
651
|
-
user: env.SMTP_USER,
|
|
652
|
-
pass: env.SMTP_PASS!,
|
|
653
|
-
} : undefined,
|
|
654
|
-
} : undefined,
|
|
655
|
-
resetPasswordUrl: env.FRONTEND_URL || "http://localhost:3000",
|
|
656
|
-
verifyEmailUrl: env.FRONTEND_URL || "http://localhost:3000",
|
|
657
|
-
templates: {
|
|
658
|
-
// Override the default Spanish welcome email
|
|
659
|
-
welcomeEmail: (user, appName) => ({
|
|
660
|
-
subject: `Welcome to ${appName}!`,
|
|
661
|
-
html: `<h1>Welcome!</h1><p>Hi ${user.displayName || user.email}, thanks for joining ${appName}.</p>`,
|
|
662
|
-
text: `Welcome to ${appName}! Thanks for joining.`,
|
|
663
|
-
}),
|
|
664
|
-
},
|
|
665
|
-
},
|
|
666
|
-
},
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
console.log(`Server running on port ${env.PORT}`);
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
## Error Handling
|
|
673
|
-
|
|
674
|
-
### Email Service Errors
|
|
675
|
-
|
|
676
|
-
| Error | Condition | HTTP Status |
|
|
677
|
-
|-------|-----------|-------------|
|
|
678
|
-
| `"Email service not configured. Password reset is not available."` | `POST /api/auth/forgot-password` when no email configured | 503 |
|
|
679
|
-
| `"Email service not configured. Email verification is not available."` | `POST /api/auth/send-verification` when no email configured | 503 |
|
|
680
|
-
| `"Email is already verified"` | `POST /api/auth/send-verification` for a verified user | 400 |
|
|
681
|
-
| `"Invalid or expired reset token"` | `POST /api/auth/reset-password` with bad/expired token | 400 |
|
|
682
|
-
| `"Invalid or expired verification token"` | `GET /api/auth/verify-email` with bad token | 400 |
|
|
683
|
-
| `"Email service not configured. Provide SMTP config or sendEmail function."` | Calling `emailService.send()` with no transport | Error thrown |
|
|
684
|
-
| `"Failed to send email: {message}"` | SMTP send failure | Error thrown |
|
|
685
|
-
|
|
686
|
-
### Silent Failures
|
|
687
|
-
|
|
688
|
-
The following email failures are logged but do **not** cause the API request to fail:
|
|
689
|
-
|
|
690
|
-
- **Password reset email:** If `send()` throws, the error is logged but `POST /api/auth/forgot-password` still returns `{ success: true }` (security: don't reveal failures)
|
|
691
|
-
- **Welcome email:** Sent fire-and-forget — failures are logged but don't block registration
|
|
692
|
-
- **User invitation email:** If `send()` throws, the response falls back to returning `temporaryPassword`
|
|
693
|
-
|
|
694
|
-
## References
|
|
695
|
-
|
|
696
|
-
- **Documentation:** [rebase.pro/docs](https://rebase.pro/docs)
|
|
697
|
-
- **GitHub:** [github.com/rebasepro/rebase](https://github.com/rebasepro/rebase)
|
|
698
|
-
- **Nodemailer:** [nodemailer.com](https://nodemailer.com)
|
|
699
|
-
- **AWS SES SDK:** [docs.aws.amazon.com/ses](https://docs.aws.amazon.com/ses/latest/dg/send-email-api.html)
|
|
700
|
-
- **Resend:** [resend.com/docs](https://resend.com/docs)
|
|
701
|
-
- **Postmark:** [postmarkapp.com/developer](https://postmarkapp.com/developer)
|