needware-cli 1.5.22 → 1.5.23
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/dist/tools/add-secret-tool.d.ts +15 -0
- package/dist/tools/add-secret-tool.d.ts.map +1 -0
- package/dist/tools/add-secret-tool.js +53 -0
- package/dist/tools/add-secret-tool.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/tool-registry.d.ts.map +1 -1
- package/dist/tools/tool-registry.js +2 -0
- package/dist/tools/tool-registry.js.map +1 -1
- package/package.json +1 -1
- package/skills/resend-integration/SKILL.md +657 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Secret Tool
|
|
3
|
+
* Tool for adding secrets to Supabase project
|
|
4
|
+
*/
|
|
5
|
+
import { BaseTool } from './base-tool.js';
|
|
6
|
+
import { ToolDefinition, ToolResult } from '../types/tool.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
export declare class AddSecretTool extends BaseTool {
|
|
9
|
+
definition: ToolDefinition;
|
|
10
|
+
getZodSchema(): {
|
|
11
|
+
placeholder: z.ZodString;
|
|
12
|
+
};
|
|
13
|
+
execute(input: Record<string, any>): Promise<ToolResult>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=add-secret-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-secret-tool.d.ts","sourceRoot":"","sources":["../../src/tools/add-secret-tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,qBAAa,aAAc,SAAQ,QAAQ;IACzC,UAAU,EAAE,cAAc,CAaxB;IAEF,YAAY;;;IAMN,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;CA4B/D"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Secret Tool
|
|
3
|
+
* Tool for adding secrets to Supabase project
|
|
4
|
+
*/
|
|
5
|
+
import { BaseTool } from './base-tool.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
export class AddSecretTool extends BaseTool {
|
|
9
|
+
definition = {
|
|
10
|
+
name: 'add-secret',
|
|
11
|
+
description: 'Add a secret (environment variable) to the Supabase project for use in Edge Functions',
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
placeholder: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'The placeholder of the secret (environment variable), e.g., RESEND_API_KEY',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ['placeholder'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
getZodSchema() {
|
|
24
|
+
return {
|
|
25
|
+
placeholder: z.string().min(1).describe('The placeholder of the secret (environment variable)'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async execute(input) {
|
|
29
|
+
try {
|
|
30
|
+
// 验证输入参数
|
|
31
|
+
this.validateInput(input, ['placeholder']);
|
|
32
|
+
const { placeholder } = input;
|
|
33
|
+
console.log(chalk.blue(`\n🔐 准备添加密钥: ${placeholder}...\n`));
|
|
34
|
+
console.log(chalk.green(`\n✓ 密钥 "${placeholder}" 准备就绪\n`));
|
|
35
|
+
// 构建返回的 JSON 数据
|
|
36
|
+
const resultData = {
|
|
37
|
+
placeholder: placeholder,
|
|
38
|
+
secret_added: false,
|
|
39
|
+
secret_names: [placeholder],
|
|
40
|
+
requires_approval: true,
|
|
41
|
+
message: '密钥等待用户提供值',
|
|
42
|
+
instructions: `请在 Supabase 项目设置中添加此密钥,或使用 Supabase CLI: supabase secrets set ${placeholder}=your_value_here`
|
|
43
|
+
};
|
|
44
|
+
return this.successWithContent(JSON.stringify(resultData, null, 2));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : '生成密钥请求失败';
|
|
48
|
+
console.log(chalk.red(`\n✖ ${message}\n`));
|
|
49
|
+
return this.errorWithContent(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=add-secret-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-secret-tool.js","sourceRoot":"","sources":["../../src/tools/add-secret-tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,OAAO,aAAc,SAAQ,QAAQ;IACzC,UAAU,GAAmB;QAC3B,IAAI,EAAE,YAAY;QAClB,WAAW,EAAE,uFAAuF;QACpG,YAAY,EAAE;YACZ,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,WAAW,EAAE;oBACX,IAAI,EAAE,QAAQ;oBACd,WAAW,EAAE,4EAA4E;iBAC1F;aACF;YACD,QAAQ,EAAE,CAAC,aAAa,CAAC;SAC1B;KACF,CAAC;IAEF,YAAY;QACV,OAAO;YACL,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sDAAsD,CAAC;SAChG,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,KAA0B;QACtC,IAAI,CAAC;YACH,SAAS;YACT,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;YAE3C,MAAM,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;YAE9B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,WAAW,OAAO,CAAC,CAAC,CAAC;YAE5D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,WAAW,UAAU,CAAC,CAAC,CAAC;YAE3D,gBAAgB;YAChB,MAAM,UAAU,GAAG;gBACjB,WAAW,EAAE,WAAW;gBACxB,YAAY,EAAE,KAAK;gBACnB,YAAY,EAAE,CAAC,WAAW,CAAC;gBAC3B,iBAAiB,EAAE,IAAI;gBACvB,OAAO,EAAE,WAAW;gBACpB,YAAY,EAAE,iEAAiE,WAAW,kBAAkB;aAC7G,CAAC;YAEF,OAAO,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACtE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;YACpE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,OAAO,IAAI,CAAC,CAAC,CAAC;YAC3C,OAAO,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;CACF"}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export { SupabaseEnableTool } from './supabase-enable-tool.js';
|
|
|
6
6
|
export { SupabaseDeployFunctionsTool } from './supabase-deploy-functions-tool.js';
|
|
7
7
|
export { SupabaseExecuteSqlTool } from './supabase-execute-sql-tool.js';
|
|
8
8
|
export { AiGatewayEnableTool } from './ai-gateway-enable-tool.js';
|
|
9
|
+
export { AddSecretTool } from './add-secret-tool.js';
|
|
9
10
|
export { ToolRegistry, ToolConfig } from './tool-registry.js';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/tools/index.js
CHANGED
|
@@ -6,5 +6,6 @@ export { SupabaseEnableTool } from './supabase-enable-tool.js';
|
|
|
6
6
|
export { SupabaseDeployFunctionsTool } from './supabase-deploy-functions-tool.js';
|
|
7
7
|
export { SupabaseExecuteSqlTool } from './supabase-execute-sql-tool.js';
|
|
8
8
|
export { AiGatewayEnableTool } from './ai-gateway-enable-tool.js';
|
|
9
|
+
export { AddSecretTool } from './add-secret-tool.js';
|
|
9
10
|
export { ToolRegistry } from './tool-registry.js';
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
package/dist/tools/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAkB,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAc,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAkB,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAc,MAAM,oBAAoB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../../src/tools/tool-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-registry.d.ts","sourceRoot":"","sources":["../../src/tools/tool-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOxD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAgC;IAEpD;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI;IAa5C;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAIjC;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAI1C;;OAEG;IACH,MAAM,CAAC,iBAAiB,IAAI,cAAc,EAAE;IAI5C;;OAEG;IACH,MAAM,CAAC,MAAM,IAAI,IAAI,EAAE;IAIvB;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIjC;;;;OAIG;WACU,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;IAQ5E;;OAEG;IACH,MAAM,CAAC,gBAAgB,IAAI,GAAG,EAAE;CAOjC"}
|
|
@@ -6,6 +6,7 @@ import { SupabaseEnableTool } from './supabase-enable-tool.js';
|
|
|
6
6
|
import { SupabaseDeployFunctionsTool } from './supabase-deploy-functions-tool.js';
|
|
7
7
|
import { SupabaseExecuteSqlTool } from './supabase-execute-sql-tool.js';
|
|
8
8
|
import { AiGatewayEnableTool } from './ai-gateway-enable-tool.js';
|
|
9
|
+
import { AddSecretTool } from './add-secret-tool.js';
|
|
9
10
|
export class ToolRegistry {
|
|
10
11
|
static tools = new Map();
|
|
11
12
|
/**
|
|
@@ -21,6 +22,7 @@ export class ToolRegistry {
|
|
|
21
22
|
this.register(new SupabaseDeployFunctionsTool(config?.gateway));
|
|
22
23
|
this.register(new SupabaseExecuteSqlTool(config?.gateway));
|
|
23
24
|
this.register(new AiGatewayEnableTool(config?.gateway));
|
|
25
|
+
this.register(new AddSecretTool(config?.gateway));
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* 注册工具
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../../src/tools/tool-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../../src/tools/tool-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAQrD,MAAM,OAAO,YAAY;IACf,MAAM,CAAC,KAAK,GAAsB,IAAI,GAAG,EAAE,CAAC;IAEpD;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,MAAmB;QACnC,WAAW;QACX,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAEnB,cAAc;QACd,eAAe;QACf,IAAI,CAAC,QAAQ,CAAC,IAAI,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,QAAQ,CAAC,IAAI,2BAA2B,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAChE,IAAI,CAAC,QAAQ,CAAC,IAAI,sBAAsB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,QAAQ,CAAC,IAAI,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;QACxD,IAAI,CAAC,QAAQ,CAAC,IAAI,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAU;QACxB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,iBAAiB;QACtB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACxE,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,MAAM;QACX,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,KAA0B;QAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,gBAAgB;QACrB,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC5C,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,WAAW,EAAE,GAAG,CAAC,WAAW;YAC5B,YAAY,EAAE,GAAG,CAAC,YAAY;SAC/B,CAAC,CAAC,CAAC;IACN,CAAC;;AAGH,qCAAqC;AACrC,YAAY,CAAC,UAAU,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resend-integration
|
|
3
|
+
description: Complete Resend email integration for sending transactional emails, notifications, and alerts. REQUIRES Supabase integration as prerequisite (Resend functions run on Supabase Edge Functions). Use this skill when users mention sending emails, email notifications, transactional emails, or email alerts. 使用此技能处理邮件发送、邮件通知、事务性邮件等功能。必须先集成 Supabase 才能使用此技能。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Resend Integration
|
|
7
|
+
|
|
8
|
+
## Before Starting - Integration Check
|
|
9
|
+
|
|
10
|
+
**CRITICAL**: Resend integration requires Supabase Edge Functions. Before proceeding, always check Supabase integration status:
|
|
11
|
+
|
|
12
|
+
**Step 1: Check Supabase Integration**
|
|
13
|
+
|
|
14
|
+
Check if Supabase is already integrated:
|
|
15
|
+
- Look for `src/lib/supabase.ts` file
|
|
16
|
+
- Check `.env` file for Supabase environment variables:
|
|
17
|
+
- `VITE_SUPABASE_PROJECT_ID`
|
|
18
|
+
- `VITE_SUPABASE_PUBLISHABLE_KEY`
|
|
19
|
+
- `VITE_SUPABASE_URL`
|
|
20
|
+
- Check for `supabase/functions/` directory
|
|
21
|
+
|
|
22
|
+
**Step 2: Handle Based on Status**
|
|
23
|
+
|
|
24
|
+
**If Supabase is already integrated** (supabase.ts exists with valid configuration):
|
|
25
|
+
- ✓ Proceed with Resend integration
|
|
26
|
+
- Inform user: "✓ Supabase is integrated. Proceeding with Resend setup."
|
|
27
|
+
|
|
28
|
+
**If Supabase is NOT integrated** (no supabase.ts or missing environment variables):
|
|
29
|
+
- ❌ Stop immediately
|
|
30
|
+
- Inform user: "⚠️ Supabase integration is required before setting up Resend. Resend email functions run on Supabase Edge Functions."
|
|
31
|
+
- Suggest: "Please enable Supabase first by saying 'Enable Cloud' or use the supabase-integration skill."
|
|
32
|
+
- Do NOT proceed with Resend setup until Supabase is properly configured
|
|
33
|
+
|
|
34
|
+
**Step 3: Verify Edge Functions Support**
|
|
35
|
+
|
|
36
|
+
Once Supabase is confirmed:
|
|
37
|
+
- Check if `supabase/functions/` directory exists
|
|
38
|
+
- If not, create the directory structure
|
|
39
|
+
- Proceed with Resend Edge Function creation
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### When to Use This Skill
|
|
46
|
+
|
|
47
|
+
Use this skill when working with:
|
|
48
|
+
- Transactional emails (welcome emails, password resets)
|
|
49
|
+
- Email notifications (alerts, reminders)
|
|
50
|
+
- Batch email sending
|
|
51
|
+
- HTML email templates
|
|
52
|
+
- Email verification and confirmation
|
|
53
|
+
- User communication
|
|
54
|
+
|
|
55
|
+
**关键原则**:
|
|
56
|
+
- ✅ 使用 Resend 作为邮件发送服务
|
|
57
|
+
- ✅ 所有邮件功能通过 Supabase Edge Functions 实现
|
|
58
|
+
- ⚠️ **必须先集成 Supabase** - Resend 依赖 Supabase Edge Functions 运行
|
|
59
|
+
|
|
60
|
+
## Initial Setup
|
|
61
|
+
|
|
62
|
+
### Prerequisites
|
|
63
|
+
|
|
64
|
+
**Required**:
|
|
65
|
+
1. ✅ **Supabase must be integrated** - Resend functions run on Supabase Edge Functions
|
|
66
|
+
- Verify `src/lib/supabase.ts` exists
|
|
67
|
+
- Verify Supabase environment variables are configured
|
|
68
|
+
- Verify `supabase/functions/` directory exists
|
|
69
|
+
|
|
70
|
+
2. 🔑 **Resend API key** - If no API key is available, remind user to:
|
|
71
|
+
- Visit [Resend Dashboard](https://resend.com/api-keys)
|
|
72
|
+
- Create a new API key
|
|
73
|
+
- Add key to Supabase Edge Functions environment variables (`supabase/.env.local`)
|
|
74
|
+
|
|
75
|
+
### Installation in Edge Functions
|
|
76
|
+
|
|
77
|
+
**Step 1: Import Resend in Edge Function**
|
|
78
|
+
|
|
79
|
+
Create a new Edge Function file in `/supabase/functions/<function-name>/index.ts`:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
83
|
+
import { Resend } from "https://esm.sh/resend@2.0.0";
|
|
84
|
+
|
|
85
|
+
const resend = new Resend(Deno.env.get("RESEND_API_KEY"));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Step 2: Configure Environment Variables**
|
|
89
|
+
|
|
90
|
+
Add to `/supabase/.env.local`:
|
|
91
|
+
|
|
92
|
+
```env
|
|
93
|
+
RESEND_API_KEY=re_your_api_key_here
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Step 3: CORS Headers Setup**
|
|
97
|
+
|
|
98
|
+
Always include CORS headers for client-side invocation:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const corsHeaders = {
|
|
102
|
+
"Access-Control-Allow-Origin": "*",
|
|
103
|
+
"Access-Control-Allow-Headers":
|
|
104
|
+
"authorization, x-client-info, apikey, content-type",
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Email Sending Patterns
|
|
109
|
+
|
|
110
|
+
### Basic Email Template
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
114
|
+
import { Resend } from "https://esm.sh/resend@2.0.0";
|
|
115
|
+
|
|
116
|
+
const resend = new Resend(Deno.env.get("RESEND_API_KEY"));
|
|
117
|
+
|
|
118
|
+
const corsHeaders = {
|
|
119
|
+
"Access-Control-Allow-Origin": "*",
|
|
120
|
+
"Access-Control-Allow-Headers":
|
|
121
|
+
"authorization, x-client-info, apikey, content-type",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
interface EmailRequest {
|
|
125
|
+
to: string;
|
|
126
|
+
subject: string;
|
|
127
|
+
message: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const handler = async (req: Request): Promise<Response> => {
|
|
131
|
+
// Handle CORS preflight requests
|
|
132
|
+
if (req.method === "OPTIONS") {
|
|
133
|
+
return new Response(null, { headers: corsHeaders });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { to, subject, message }: EmailRequest = await req.json();
|
|
138
|
+
|
|
139
|
+
const { data, error } = await resend.emails.send({
|
|
140
|
+
from: "Your App <onboarding@resend.dev>",
|
|
141
|
+
to: [to],
|
|
142
|
+
subject: subject,
|
|
143
|
+
html: `<p>${message}</p>`,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (error) {
|
|
147
|
+
throw new Error(error.message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return new Response(
|
|
151
|
+
JSON.stringify({ success: true, data }),
|
|
152
|
+
{
|
|
153
|
+
status: 200,
|
|
154
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
} catch (error: any) {
|
|
158
|
+
console.error("Error sending email:", error);
|
|
159
|
+
return new Response(
|
|
160
|
+
JSON.stringify({ error: error.message }),
|
|
161
|
+
{
|
|
162
|
+
status: 500,
|
|
163
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
serve(handler);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Batch Email Sending
|
|
173
|
+
|
|
174
|
+
Send emails to multiple recipients:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
interface BatchEmailRequest {
|
|
178
|
+
recipients: {
|
|
179
|
+
name: string;
|
|
180
|
+
email: string;
|
|
181
|
+
}[];
|
|
182
|
+
subject: string;
|
|
183
|
+
message: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const handler = async (req: Request): Promise<Response> => {
|
|
187
|
+
if (req.method === "OPTIONS") {
|
|
188
|
+
return new Response(null, { headers: corsHeaders });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const { recipients, subject, message }: BatchEmailRequest = await req.json();
|
|
193
|
+
|
|
194
|
+
if (!recipients || recipients.length === 0) {
|
|
195
|
+
return new Response(
|
|
196
|
+
JSON.stringify({ error: "No recipients provided" }),
|
|
197
|
+
{
|
|
198
|
+
status: 400,
|
|
199
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Send emails in parallel
|
|
205
|
+
const results = await Promise.all(
|
|
206
|
+
recipients.map(async (recipient) => {
|
|
207
|
+
const emailResponse = await resend.emails.send({
|
|
208
|
+
from: "Your App <onboarding@resend.dev>",
|
|
209
|
+
to: [recipient.email],
|
|
210
|
+
subject: subject,
|
|
211
|
+
html: `<p>Hi ${recipient.name},</p><p>${message}</p>`,
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
recipient: recipient.email,
|
|
215
|
+
response: emailResponse
|
|
216
|
+
};
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return new Response(
|
|
221
|
+
JSON.stringify({ success: true, results }),
|
|
222
|
+
{
|
|
223
|
+
status: 200,
|
|
224
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
return new Response(
|
|
229
|
+
JSON.stringify({ error: error.message }),
|
|
230
|
+
{
|
|
231
|
+
status: 500,
|
|
232
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Alert Email Example
|
|
240
|
+
|
|
241
|
+
Complete example for sending alert emails:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
245
|
+
import { Resend } from "https://esm.sh/resend@2.0.0";
|
|
246
|
+
|
|
247
|
+
const resend = new Resend(Deno.env.get("RESEND_API_KEY"));
|
|
248
|
+
|
|
249
|
+
const corsHeaders = {
|
|
250
|
+
"Access-Control-Allow-Origin": "*",
|
|
251
|
+
"Access-Control-Allow-Headers":
|
|
252
|
+
"authorization, x-client-info, apikey, content-type",
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
interface AlertEmailRequest {
|
|
256
|
+
contacts: {
|
|
257
|
+
name: string;
|
|
258
|
+
email: string;
|
|
259
|
+
}[];
|
|
260
|
+
userName?: string;
|
|
261
|
+
message?: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const handler = async (req: Request): Promise<Response> => {
|
|
265
|
+
if (req.method === "OPTIONS") {
|
|
266
|
+
return new Response(null, { headers: corsHeaders });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const { contacts, userName = "用户", message }: AlertEmailRequest = await req.json();
|
|
271
|
+
|
|
272
|
+
if (!contacts || contacts.length === 0) {
|
|
273
|
+
return new Response(
|
|
274
|
+
JSON.stringify({ error: "No contacts provided" }),
|
|
275
|
+
{
|
|
276
|
+
status: 400,
|
|
277
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const results = await Promise.all(
|
|
283
|
+
contacts.map(async (contact) => {
|
|
284
|
+
const emailResponse = await resend.emails.send({
|
|
285
|
+
from: "Your App <onboarding@resend.dev>",
|
|
286
|
+
to: [contact.email],
|
|
287
|
+
subject: `⚠️ Alert: ${userName} triggered notification`,
|
|
288
|
+
html: `
|
|
289
|
+
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
290
|
+
<h1 style="color: #f59e0b; border-bottom: 2px solid #f59e0b; padding-bottom: 10px;">
|
|
291
|
+
⚠️ Alert Notification
|
|
292
|
+
</h1>
|
|
293
|
+
<p>Hi ${contact.name},</p>
|
|
294
|
+
<p>This is an alert notification from Your App.</p>
|
|
295
|
+
<p><strong>${userName}</strong> ${message || "has triggered an alert notification."}</p>
|
|
296
|
+
<div style="background: #fef3c7; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
|
297
|
+
<p style="margin: 0; color: #92400e;">
|
|
298
|
+
Please take appropriate action.
|
|
299
|
+
</p>
|
|
300
|
+
</div>
|
|
301
|
+
<p style="color: #666; font-size: 14px;">
|
|
302
|
+
This is an automated email from Your App.
|
|
303
|
+
</p>
|
|
304
|
+
</div>
|
|
305
|
+
`,
|
|
306
|
+
});
|
|
307
|
+
return { contact: contact.email, response: emailResponse };
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
console.log("Emails sent successfully:", results);
|
|
312
|
+
|
|
313
|
+
return new Response(
|
|
314
|
+
JSON.stringify({ success: true, results }),
|
|
315
|
+
{
|
|
316
|
+
status: 200,
|
|
317
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
} catch (error: any) {
|
|
321
|
+
console.error("Error in send-alert-email function:", error);
|
|
322
|
+
return new Response(
|
|
323
|
+
JSON.stringify({ error: error.message }),
|
|
324
|
+
{
|
|
325
|
+
status: 500,
|
|
326
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
serve(handler);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Email Templates
|
|
336
|
+
|
|
337
|
+
### Welcome Email Template
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const welcomeEmailHTML = (userName: string) => `
|
|
341
|
+
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
|
342
|
+
<h1>Welcome to Our App!</h1>
|
|
343
|
+
<p>Hi ${userName},</p>
|
|
344
|
+
<p>Thank you for signing up. We're excited to have you on board!</p>
|
|
345
|
+
<a href="https://yourapp.com/get-started"
|
|
346
|
+
style="background: #3b82f6; color: white; padding: 12px 24px;
|
|
347
|
+
text-decoration: none; border-radius: 6px; display: inline-block;">
|
|
348
|
+
Get Started
|
|
349
|
+
</a>
|
|
350
|
+
</div>
|
|
351
|
+
`;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Password Reset Template
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
const resetPasswordHTML = (resetLink: string) => `
|
|
358
|
+
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
|
359
|
+
<h1>Reset Your Password</h1>
|
|
360
|
+
<p>Click the button below to reset your password:</p>
|
|
361
|
+
<a href="${resetLink}"
|
|
362
|
+
style="background: #ef4444; color: white; padding: 12px 24px;
|
|
363
|
+
text-decoration: none; border-radius: 6px; display: inline-block;">
|
|
364
|
+
Reset Password
|
|
365
|
+
</a>
|
|
366
|
+
<p style="color: #666; font-size: 14px; margin-top: 20px;">
|
|
367
|
+
If you didn't request this, please ignore this email.
|
|
368
|
+
</p>
|
|
369
|
+
</div>
|
|
370
|
+
`;
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Notification Email Template
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
const notificationEmailHTML = (title: string, content: string) => `
|
|
377
|
+
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
378
|
+
<h2 style="color: #1f2937; border-bottom: 2px solid #e5e7eb; padding-bottom: 10px;">
|
|
379
|
+
${title}
|
|
380
|
+
</h2>
|
|
381
|
+
<div style="background: #f3f4f6; padding: 15px; border-radius: 8px; margin: 20px 0;">
|
|
382
|
+
<p style="margin: 0; color: #374151;">${content}</p>
|
|
383
|
+
</div>
|
|
384
|
+
<p style="color: #6b7280; font-size: 14px;">
|
|
385
|
+
This is an automated notification from Your App.
|
|
386
|
+
</p>
|
|
387
|
+
</div>
|
|
388
|
+
`;
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Invoking from Client
|
|
392
|
+
|
|
393
|
+
### Using Supabase Functions
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// In your React/Vue/etc. component
|
|
397
|
+
import { supabase } from '@/lib/supabase';
|
|
398
|
+
|
|
399
|
+
// Send single email
|
|
400
|
+
const sendEmail = async () => {
|
|
401
|
+
const { data, error } = await supabase.functions.invoke('send-email', {
|
|
402
|
+
body: {
|
|
403
|
+
to: 'user@example.com',
|
|
404
|
+
subject: 'Hello from Your App',
|
|
405
|
+
message: 'This is a test email'
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (error) {
|
|
410
|
+
console.error('Error sending email:', error);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.log('Email sent:', data);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Send alert emails
|
|
418
|
+
const sendAlertEmails = async () => {
|
|
419
|
+
const { data, error } = await supabase.functions.invoke('send-alert-email', {
|
|
420
|
+
body: {
|
|
421
|
+
contacts: [
|
|
422
|
+
{ name: 'John Doe', email: 'john@example.com' },
|
|
423
|
+
{ name: 'Jane Smith', email: 'jane@example.com' }
|
|
424
|
+
],
|
|
425
|
+
userName: '张三',
|
|
426
|
+
message: '触发了紧急通知'
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (error) {
|
|
431
|
+
console.error('Error sending alerts:', error);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log('Alerts sent:', data);
|
|
436
|
+
};
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Common Use Cases
|
|
440
|
+
|
|
441
|
+
### 1. Welcome Emails
|
|
442
|
+
Send when user signs up:
|
|
443
|
+
```typescript
|
|
444
|
+
const { data, error } = await supabase.functions.invoke('send-welcome-email', {
|
|
445
|
+
body: { email: user.email, name: user.name }
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### 2. Password Reset
|
|
450
|
+
Send password reset link:
|
|
451
|
+
```typescript
|
|
452
|
+
const { data, error } = await supabase.functions.invoke('send-password-reset', {
|
|
453
|
+
body: { email: user.email, resetToken: token }
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 3. Email Verification
|
|
458
|
+
Send verification code:
|
|
459
|
+
```typescript
|
|
460
|
+
const { data, error } = await supabase.functions.invoke('send-verification', {
|
|
461
|
+
body: { email: user.email, code: verificationCode }
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### 4. Transaction Confirmations
|
|
466
|
+
Send order/payment confirmations:
|
|
467
|
+
```typescript
|
|
468
|
+
const { data, error } = await supabase.functions.invoke('send-confirmation', {
|
|
469
|
+
body: { email: user.email, orderDetails: order }
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### 5. Scheduled Reminders
|
|
474
|
+
Send reminder emails:
|
|
475
|
+
```typescript
|
|
476
|
+
const { data, error } = await supabase.functions.invoke('send-reminder', {
|
|
477
|
+
body: { email: user.email, reminderText: reminder }
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Best Practices
|
|
482
|
+
|
|
483
|
+
### 1. Email Sender Configuration
|
|
484
|
+
|
|
485
|
+
**✅ DO**:
|
|
486
|
+
- Use custom domain for production: `Your App <noreply@yourdomain.com>`
|
|
487
|
+
- Use descriptive sender names
|
|
488
|
+
- Keep sender email consistent
|
|
489
|
+
|
|
490
|
+
**❌ DON'T**:
|
|
491
|
+
- Use `onboarding@resend.dev` in production (for testing only)
|
|
492
|
+
- Change sender email frequently
|
|
493
|
+
- Use confusing sender names
|
|
494
|
+
|
|
495
|
+
### 2. Error Handling
|
|
496
|
+
|
|
497
|
+
Always handle errors gracefully:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
try {
|
|
501
|
+
const { data, error } = await resend.emails.send({...});
|
|
502
|
+
|
|
503
|
+
if (error) {
|
|
504
|
+
console.error("Resend API error:", error);
|
|
505
|
+
throw new Error(error.message);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return new Response(JSON.stringify({ success: true, data }), {...});
|
|
509
|
+
} catch (error: any) {
|
|
510
|
+
console.error("Unexpected error:", error);
|
|
511
|
+
return new Response(
|
|
512
|
+
JSON.stringify({ error: error.message }),
|
|
513
|
+
{ status: 500, headers: {...} }
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 3. Rate Limiting
|
|
519
|
+
|
|
520
|
+
Implement rate limiting for batch sends:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
// Send emails in batches of 10
|
|
524
|
+
const batchSize = 10;
|
|
525
|
+
for (let i = 0; i < recipients.length; i += batchSize) {
|
|
526
|
+
const batch = recipients.slice(i, i + batchSize);
|
|
527
|
+
await Promise.all(batch.map(recipient => sendEmail(recipient)));
|
|
528
|
+
// Add delay between batches if needed
|
|
529
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### 4. HTML Email Best Practices
|
|
534
|
+
|
|
535
|
+
**✅ DO**:
|
|
536
|
+
- Use inline CSS styles
|
|
537
|
+
- Keep width under 600px
|
|
538
|
+
- Test on multiple email clients
|
|
539
|
+
- Include plain text fallback
|
|
540
|
+
- Use responsive design
|
|
541
|
+
- Add unsubscribe links (if applicable)
|
|
542
|
+
|
|
543
|
+
**❌ DON'T**:
|
|
544
|
+
- Use external CSS files
|
|
545
|
+
- Use JavaScript
|
|
546
|
+
- Use complex layouts
|
|
547
|
+
- Forget alt text for images
|
|
548
|
+
|
|
549
|
+
### 5. Security
|
|
550
|
+
|
|
551
|
+
**✅ DO**:
|
|
552
|
+
- Store API key in environment variables
|
|
553
|
+
- Validate input data
|
|
554
|
+
- Sanitize user-generated content
|
|
555
|
+
- Use HTTPS for all links
|
|
556
|
+
- Implement rate limiting
|
|
557
|
+
|
|
558
|
+
**❌ DON'T**:
|
|
559
|
+
- Hardcode API keys
|
|
560
|
+
- Trust user input without validation
|
|
561
|
+
- Include sensitive data in emails
|
|
562
|
+
- Allow arbitrary email sending
|
|
563
|
+
|
|
564
|
+
## Debugging
|
|
565
|
+
|
|
566
|
+
### Common Issues
|
|
567
|
+
|
|
568
|
+
**1. Email not received**:
|
|
569
|
+
- Check spam/junk folder
|
|
570
|
+
- Verify recipient email address
|
|
571
|
+
- Check Resend Dashboard for delivery status
|
|
572
|
+
- Verify sender domain configuration
|
|
573
|
+
|
|
574
|
+
**2. API Key errors**:
|
|
575
|
+
- Verify `RESEND_API_KEY` in environment variables
|
|
576
|
+
- Check API key is active in Resend Dashboard
|
|
577
|
+
- Ensure proper Deno.env.get() usage
|
|
578
|
+
|
|
579
|
+
**3. CORS errors**:
|
|
580
|
+
- Verify CORS headers are included
|
|
581
|
+
- Handle OPTIONS requests
|
|
582
|
+
- Check browser console for specific errors
|
|
583
|
+
|
|
584
|
+
**4. Function timeout**:
|
|
585
|
+
- Reduce batch size
|
|
586
|
+
- Optimize email sending logic
|
|
587
|
+
- Check for blocking operations
|
|
588
|
+
|
|
589
|
+
### Logging
|
|
590
|
+
|
|
591
|
+
Add comprehensive logging:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
console.log("Sending email to:", recipient.email);
|
|
595
|
+
console.log("Email response:", emailResponse);
|
|
596
|
+
console.error("Error sending email:", error);
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Testing
|
|
600
|
+
|
|
601
|
+
Test Edge Function locally:
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
supabase functions serve send-email --env-file supabase/.env.local
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Test with curl:
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
curl -i --location --request POST 'http://localhost:54321/functions/v1/send-email' \
|
|
611
|
+
--header 'Authorization: Bearer YOUR_ANON_KEY' \
|
|
612
|
+
--header 'Content-Type: application/json' \
|
|
613
|
+
--data '{"to":"test@example.com","subject":"Test","message":"Hello"}'
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## Project Structure
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
project-root/
|
|
620
|
+
├── supabase/
|
|
621
|
+
│ ├── functions/
|
|
622
|
+
│ │ ├── send-email/
|
|
623
|
+
│ │ │ └── index.ts # Basic email sending
|
|
624
|
+
│ │ ├── send-alert-email/
|
|
625
|
+
│ │ │ └── index.ts # Alert emails
|
|
626
|
+
│ │ ├── send-welcome-email/
|
|
627
|
+
│ │ │ └── index.ts # Welcome emails
|
|
628
|
+
│ │ └── send-notification/
|
|
629
|
+
│ │ └── index.ts # Notification emails
|
|
630
|
+
│ └── .env.local # RESEND_API_KEY
|
|
631
|
+
├── src/
|
|
632
|
+
│ ├── lib/
|
|
633
|
+
│ │ └── supabase.ts # Supabase Client
|
|
634
|
+
│ └── components/
|
|
635
|
+
│ └── EmailForm.tsx # Email sending UI
|
|
636
|
+
└── .env # Frontend env vars
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
## Additional Resources
|
|
640
|
+
|
|
641
|
+
- [Resend Documentation](https://resend.com/docs)
|
|
642
|
+
- [Resend API Reference](https://resend.com/docs/api-reference)
|
|
643
|
+
- [Email Templates Best Practices](https://resend.com/docs/send-with-react)
|
|
644
|
+
- [Supabase Edge Functions Guide](https://supabase.com/docs/guides/functions)
|
|
645
|
+
|
|
646
|
+
## Migration Checklist
|
|
647
|
+
|
|
648
|
+
When moving to production:
|
|
649
|
+
|
|
650
|
+
- [ ] Register custom domain in Resend Dashboard
|
|
651
|
+
- [ ] Update sender email from `onboarding@resend.dev` to custom domain
|
|
652
|
+
- [ ] Configure SPF, DKIM, and DMARC records
|
|
653
|
+
- [ ] Update `RESEND_API_KEY` with production key
|
|
654
|
+
- [ ] Test all email flows
|
|
655
|
+
- [ ] Set up email delivery monitoring
|
|
656
|
+
- [ ] Implement unsubscribe functionality (if needed)
|
|
657
|
+
- [ ] Add email preference management
|