@polymorphism-tech/morph-spec 6.0.0 → 7.1.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 +367 -366
- package/docs/CHEATSHEET.md +3 -3
- package/docs/QUICKSTART.md +4 -4
- package/framework/CLAUDE.md +13 -0
- package/framework/MORPH.md +57 -36
- package/framework/agents.json +132 -127
- package/framework/commands/morph-apply.md +4 -21
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-proposal.md +2 -0
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/core/state-sync.js +212 -0
- package/framework/hooks/shared/phase-utils.js +5 -4
- package/framework/hooks/shared/state-reader.js +1 -1
- package/framework/phases.json +1 -1
- package/framework/rubrics/backend.json +41 -36
- package/framework/rules/ai-agents-standards.md +45 -15
- package/framework/rules/morph-workflow.md +15 -34
- package/framework/rules/neon-standards.md +25 -26
- package/framework/schemas/tasks.schema.json +35 -0
- package/framework/skills/core/morph-apply/SKILL.md +154 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +53 -30
- package/framework/skills/core/morph-implement/SKILL.md +25 -14
- package/framework/skills/core/morph-plan/SKILL.md +91 -23
- package/framework/skills/core/morph-proposal/SKILL.md +137 -0
- package/framework/skills/core/morph-review/SKILL.md +43 -7
- package/framework/skills/core/morph-stack-scan/SKILL.md +40 -1
- package/framework/standards/STANDARDS.json +1729 -1134
- package/framework/standards/ai-agents/agent-session.md +160 -0
- package/framework/standards/ai-agents/batching.md +193 -0
- package/framework/standards/ai-agents/context-providers.md +188 -0
- package/framework/standards/ai-agents/mcp-server.md +250 -0
- package/framework/standards/ai-agents/mcp-tools.md +197 -0
- package/framework/standards/ai-agents/middleware-patterns.md +207 -0
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -0
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -0
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -0
- package/framework/standards/ai-agents/modalities-vision.md +201 -0
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -0
- package/framework/standards/ai-agents/production.md +114 -258
- package/framework/standards/ai-agents/prompts-in-db.md +214 -0
- package/framework/standards/ai-agents/providers/model-registry.md +158 -0
- package/framework/standards/ai-agents/rag-custom-pgvector.md +243 -0
- package/framework/standards/ai-agents/service-tier-flex.md +135 -0
- package/framework/standards/ai-agents/setup.md +142 -316
- package/framework/standards/ai-agents/structured-output.md +249 -0
- package/framework/standards/ai-agents/sweet-spot.md +109 -0
- package/framework/standards/ai-agents/workflows.md +114 -284
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +433 -429
- package/framework/standards/backend/api/validation.md +92 -88
- package/framework/standards/backend/database/postgresql/database.md +336 -332
- package/framework/standards/backend/database/vector-search-rag.md +599 -541
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +4 -0
- package/framework/standards/backend/dotnet/result-pattern.md +4 -0
- package/framework/standards/backend/dotnet/scalar-ui.md +176 -172
- package/framework/standards/backend/dotnet/scrutor.md +4 -0
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +278 -274
- package/framework/standards/backend/integrations/asaas/asaas-api.md +260 -216
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +354 -350
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +30 -19
- package/framework/standards/backend/integrations/resend/resend-email.md +400 -385
- package/framework/standards/data/nosql/cache/redis.md +106 -97
- package/framework/standards/data/nosql/cosmos-db.md +127 -118
- package/framework/standards/data/vector-search/rag-chunking.md +113 -104
- package/framework/standards/frontend/design-system/ai-image-generation.md +4 -0
- package/framework/standards/frontend/design-system/animations.md +164 -160
- package/framework/standards/frontend/design-system/naming.md +68 -64
- package/framework/standards/frontend/gsap/gsap-core.md +14 -8
- package/framework/standards/frontend/gsap/gsap-plugins.md +26 -21
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +9 -7
- package/framework/standards/frontend/nextjs/app-router.md +127 -123
- package/framework/standards/frontend/nextjs/components.md +136 -132
- package/framework/standards/frontend/nextjs/data-fetching.md +130 -126
- package/framework/standards/frontend/nextjs/forms.md +134 -128
- package/framework/standards/frontend/nextjs/motion-patterns.md +328 -322
- package/framework/standards/frontend/nextjs/naming-conventions.md +71 -67
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +229 -225
- package/framework/standards/frontend/nextjs/project-structure.md +106 -102
- package/framework/standards/frontend/nextjs/state-management.md +76 -72
- package/framework/standards/frontend/nextjs/testing.md +115 -111
- package/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -192
- package/framework/standards/infrastructure/neon/neon-pgrag.md +6 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +4 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +5 -1
- package/framework/standards/integration/mcp/mcp-tools.md +471 -462
- package/framework/templates/REGISTRY.json +1745 -1239
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -0
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -0
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -0
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -0
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -0
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -0
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -0
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -0
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -0
- package/framework/templates/docs/onboarding.md +4 -4
- package/framework/templates/tasks.template.json +17 -0
- package/package.json +89 -83
- package/src/commands/project/init.js +16 -0
- package/src/commands/validation/validate-feature.js +1 -1
- package/src/core/paths/output-schema.js +6 -6
- package/src/core/state/state-manager.js +1 -1
- package/src/core/templates/template-registry.js +289 -256
- package/src/lib/detectors/design-system-detector.js +1 -1
- package/src/lib/generators/context-generator.js +516 -518
- package/src/lib/generators/scaffold-generator.js +23 -26
- package/src/lib/installers/memory-installer.js +138 -0
- package/src/lib/phase-chain/phase-validator.js +4 -4
- package/src/lib/risk-detector.js +80 -0
- package/src/lib/tasks/tasks-json-parser.js +185 -0
- package/src/scripts/setup-infra.js +25 -0
- package/src/utils/hooks-installer.js +59 -10
- package/framework/standards/infrastructure/neon/neon-semantic-kernel.md +0 -310
- package/framework/templates/integrations/semantic-kernel-neon.cs +0 -125
|
@@ -1,385 +1,400 @@
|
|
|
1
|
-
# Resend Email Service Integration Standard
|
|
2
|
-
|
|
3
|
-
> **Scope:** universal
|
|
4
|
-
> **Layer:** 2 (on keyword)
|
|
5
|
-
> **Keywords:** resend, email, transactional, smtp, sendgrid alternative
|
|
6
|
-
> **Load When:** resend or transactional email keywords detected
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1
|
+
# Resend Email Service Integration Standard
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 2 (on keyword)
|
|
5
|
+
> **Keywords:** resend, email, transactional, smtp, sendgrid alternative
|
|
6
|
+
> **Load When:** resend or transactional email keywords detected
|
|
7
|
+
|
|
8
|
+
**Verified against:** Resend Node SDK 4 + Resend .NET SDK + React Email. Last-verified: 2026-05-20.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Modern transactional email service for developers, built for Next.js and .NET.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
Resend provides:
|
|
19
|
+
- Simple API for sending emails
|
|
20
|
+
- React Email template support
|
|
21
|
+
- Email tracking and analytics
|
|
22
|
+
- Domain verification
|
|
23
|
+
- Webhooks for delivery status
|
|
24
|
+
- 100 free emails/day
|
|
25
|
+
|
|
26
|
+
**Stack:** Universal (.NET + Next.js)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Core Principles
|
|
31
|
+
|
|
32
|
+
1. **API-First**: Use REST API, not SMTP
|
|
33
|
+
2. **Type Safety**: Use official SDKs with TypeScript/C# types
|
|
34
|
+
3. **Template-Based**: Use React Email for Next.js, Razor for .NET
|
|
35
|
+
4. **Async**: Always send emails asynchronously
|
|
36
|
+
5. **Monitor**: Track delivery status via webhooks
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation & Setup
|
|
41
|
+
|
|
42
|
+
### Next.js Setup
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install resend react-email
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// lib/resend.ts
|
|
50
|
+
import { Resend } from 'resend';
|
|
51
|
+
|
|
52
|
+
export const resend = new Resend(process.env.RESEND_API_KEY);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### .NET Setup
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
dotnet add package Resend
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```csharp
|
|
62
|
+
// Program.cs
|
|
63
|
+
using Resend;
|
|
64
|
+
|
|
65
|
+
builder.Services.AddOptions();
|
|
66
|
+
builder.Services.AddHttpClient<ResendClient>();
|
|
67
|
+
builder.Services.Configure<ResendClientOptions>(o =>
|
|
68
|
+
{
|
|
69
|
+
o.ApiToken = builder.Configuration["Resend:ApiKey"]!;
|
|
70
|
+
});
|
|
71
|
+
builder.Services.AddTransient<IResend, ResendClient>();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Environment Variables
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# .env.local (Next.js) or appsettings.json (.NET)
|
|
78
|
+
RESEND_API_KEY=re_...
|
|
79
|
+
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Next.js Usage
|
|
85
|
+
|
|
86
|
+
### React Email Template
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// emails/WelcomeEmail.tsx
|
|
90
|
+
import { Html, Button, Container, Text } from '@react-email/components';
|
|
91
|
+
|
|
92
|
+
interface WelcomeEmailProps {
|
|
93
|
+
name: string;
|
|
94
|
+
verificationUrl: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default function WelcomeEmail({ name, verificationUrl }: WelcomeEmailProps) {
|
|
98
|
+
return (
|
|
99
|
+
<Html>
|
|
100
|
+
<Container>
|
|
101
|
+
<Text>Welcome, {name}!</Text>
|
|
102
|
+
<Button href={verificationUrl}>Verify Email</Button>
|
|
103
|
+
</Container>
|
|
104
|
+
</Html>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Send Email (API Route)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// app/api/send-welcome/route.ts
|
|
113
|
+
import { resend } from '@/lib/resend';
|
|
114
|
+
import WelcomeEmail from '@/emails/WelcomeEmail';
|
|
115
|
+
|
|
116
|
+
export async function POST(req: Request) {
|
|
117
|
+
const { email, name, verificationUrl } = await req.json();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const { data, error } = await resend.emails.send({
|
|
121
|
+
from: 'MyApp <noreply@yourdomain.com>',
|
|
122
|
+
to: email,
|
|
123
|
+
subject: 'Welcome to MyApp!',
|
|
124
|
+
react: WelcomeEmail({ name, verificationUrl }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (error) {
|
|
128
|
+
return Response.json({ error }, { status: 500 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Response.json({ id: data.id });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return Response.json({ error: 'Failed to send email' }, { status: 500 });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## .NET Usage
|
|
141
|
+
|
|
142
|
+
### Email Service Interface
|
|
143
|
+
|
|
144
|
+
```csharp
|
|
145
|
+
// Services/IEmailService.cs
|
|
146
|
+
public interface IEmailService
|
|
147
|
+
{
|
|
148
|
+
Task<string> SendWelcomeEmailAsync(string toEmail, string name);
|
|
149
|
+
Task<string> SendPasswordResetAsync(string toEmail, string resetUrl);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Email Service Implementation
|
|
154
|
+
|
|
155
|
+
```csharp
|
|
156
|
+
// Services/EmailService.cs
|
|
157
|
+
using Resend;
|
|
158
|
+
|
|
159
|
+
public class EmailService : IEmailService
|
|
160
|
+
{
|
|
161
|
+
private readonly IResend _resend;
|
|
162
|
+
private readonly IConfiguration _config;
|
|
163
|
+
|
|
164
|
+
public EmailService(IResend resend, IConfiguration config)
|
|
165
|
+
{
|
|
166
|
+
_resend = resend;
|
|
167
|
+
_config = config;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public async Task<string> SendWelcomeEmailAsync(string toEmail, string name)
|
|
171
|
+
{
|
|
172
|
+
var message = new EmailMessage
|
|
173
|
+
{
|
|
174
|
+
From = _config["Resend:FromEmail"]!,
|
|
175
|
+
To = toEmail,
|
|
176
|
+
Subject = "Welcome!",
|
|
177
|
+
HtmlBody = $@"
|
|
178
|
+
<h1>Welcome, {name}!</h1>
|
|
179
|
+
<p>Thank you for joining us.</p>
|
|
180
|
+
"
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
var response = await _resend.EmailSendAsync(message);
|
|
184
|
+
return response.Id;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public async Task<string> SendPasswordResetAsync(string toEmail, string resetUrl)
|
|
188
|
+
{
|
|
189
|
+
var message = new EmailMessage
|
|
190
|
+
{
|
|
191
|
+
From = _config["Resend:FromEmail"]!,
|
|
192
|
+
To = toEmail,
|
|
193
|
+
Subject = "Reset Your Password",
|
|
194
|
+
HtmlBody = $@"
|
|
195
|
+
<h1>Password Reset</h1>
|
|
196
|
+
<p>Click the link below to reset your password:</p>
|
|
197
|
+
<a href=""{resetUrl}"">Reset Password</a>
|
|
198
|
+
"
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
var response = await _resend.EmailSendAsync(message);
|
|
202
|
+
return response.Id;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Register in DI
|
|
207
|
+
builder.Services.AddScoped<IEmailService, EmailService>();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Batch Emails
|
|
213
|
+
|
|
214
|
+
### Next.js Batch Send
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Send to multiple recipients
|
|
218
|
+
const { data, error } = await resend.batch.send([
|
|
219
|
+
{
|
|
220
|
+
from: 'MyApp <noreply@yourdomain.com>',
|
|
221
|
+
to: 'user1@example.com',
|
|
222
|
+
subject: 'Notification',
|
|
223
|
+
react: NotificationEmail({ message: 'Update 1' }),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
from: 'MyApp <noreply@yourdomain.com>',
|
|
227
|
+
to: 'user2@example.com',
|
|
228
|
+
subject: 'Notification',
|
|
229
|
+
react: NotificationEmail({ message: 'Update 2' }),
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Webhooks for Delivery Status
|
|
237
|
+
|
|
238
|
+
### Setup Webhook Endpoint
|
|
239
|
+
|
|
240
|
+
Resend webhooks are signed with **Svix** — verify with `resend.webhooks.verify()`,
|
|
241
|
+
which checks the `svix-id` / `svix-timestamp` / `svix-signature` headers against
|
|
242
|
+
the raw request body. Do **not** roll a manual HMAC: Resend does not use a plain
|
|
243
|
+
`resend-signature` header, and a hand-written check will reject valid webhooks.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// app/api/webhooks/resend/route.ts
|
|
247
|
+
import { resend } from '@/lib/resend';
|
|
248
|
+
|
|
249
|
+
export async function POST(req: Request) {
|
|
250
|
+
// Raw body is required — verification fails on a re-serialized payload
|
|
251
|
+
const payload = await req.text();
|
|
252
|
+
|
|
253
|
+
let event;
|
|
254
|
+
try {
|
|
255
|
+
// Throws on an invalid signature; returns the parsed payload on success
|
|
256
|
+
event = resend.webhooks.verify({
|
|
257
|
+
payload,
|
|
258
|
+
headers: {
|
|
259
|
+
id: req.headers.get('svix-id')!,
|
|
260
|
+
timestamp: req.headers.get('svix-timestamp')!,
|
|
261
|
+
signature: req.headers.get('svix-signature')!,
|
|
262
|
+
},
|
|
263
|
+
webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
|
|
264
|
+
});
|
|
265
|
+
} catch {
|
|
266
|
+
return new Response('Invalid signature', { status: 401 });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
switch (event.type) {
|
|
270
|
+
case 'email.delivered':
|
|
271
|
+
await handleEmailDelivered(event.data);
|
|
272
|
+
break;
|
|
273
|
+
case 'email.bounced':
|
|
274
|
+
await handleEmailBounced(event.data);
|
|
275
|
+
break;
|
|
276
|
+
case 'email.complained':
|
|
277
|
+
await handleEmailComplained(event.data);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return new Response('OK');
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Best Practices
|
|
288
|
+
|
|
289
|
+
### Background Jobs (with Hangfire)
|
|
290
|
+
|
|
291
|
+
```csharp
|
|
292
|
+
// Don't block HTTP requests with email sending
|
|
293
|
+
BackgroundJob.Enqueue<IEmailService>(
|
|
294
|
+
x => x.SendWelcomeEmailAsync(email, name));
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Rate Limiting
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Implement rate limiting for email sends
|
|
301
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
302
|
+
import { Redis } from '@upstash/redis';
|
|
303
|
+
|
|
304
|
+
const ratelimit = new Ratelimit({
|
|
305
|
+
redis: Redis.fromEnv(),
|
|
306
|
+
limiter: Ratelimit.slidingWindow(10, '1 h'), // 10 emails per hour
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const { success } = await ratelimit.limit(userId);
|
|
310
|
+
if (!success) {
|
|
311
|
+
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await resend.emails.send({ ... });
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Retry Logic (.NET)
|
|
318
|
+
|
|
319
|
+
```csharp
|
|
320
|
+
using Polly;
|
|
321
|
+
|
|
322
|
+
var retryPolicy = Policy
|
|
323
|
+
.Handle<HttpRequestException>()
|
|
324
|
+
.WaitAndRetryAsync(3, retryAttempt =>
|
|
325
|
+
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
|
|
326
|
+
|
|
327
|
+
await retryPolicy.ExecuteAsync(async () =>
|
|
328
|
+
{
|
|
329
|
+
await _resend.EmailSendAsync(message);
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Domain Verification
|
|
336
|
+
|
|
337
|
+
1. Go to Resend Dashboard → Domains
|
|
338
|
+
2. Add your domain (e.g., `yourdomain.com`)
|
|
339
|
+
3. Add DNS records (SPF, DKIM, DMARC)
|
|
340
|
+
4. Wait for verification (up to 72 hours)
|
|
341
|
+
|
|
342
|
+
### DNS Records Example
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
TXT @ v=spf1 include:resend.com ~all
|
|
346
|
+
TXT resend._domainkey <DKIM_VALUE>
|
|
347
|
+
TXT _dmarc v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Common Email Templates
|
|
353
|
+
|
|
354
|
+
### Order Confirmation
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
export default function OrderConfirmation({ orderNumber, total, items }) {
|
|
358
|
+
return (
|
|
359
|
+
<Html>
|
|
360
|
+
<Container>
|
|
361
|
+
<Text>Order #{orderNumber} Confirmed</Text>
|
|
362
|
+
<Text>Total: ${total}</Text>
|
|
363
|
+
<ul>
|
|
364
|
+
{items.map(item => (
|
|
365
|
+
<li key={item.id}>{item.name} - ${item.price}</li>
|
|
366
|
+
))}
|
|
367
|
+
</ul>
|
|
368
|
+
</Container>
|
|
369
|
+
</Html>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Magic Link Auth
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
export default function MagicLink({ url, expiresIn }) {
|
|
378
|
+
return (
|
|
379
|
+
<Html>
|
|
380
|
+
<Container>
|
|
381
|
+
<Text>Sign in to your account</Text>
|
|
382
|
+
<Button href={url}>Sign In</Button>
|
|
383
|
+
<Text>This link expires in {expiresIn} minutes.</Text>
|
|
384
|
+
</Container>
|
|
385
|
+
</Html>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## References
|
|
393
|
+
|
|
394
|
+
- [Resend Documentation](https://resend.com/docs)
|
|
395
|
+
- [React Email](https://react.email/)
|
|
396
|
+
- [Resend .NET SDK](https://github.com/resend/resend-dotnet)
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
*MORPH-SPEC by Polymorphism Tech*
|