@polymorphism-tech/morph-spec 4.7.0 → 4.7.2
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/.morph/.morphversion +5 -0
- package/.morph/analytics/threads-log.jsonl +5 -0
- package/.morph/config/config.json +8 -0
- package/.morph/framework/agents.json +1815 -0
- package/.morph/framework/hooks/README.md +205 -0
- package/.morph/framework/hooks/claude-code/notification/approval-reminder.js +54 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/dispatch.js +83 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +42 -0
- package/.morph/framework/hooks/claude-code/pre-compact/save-morph-context.js +61 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +71 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +58 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +64 -0
- package/.morph/framework/hooks/claude-code/session-start/inject-morph-context.js +94 -0
- package/.morph/framework/hooks/claude-code/statusline.py +538 -0
- package/.morph/framework/hooks/claude-code/statusline.sh +7 -0
- package/.morph/framework/hooks/claude-code/stop/validate-completion.js +88 -0
- package/.morph/framework/hooks/claude-code/user-prompt/enrich-prompt.js +91 -0
- package/.morph/framework/hooks/git/commit-msg/conventional-commits.sh +33 -0
- package/.morph/framework/hooks/git/pre-commit/agents.sh +25 -0
- package/.morph/framework/hooks/git/pre-commit/orchestrator.sh +64 -0
- package/.morph/framework/hooks/git/pre-commit/specs.sh +50 -0
- package/.morph/framework/hooks/git/pre-push/run-tests.sh +44 -0
- package/.morph/framework/hooks/shared/hook-response.js +45 -0
- package/.morph/framework/hooks/shared/phase-utils.js +129 -0
- package/.morph/framework/hooks/shared/state-reader.js +138 -0
- package/.morph/framework/hooks/shared/stdin-reader.js +26 -0
- package/.morph/framework/standards/STANDARDS.json +933 -0
- package/.morph/framework/standards/ai-agents/blazor-ui.md +364 -0
- package/.morph/framework/standards/ai-agents/production.md +415 -0
- package/.morph/framework/standards/ai-agents/setup.md +418 -0
- package/.morph/framework/standards/ai-agents/team-orchestration.md +479 -0
- package/.morph/framework/standards/ai-agents/workflows.md +354 -0
- package/.morph/framework/standards/architecture/ddd/aggregates.md +120 -0
- package/.morph/framework/standards/architecture/ddd/bounded-contexts.md +105 -0
- package/.morph/framework/standards/architecture/ddd/complexity-levels.md +108 -0
- package/.morph/framework/standards/architecture/ddd/entities.md +99 -0
- package/.morph/framework/standards/architecture/ddd/ubiquitous-language.md +58 -0
- package/.morph/framework/standards/architecture/ddd/value-objects.md +124 -0
- package/.morph/framework/standards/backend/api/minimal-api.md +494 -0
- package/.morph/framework/standards/backend/api/rest.md +492 -0
- package/.morph/framework/standards/backend/api/validation.md +88 -0
- package/.morph/framework/standards/backend/authentication/passkeys.md +428 -0
- package/.morph/framework/standards/backend/database/ef-core.md +199 -0
- package/.morph/framework/standards/backend/database/migrations.md +393 -0
- package/.morph/framework/standards/backend/database/postgresql/database.md +352 -0
- package/.morph/framework/standards/backend/database/repository-patterns.md +528 -0
- package/.morph/framework/standards/backend/database/vector-search-rag.md +541 -0
- package/.morph/framework/standards/backend/dotnet/async.md +366 -0
- package/.morph/framework/standards/backend/dotnet/core.md +117 -0
- package/.morph/framework/standards/backend/dotnet/di.md +439 -0
- package/.morph/framework/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/.morph/framework/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/.morph/framework/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/.morph/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/.morph/framework/standards/backend/integrations/resend/resend-email.md +385 -0
- package/.morph/framework/standards/context/analytics.md +96 -0
- package/.morph/framework/standards/context/bundles.md +110 -0
- package/.morph/framework/standards/context/priming.md +78 -0
- package/.morph/framework/standards/core/architecture.md +185 -0
- package/.morph/framework/standards/core/coding.md +214 -0
- package/.morph/framework/standards/core/git-branching-strategy.md +403 -0
- package/.morph/framework/standards/core/git.md +185 -0
- package/.morph/framework/standards/core/testing.md +295 -0
- package/.morph/framework/standards/data/nosql/blob-storage.md +102 -0
- package/.morph/framework/standards/data/nosql/cache/redis.md +97 -0
- package/.morph/framework/standards/data/nosql/cosmos-db.md +118 -0
- package/.morph/framework/standards/data/vector-search/azure-ai-search.md +121 -0
- package/.morph/framework/standards/data/vector-search/rag-chunking.md +104 -0
- package/.morph/framework/standards/frontend/blazor/design-checklist.md +222 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui.md +137 -0
- package/.morph/framework/standards/frontend/blazor/html-conversion.md +184 -0
- package/.morph/framework/standards/frontend/blazor/lifecycle.md +195 -0
- package/.morph/framework/standards/frontend/blazor/pitfalls.md +198 -0
- package/.morph/framework/standards/frontend/blazor/state.md +191 -0
- package/.morph/framework/standards/frontend/design-system/animations.md +151 -0
- package/.morph/framework/standards/frontend/design-system/naming.md +64 -0
- package/.morph/framework/standards/frontend/nextjs/app-router.md +123 -0
- package/.morph/framework/standards/frontend/nextjs/components.md +132 -0
- package/.morph/framework/standards/frontend/nextjs/data-fetching.md +126 -0
- package/.morph/framework/standards/frontend/nextjs/forms.md +128 -0
- package/.morph/framework/standards/frontend/nextjs/naming-conventions.md +67 -0
- package/.morph/framework/standards/frontend/nextjs/nextjs-patterns.md +215 -0
- package/.morph/framework/standards/frontend/nextjs/project-structure.md +102 -0
- package/.morph/framework/standards/frontend/nextjs/state-management.md +72 -0
- package/.morph/framework/standards/frontend/nextjs/testing.md +111 -0
- package/.morph/framework/standards/infrastructure/azure/azure.md +624 -0
- package/.morph/framework/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/.morph/framework/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/.morph/framework/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/.morph/framework/standards/infrastructure/azure/services/functions.md +486 -0
- package/.morph/framework/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/.morph/framework/standards/infrastructure/azure/services/storage.md +407 -0
- package/.morph/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/.morph/framework/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/.morph/framework/standards/integration/api/graphql.md +91 -0
- package/.morph/framework/standards/integration/api/grpc.md +114 -0
- package/.morph/framework/standards/integration/api/rest-design.md +95 -0
- package/.morph/framework/standards/integration/event-driven/cqrs.md +101 -0
- package/.morph/framework/standards/integration/event-driven/event-sourcing.md +124 -0
- package/.morph/framework/standards/integration/event-driven/service-bus.md +95 -0
- package/.morph/framework/standards/integration/mcp/mcp-tools.md +384 -0
- package/.morph/framework/standards/observability/logging.md +131 -0
- package/.morph/framework/standards/observability/metrics.md +121 -0
- package/.morph/framework/standards/observability/monitoring.md +114 -0
- package/.morph/framework/standards/observability/tracing.md +132 -0
- package/.morph/framework/standards/workflows/parallel-execution.md +112 -0
- package/.morph/framework/standards/workflows/thread-management.md +113 -0
- package/.morph/framework/templates/.idea/morph-templates.xml +92 -0
- package/.morph/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/.morph/framework/templates/IDE-SNIPPETS.md +266 -0
- package/.morph/framework/templates/README.md +814 -0
- package/.morph/framework/templates/REGISTRY.json +1888 -0
- package/.morph/framework/templates/code/dotnet/backend/repository.cs +141 -0
- package/.morph/framework/templates/code/dotnet/backend/service.cs +139 -0
- package/.morph/framework/templates/code/dotnet/contracts/Commands.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/Entities.cs +25 -0
- package/.morph/framework/templates/code/dotnet/contracts/Queries.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/README.md +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level1.cs +69 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level2.cs +86 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level3.cs +41 -0
- package/.morph/framework/templates/code/dotnet/database/migration.cs +83 -0
- package/.morph/framework/templates/code/dotnet/frontend/component.razor +239 -0
- package/.morph/framework/templates/code/dotnet/jobs/agent.cs +163 -0
- package/.morph/framework/templates/code/dotnet/jobs/job.cs +171 -0
- package/.morph/framework/templates/code/dotnet/test.cs +239 -0
- package/.morph/framework/templates/code/sql/rls-policy.sql +57 -0
- package/.morph/framework/templates/code/sql/supabase-migration.sql +100 -0
- package/.morph/framework/templates/code/sql/supabase-migration.template.sql +113 -0
- package/.morph/framework/templates/code/typescript/contracts.ts +168 -0
- package/.morph/framework/templates/context/CONTEXT-FEATURE.md +276 -0
- package/.morph/framework/templates/context/CONTEXT.md +181 -0
- package/.morph/framework/templates/docs/clarifications.md +253 -0
- package/.morph/framework/templates/docs/onboarding.md +123 -0
- package/.morph/framework/templates/docs/proposal.md +182 -0
- package/.morph/framework/templates/docs/schema-analysis.md +119 -0
- package/.morph/framework/templates/docs/spec.md +198 -0
- package/.morph/framework/templates/docs/ui-components.md +124 -0
- package/.morph/framework/templates/docs/ui-design-system.md +76 -0
- package/.morph/framework/templates/docs/ui-flows.md +167 -0
- package/.morph/framework/templates/docs/ui-mockups.md +98 -0
- package/.morph/framework/templates/docs/user-stories.md +34 -0
- package/.morph/framework/templates/examples/design-system-examples.md +357 -0
- package/.morph/framework/templates/examples/spec-examples.md +90 -0
- package/.morph/framework/templates/feature/decisions.md +187 -0
- package/.morph/framework/templates/feature/recap.md +146 -0
- package/.morph/framework/templates/feature/tasks.md +199 -0
- package/.morph/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -0
- package/.morph/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/env.mjs.hbs +32 -0
- package/.morph/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -0
- package/.morph/framework/templates/frontend/nextjs/page.tsx.hbs +22 -0
- package/.morph/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -0
- package/.morph/framework/templates/infrastructure/azure/Dockerfile.example +82 -0
- package/.morph/framework/templates/infrastructure/azure/README.md +286 -0
- package/.morph/framework/templates/infrastructure/azure/app-insights.bicep +63 -0
- package/.morph/framework/templates/infrastructure/azure/app-service.bicep +164 -0
- package/.morph/framework/templates/infrastructure/azure/container-app-env.bicep +49 -0
- package/.morph/framework/templates/infrastructure/azure/container-app.bicep +156 -0
- package/.morph/framework/templates/infrastructure/azure/deploy-checklist.md +426 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.ps1 +229 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.sh +208 -0
- package/.morph/framework/templates/infrastructure/azure/key-vault.bicep +91 -0
- package/.morph/framework/templates/infrastructure/azure/main.bicep +189 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.dev.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.prod.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.staging.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/sql-database.bicep +103 -0
- package/.morph/framework/templates/infrastructure/azure/storage.bicep +106 -0
- package/.morph/framework/templates/infrastructure/docker/Dockerfile.template +58 -0
- package/.morph/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -0
- package/.morph/framework/templates/infrastructure/docker/easypanel.template.json +54 -0
- package/.morph/framework/templates/infrastructure/github/README.md +593 -0
- package/.morph/framework/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +22 -0
- package/.morph/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -0
- package/.morph/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +61 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +31 -0
- package/.morph/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -0
- package/.morph/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -0
- package/.morph/framework/templates/integrations/asaas-client.cs +387 -0
- package/.morph/framework/templates/integrations/asaas-webhook.cs +351 -0
- package/.morph/framework/templates/integrations/azure-identity-config.cs +288 -0
- package/.morph/framework/templates/integrations/clerk-config.cs +258 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-retry.md +78 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-validation.md +97 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-wrapper.md +36 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +126 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/squad-leader.md +43 -0
- package/.morph/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -0
- package/.morph/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -0
- package/.morph/framework/templates/project-structure/dotnet-ddd.md +70 -0
- package/.morph/framework/templates/saas/subscription.cs +347 -0
- package/.morph/framework/templates/saas/tenant.cs +338 -0
- package/.morph/framework/templates/state.template.json +17 -0
- package/.morph/framework/templates/ui/FluentDesignTheme.cs +149 -0
- package/.morph/framework/templates/ui/MudTheme.cs +281 -0
- package/.morph/framework/templates/ui/design-system.css +226 -0
- package/.morph/logs/tool-failures.log +17 -0
- package/.morph/memory/pre-compact-2026-02-24T17-43-30-049Z.json +16 -0
- package/.morph/plans/eager-watching-bunny.md +105 -0
- package/.morph/plans/temporal-seeking-nebula.md +45 -0
- package/.morph/state.json +48 -0
- package/CLAUDE.md +1 -1
- package/README.md +119 -99
- package/bin/morph-spec.js +0 -9
- package/framework/CLAUDE.md +1 -1
- package/framework/hooks/README.md +10 -6
- package/framework/hooks/claude-code/notification/approval-reminder.js +2 -0
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +1 -1
- package/framework/hooks/claude-code/stop/validate-completion.js +1 -1
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +1 -1
- package/package.json +1 -1
- package/src/commands/project/init.js +15 -42
- package/src/commands/project/update.js +22 -37
- package/src/lib/installers/mcp-installer.js +18 -3
- package/src/utils/hooks-installer.js +5 -15
- package/src/commands/project/detect.js +0 -114
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# Hangfire Background Jobs Standard
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 2 (on keyword)
|
|
5
|
+
> **Keywords:** hangfire, background job, recurring, fire-and-forget, scheduled
|
|
6
|
+
> **Load When:** hangfire or background job keywords detected
|
|
7
|
+
|
|
8
|
+
Background job processing for .NET applications with Hangfire.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Hangfire provides:
|
|
15
|
+
- Fire-and-forget jobs
|
|
16
|
+
- Delayed jobs
|
|
17
|
+
- Recurring jobs (cron schedules)
|
|
18
|
+
- Job continuations
|
|
19
|
+
- Built-in dashboard
|
|
20
|
+
- Retry logic with exponential backoff
|
|
21
|
+
|
|
22
|
+
**Stack:** .NET 10
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Core Principles
|
|
27
|
+
|
|
28
|
+
1. **Stateless Jobs**: Job methods should be stateless and idempotent
|
|
29
|
+
2. **Minimal Dependencies**: Jobs should not depend on HTTP context or scoped services
|
|
30
|
+
3. **Explicit Retries**: Configure retry behavior per job type
|
|
31
|
+
4. **Monitor Dashboard**: Use Hangfire Dashboard for job monitoring
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation & Setup
|
|
36
|
+
|
|
37
|
+
### Install Packages
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
dotnet add package Hangfire.Core
|
|
41
|
+
dotnet add package Hangfire.SqlServer
|
|
42
|
+
dotnet add package Hangfire.AspNetCore
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Program.cs Configuration
|
|
46
|
+
|
|
47
|
+
```csharp
|
|
48
|
+
// Program.cs
|
|
49
|
+
using Hangfire;
|
|
50
|
+
using Hangfire.SqlServer;
|
|
51
|
+
|
|
52
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
53
|
+
|
|
54
|
+
// Add Hangfire services
|
|
55
|
+
builder.Services.AddHangfire(configuration => configuration
|
|
56
|
+
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
|
57
|
+
.UseSimpleAssemblyNameTypeSerializer()
|
|
58
|
+
.UseRecommendedSerializerSettings()
|
|
59
|
+
.UseSqlServerStorage(
|
|
60
|
+
builder.Configuration.GetConnectionString("HangfireConnection"),
|
|
61
|
+
new SqlServerStorageOptions
|
|
62
|
+
{
|
|
63
|
+
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
|
64
|
+
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
|
|
65
|
+
QueuePollInterval = TimeSpan.Zero,
|
|
66
|
+
UseRecommendedIsolationLevel = true,
|
|
67
|
+
DisableGlobalLocks = true
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
builder.Services.AddHangfireServer();
|
|
71
|
+
|
|
72
|
+
var app = builder.Build();
|
|
73
|
+
|
|
74
|
+
// Hangfire Dashboard (protect in production!)
|
|
75
|
+
app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|
76
|
+
{
|
|
77
|
+
Authorization = new[] { new HangfireAuthorizationFilter() }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
app.Run();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Job Types
|
|
86
|
+
|
|
87
|
+
### Fire-and-Forget Jobs
|
|
88
|
+
|
|
89
|
+
```csharp
|
|
90
|
+
// Enqueue immediately
|
|
91
|
+
BackgroundJob.Enqueue<IEmailService>(x => x.SendWelcomeEmail(userId));
|
|
92
|
+
|
|
93
|
+
// Alternative: Static method
|
|
94
|
+
BackgroundJob.Enqueue(() => Console.WriteLine("Hello, world!"));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Delayed Jobs
|
|
98
|
+
|
|
99
|
+
```csharp
|
|
100
|
+
// Run after 1 hour
|
|
101
|
+
BackgroundJob.Schedule<IReportService>(
|
|
102
|
+
x => x.GenerateMonthlyReport(),
|
|
103
|
+
TimeSpan.FromHours(1));
|
|
104
|
+
|
|
105
|
+
// Run at specific time
|
|
106
|
+
BackgroundJob.Schedule<INotificationService>(
|
|
107
|
+
x => x.SendReminder(userId),
|
|
108
|
+
DateTimeOffset.UtcNow.AddDays(7));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Recurring Jobs
|
|
112
|
+
|
|
113
|
+
```csharp
|
|
114
|
+
// Run daily at 2 AM UTC
|
|
115
|
+
RecurringJob.AddOrUpdate<ICleanupService>(
|
|
116
|
+
"cleanup-old-data",
|
|
117
|
+
x => x.CleanupOldRecords(),
|
|
118
|
+
Cron.Daily(2));
|
|
119
|
+
|
|
120
|
+
// Run every 15 minutes
|
|
121
|
+
RecurringJob.AddOrUpdate<ISyncService>(
|
|
122
|
+
"sync-external-data",
|
|
123
|
+
x => x.SyncData(),
|
|
124
|
+
"*/15 * * * *");
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Job Continuations
|
|
128
|
+
|
|
129
|
+
```csharp
|
|
130
|
+
var parentJobId = BackgroundJob.Enqueue<IOrderService>(
|
|
131
|
+
x => x.ProcessOrder(orderId));
|
|
132
|
+
|
|
133
|
+
BackgroundJob.ContinueJobWith<IEmailService>(
|
|
134
|
+
parentJobId,
|
|
135
|
+
x => x.SendOrderConfirmation(orderId));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Job Service Pattern
|
|
141
|
+
|
|
142
|
+
```csharp
|
|
143
|
+
// Services/Jobs/IEmailJobService.cs
|
|
144
|
+
public interface IEmailJobService
|
|
145
|
+
{
|
|
146
|
+
Task SendWelcomeEmailAsync(string userId);
|
|
147
|
+
Task SendPasswordResetAsync(string email);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Services/Jobs/EmailJobService.cs
|
|
151
|
+
public class EmailJobService : IEmailJobService
|
|
152
|
+
{
|
|
153
|
+
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
|
154
|
+
private readonly IEmailSender _emailSender;
|
|
155
|
+
|
|
156
|
+
public EmailJobService(
|
|
157
|
+
IDbContextFactory<AppDbContext> dbFactory,
|
|
158
|
+
IEmailSender emailSender)
|
|
159
|
+
{
|
|
160
|
+
_dbFactory = dbFactory;
|
|
161
|
+
_emailSender = emailSender;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async Task SendWelcomeEmailAsync(string userId)
|
|
165
|
+
{
|
|
166
|
+
// Use IDbContextFactory for background jobs
|
|
167
|
+
await using var db = await _dbFactory.CreateDbContextAsync();
|
|
168
|
+
|
|
169
|
+
var user = await db.Users.FindAsync(userId);
|
|
170
|
+
if (user == null) return;
|
|
171
|
+
|
|
172
|
+
await _emailSender.SendAsync(
|
|
173
|
+
user.Email,
|
|
174
|
+
"Welcome!",
|
|
175
|
+
$"Hello {user.Name}!");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Register in DI
|
|
180
|
+
builder.Services.AddScoped<IEmailJobService, EmailJobService>();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Retry & Error Handling
|
|
186
|
+
|
|
187
|
+
### Custom Retry Attribute
|
|
188
|
+
|
|
189
|
+
```csharp
|
|
190
|
+
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
|
|
191
|
+
public async Task ProcessPaymentAsync(string paymentId)
|
|
192
|
+
{
|
|
193
|
+
// Job logic with automatic retry on failure
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Manual Retry Logic
|
|
198
|
+
|
|
199
|
+
```csharp
|
|
200
|
+
public async Task SendEmailWithRetryAsync(string email)
|
|
201
|
+
{
|
|
202
|
+
int maxRetries = 3;
|
|
203
|
+
int attempt = 0;
|
|
204
|
+
|
|
205
|
+
while (attempt < maxRetries)
|
|
206
|
+
{
|
|
207
|
+
try
|
|
208
|
+
{
|
|
209
|
+
await _emailSender.SendAsync(email, "Subject", "Body");
|
|
210
|
+
return; // Success
|
|
211
|
+
}
|
|
212
|
+
catch (Exception ex)
|
|
213
|
+
{
|
|
214
|
+
attempt++;
|
|
215
|
+
if (attempt >= maxRetries)
|
|
216
|
+
{
|
|
217
|
+
// Log and give up
|
|
218
|
+
_logger.LogError(ex, "Failed to send email after {Attempts} attempts", maxRetries);
|
|
219
|
+
throw;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Dashboard Authorization
|
|
231
|
+
|
|
232
|
+
```csharp
|
|
233
|
+
// Filters/HangfireAuthorizationFilter.cs
|
|
234
|
+
using Hangfire.Dashboard;
|
|
235
|
+
|
|
236
|
+
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
|
|
237
|
+
{
|
|
238
|
+
public bool Authorize(DashboardContext context)
|
|
239
|
+
{
|
|
240
|
+
var httpContext = context.GetHttpContext();
|
|
241
|
+
|
|
242
|
+
// Allow in development
|
|
243
|
+
if (httpContext.Request.Host.Host.Contains("localhost"))
|
|
244
|
+
return true;
|
|
245
|
+
|
|
246
|
+
// Require authenticated user with Admin role
|
|
247
|
+
return httpContext.User.Identity?.IsAuthenticated == true
|
|
248
|
+
&& httpContext.User.IsInRole("Admin");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Best Practices
|
|
256
|
+
|
|
257
|
+
### Avoid Scoped Services
|
|
258
|
+
|
|
259
|
+
```csharp
|
|
260
|
+
// ❌ BAD: Don't inject DbContext directly
|
|
261
|
+
public class BadJobService
|
|
262
|
+
{
|
|
263
|
+
private readonly AppDbContext _db; // Scoped service!
|
|
264
|
+
|
|
265
|
+
public BadJobService(AppDbContext db) => _db = db;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ✅ GOOD: Use IDbContextFactory
|
|
269
|
+
public class GoodJobService
|
|
270
|
+
{
|
|
271
|
+
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
|
272
|
+
|
|
273
|
+
public GoodJobService(IDbContextFactory<AppDbContext> dbFactory)
|
|
274
|
+
=> _dbFactory = dbFactory;
|
|
275
|
+
|
|
276
|
+
public async Task ProcessAsync()
|
|
277
|
+
{
|
|
278
|
+
await using var db = await _dbFactory.CreateDbContextAsync();
|
|
279
|
+
// Use db...
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Idempotent Jobs
|
|
285
|
+
|
|
286
|
+
```csharp
|
|
287
|
+
public async Task ProcessOrderAsync(string orderId)
|
|
288
|
+
{
|
|
289
|
+
await using var db = await _dbFactory.CreateDbContextAsync();
|
|
290
|
+
|
|
291
|
+
var order = await db.Orders.FindAsync(orderId);
|
|
292
|
+
if (order == null || order.Status == OrderStatus.Processed)
|
|
293
|
+
{
|
|
294
|
+
// Already processed or doesn't exist - skip
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Process order...
|
|
299
|
+
order.Status = OrderStatus.Processed;
|
|
300
|
+
await db.SaveChangesAsync();
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Cron Schedule Examples
|
|
307
|
+
|
|
308
|
+
```csharp
|
|
309
|
+
Cron.Minutely() // Every minute
|
|
310
|
+
Cron.Hourly() // Every hour at minute 0
|
|
311
|
+
Cron.Daily() // Every day at 00:00 UTC
|
|
312
|
+
Cron.Daily(14) // Every day at 14:00 UTC
|
|
313
|
+
Cron.Weekly() // Every Sunday at 00:00 UTC
|
|
314
|
+
Cron.Monthly() // First day of month at 00:00 UTC
|
|
315
|
+
Cron.Yearly() // January 1st at 00:00 UTC
|
|
316
|
+
|
|
317
|
+
// Custom cron
|
|
318
|
+
"*/5 * * * *" // Every 5 minutes
|
|
319
|
+
"0 */2 * * *" // Every 2 hours
|
|
320
|
+
"0 9-17 * * 1-5" // 9 AM to 5 PM, Mon-Fri
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Monitoring & Troubleshooting
|
|
326
|
+
|
|
327
|
+
### Dashboard Access
|
|
328
|
+
|
|
329
|
+
Access the dashboard at `/hangfire` (configured in Program.cs).
|
|
330
|
+
|
|
331
|
+
### Common Issues
|
|
332
|
+
|
|
333
|
+
| Issue | Cause | Solution |
|
|
334
|
+
|-------|-------|----------|
|
|
335
|
+
| Jobs not processing | Hangfire server not started | Ensure `AddHangfireServer()` is called |
|
|
336
|
+
| DbContext errors | Using scoped DbContext | Use `IDbContextFactory` |
|
|
337
|
+
| Jobs timing out | Long-running job | Split into smaller jobs with continuations |
|
|
338
|
+
| Memory leaks | Not disposing DbContext | Use `await using` for DbContext |
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## References
|
|
343
|
+
|
|
344
|
+
- [Hangfire Documentation](https://docs.hangfire.io/)
|
|
345
|
+
- [Hangfire Best Practices](https://docs.hangfire.io/en/latest/best-practices.html)
|
|
346
|
+
- [Cron Expression Generator](https://crontab.guru/)
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,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
|
+
Modern transactional email service for developers, built for Next.js and .NET.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Resend provides:
|
|
15
|
+
- Simple API for sending emails
|
|
16
|
+
- React Email template support
|
|
17
|
+
- Email tracking and analytics
|
|
18
|
+
- Domain verification
|
|
19
|
+
- Webhooks for delivery status
|
|
20
|
+
- 100 free emails/day
|
|
21
|
+
|
|
22
|
+
**Stack:** Universal (.NET + Next.js)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Core Principles
|
|
27
|
+
|
|
28
|
+
1. **API-First**: Use REST API, not SMTP
|
|
29
|
+
2. **Type Safety**: Use official SDKs with TypeScript/C# types
|
|
30
|
+
3. **Template-Based**: Use React Email for Next.js, Razor for .NET
|
|
31
|
+
4. **Async**: Always send emails asynchronously
|
|
32
|
+
5. **Monitor**: Track delivery status via webhooks
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation & Setup
|
|
37
|
+
|
|
38
|
+
### Next.js Setup
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install resend react-email
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// lib/resend.ts
|
|
46
|
+
import { Resend } from 'resend';
|
|
47
|
+
|
|
48
|
+
export const resend = new Resend(process.env.RESEND_API_KEY);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### .NET Setup
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
dotnet add package Resend
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
// Program.cs
|
|
59
|
+
using Resend;
|
|
60
|
+
|
|
61
|
+
builder.Services.AddResend(options =>
|
|
62
|
+
{
|
|
63
|
+
options.ApiKey = builder.Configuration["Resend:ApiKey"]!;
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Environment Variables
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# .env.local (Next.js) or appsettings.json (.NET)
|
|
71
|
+
RESEND_API_KEY=re_...
|
|
72
|
+
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Next.js Usage
|
|
78
|
+
|
|
79
|
+
### React Email Template
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// emails/WelcomeEmail.tsx
|
|
83
|
+
import { Html, Button, Container, Text } from '@react-email/components';
|
|
84
|
+
|
|
85
|
+
interface WelcomeEmailProps {
|
|
86
|
+
name: string;
|
|
87
|
+
verificationUrl: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function WelcomeEmail({ name, verificationUrl }: WelcomeEmailProps) {
|
|
91
|
+
return (
|
|
92
|
+
<Html>
|
|
93
|
+
<Container>
|
|
94
|
+
<Text>Welcome, {name}!</Text>
|
|
95
|
+
<Button href={verificationUrl}>Verify Email</Button>
|
|
96
|
+
</Container>
|
|
97
|
+
</Html>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Send Email (API Route)
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// app/api/send-welcome/route.ts
|
|
106
|
+
import { resend } from '@/lib/resend';
|
|
107
|
+
import WelcomeEmail from '@/emails/WelcomeEmail';
|
|
108
|
+
|
|
109
|
+
export async function POST(req: Request) {
|
|
110
|
+
const { email, name, verificationUrl } = await req.json();
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const { data, error } = await resend.emails.send({
|
|
114
|
+
from: 'MyApp <noreply@yourdomain.com>',
|
|
115
|
+
to: email,
|
|
116
|
+
subject: 'Welcome to MyApp!',
|
|
117
|
+
react: WelcomeEmail({ name, verificationUrl }),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (error) {
|
|
121
|
+
return Response.json({ error }, { status: 500 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Response.json({ id: data.id });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return Response.json({ error: 'Failed to send email' }, { status: 500 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## .NET Usage
|
|
134
|
+
|
|
135
|
+
### Email Service Interface
|
|
136
|
+
|
|
137
|
+
```csharp
|
|
138
|
+
// Services/IEmailService.cs
|
|
139
|
+
public interface IEmailService
|
|
140
|
+
{
|
|
141
|
+
Task<string> SendWelcomeEmailAsync(string toEmail, string name);
|
|
142
|
+
Task<string> SendPasswordResetAsync(string toEmail, string resetUrl);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Email Service Implementation
|
|
147
|
+
|
|
148
|
+
```csharp
|
|
149
|
+
// Services/EmailService.cs
|
|
150
|
+
using Resend;
|
|
151
|
+
|
|
152
|
+
public class EmailService : IEmailService
|
|
153
|
+
{
|
|
154
|
+
private readonly IResend _resend;
|
|
155
|
+
private readonly IConfiguration _config;
|
|
156
|
+
|
|
157
|
+
public EmailService(IResend resend, IConfiguration config)
|
|
158
|
+
{
|
|
159
|
+
_resend = resend;
|
|
160
|
+
_config = config;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public async Task<string> SendWelcomeEmailAsync(string toEmail, string name)
|
|
164
|
+
{
|
|
165
|
+
var message = new EmailMessage
|
|
166
|
+
{
|
|
167
|
+
From = _config["Resend:FromEmail"]!,
|
|
168
|
+
To = toEmail,
|
|
169
|
+
Subject = "Welcome!",
|
|
170
|
+
HtmlBody = $@"
|
|
171
|
+
<h1>Welcome, {name}!</h1>
|
|
172
|
+
<p>Thank you for joining us.</p>
|
|
173
|
+
"
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
var response = await _resend.EmailSendAsync(message);
|
|
177
|
+
return response.Id;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public async Task<string> SendPasswordResetAsync(string toEmail, string resetUrl)
|
|
181
|
+
{
|
|
182
|
+
var message = new EmailMessage
|
|
183
|
+
{
|
|
184
|
+
From = _config["Resend:FromEmail"]!,
|
|
185
|
+
To = toEmail,
|
|
186
|
+
Subject = "Reset Your Password",
|
|
187
|
+
HtmlBody = $@"
|
|
188
|
+
<h1>Password Reset</h1>
|
|
189
|
+
<p>Click the link below to reset your password:</p>
|
|
190
|
+
<a href=""{resetUrl}"">Reset Password</a>
|
|
191
|
+
"
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
var response = await _resend.EmailSendAsync(message);
|
|
195
|
+
return response.Id;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Register in DI
|
|
200
|
+
builder.Services.AddScoped<IEmailService, EmailService>();
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Batch Emails
|
|
206
|
+
|
|
207
|
+
### Next.js Batch Send
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// Send to multiple recipients
|
|
211
|
+
const { data, error } = await resend.batch.send([
|
|
212
|
+
{
|
|
213
|
+
from: 'MyApp <noreply@yourdomain.com>',
|
|
214
|
+
to: 'user1@example.com',
|
|
215
|
+
subject: 'Notification',
|
|
216
|
+
react: NotificationEmail({ message: 'Update 1' }),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
from: 'MyApp <noreply@yourdomain.com>',
|
|
220
|
+
to: 'user2@example.com',
|
|
221
|
+
subject: 'Notification',
|
|
222
|
+
react: NotificationEmail({ message: 'Update 2' }),
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Webhooks for Delivery Status
|
|
230
|
+
|
|
231
|
+
### Setup Webhook Endpoint
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// app/api/webhooks/resend/route.ts
|
|
235
|
+
import { headers } from 'next/headers';
|
|
236
|
+
import { createHmac } from 'crypto';
|
|
237
|
+
|
|
238
|
+
export async function POST(req: Request) {
|
|
239
|
+
const body = await req.text();
|
|
240
|
+
const signature = headers().get('resend-signature');
|
|
241
|
+
|
|
242
|
+
// Verify webhook signature
|
|
243
|
+
const secret = process.env.RESEND_WEBHOOK_SECRET!;
|
|
244
|
+
const expectedSignature = createHmac('sha256', secret)
|
|
245
|
+
.update(body)
|
|
246
|
+
.digest('hex');
|
|
247
|
+
|
|
248
|
+
if (signature !== expectedSignature) {
|
|
249
|
+
return new Response('Invalid signature', { status: 401 });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const event = JSON.parse(body);
|
|
253
|
+
|
|
254
|
+
switch (event.type) {
|
|
255
|
+
case 'email.delivered':
|
|
256
|
+
await handleEmailDelivered(event.data);
|
|
257
|
+
break;
|
|
258
|
+
case 'email.bounced':
|
|
259
|
+
await handleEmailBounced(event.data);
|
|
260
|
+
break;
|
|
261
|
+
case 'email.complained':
|
|
262
|
+
await handleEmailComplained(event.data);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return new Response('OK');
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Best Practices
|
|
273
|
+
|
|
274
|
+
### Background Jobs (with Hangfire)
|
|
275
|
+
|
|
276
|
+
```csharp
|
|
277
|
+
// Don't block HTTP requests with email sending
|
|
278
|
+
BackgroundJob.Enqueue<IEmailService>(
|
|
279
|
+
x => x.SendWelcomeEmailAsync(email, name));
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Rate Limiting
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Implement rate limiting for email sends
|
|
286
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
287
|
+
import { Redis } from '@upstash/redis';
|
|
288
|
+
|
|
289
|
+
const ratelimit = new Ratelimit({
|
|
290
|
+
redis: Redis.fromEnv(),
|
|
291
|
+
limiter: Ratelimit.slidingWindow(10, '1 h'), // 10 emails per hour
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const { success } = await ratelimit.limit(userId);
|
|
295
|
+
if (!success) {
|
|
296
|
+
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await resend.emails.send({ ... });
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Retry Logic (.NET)
|
|
303
|
+
|
|
304
|
+
```csharp
|
|
305
|
+
using Polly;
|
|
306
|
+
|
|
307
|
+
var retryPolicy = Policy
|
|
308
|
+
.Handle<HttpRequestException>()
|
|
309
|
+
.WaitAndRetryAsync(3, retryAttempt =>
|
|
310
|
+
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
|
|
311
|
+
|
|
312
|
+
await retryPolicy.ExecuteAsync(async () =>
|
|
313
|
+
{
|
|
314
|
+
await _resend.EmailSendAsync(message);
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Domain Verification
|
|
321
|
+
|
|
322
|
+
1. Go to Resend Dashboard → Domains
|
|
323
|
+
2. Add your domain (e.g., `yourdomain.com`)
|
|
324
|
+
3. Add DNS records (SPF, DKIM, DMARC)
|
|
325
|
+
4. Wait for verification (up to 72 hours)
|
|
326
|
+
|
|
327
|
+
### DNS Records Example
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
TXT @ v=spf1 include:resend.com ~all
|
|
331
|
+
TXT resend._domainkey <DKIM_VALUE>
|
|
332
|
+
TXT _dmarc v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Common Email Templates
|
|
338
|
+
|
|
339
|
+
### Order Confirmation
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
export default function OrderConfirmation({ orderNumber, total, items }) {
|
|
343
|
+
return (
|
|
344
|
+
<Html>
|
|
345
|
+
<Container>
|
|
346
|
+
<Text>Order #{orderNumber} Confirmed</Text>
|
|
347
|
+
<Text>Total: ${total}</Text>
|
|
348
|
+
<ul>
|
|
349
|
+
{items.map(item => (
|
|
350
|
+
<li key={item.id}>{item.name} - ${item.price}</li>
|
|
351
|
+
))}
|
|
352
|
+
</ul>
|
|
353
|
+
</Container>
|
|
354
|
+
</Html>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Magic Link Auth
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
export default function MagicLink({ url, expiresIn }) {
|
|
363
|
+
return (
|
|
364
|
+
<Html>
|
|
365
|
+
<Container>
|
|
366
|
+
<Text>Sign in to your account</Text>
|
|
367
|
+
<Button href={url}>Sign In</Button>
|
|
368
|
+
<Text>This link expires in {expiresIn} minutes.</Text>
|
|
369
|
+
</Container>
|
|
370
|
+
</Html>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## References
|
|
378
|
+
|
|
379
|
+
- [Resend Documentation](https://resend.com/docs)
|
|
380
|
+
- [React Email](https://react.email/)
|
|
381
|
+
- [Resend .NET SDK](https://github.com/resend/resend-dotnet)
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
*MORPH-SPEC by Polymorphism Tech*
|