supabase-edge-functions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Supabase Edge Functions Best Practices

Supabase Edge Functions 最佳实践

Comprehensive guide for Supabase Edge Functions development, debugging, and integration with database triggers.
一份关于Supabase Edge Functions开发、调试以及与数据库触发器集成的全面指南。

Core Concepts

核心概念

Authentication Patterns

认证模式

Edge Functions support two authentication modes:
  1. JWT Verification (verify_jwt: true) - Default, validates Authorization header contains valid JWT
  2. Custom Auth (verify_jwt: false) - Function handles auth internally (API keys, webhooks)
Service Role vs Anon Key:
  • Anon key (
    eyJ...
    , ~219 chars): JWT for client-side, limited permissions via RLS
  • Service role key (
    sb_secret_...
    , ~600 chars): Full admin access, bypasses RLS, NEVER expose to client
Edge Functions支持两种认证模式:
  1. JWT验证(verify_jwt: true) - 默认模式,验证Authorization请求头中包含有效的JWT
  2. 自定义认证(verify_jwt: false) - 由函数内部处理认证逻辑(如API密钥、Webhook)
服务角色密钥 vs 匿名密钥:
  • 匿名密钥
    eyJ...
    ,约219字符):客户端侧使用的JWT,通过行级安全策略(RLS)限制权限
  • 服务角色密钥
    sb_secret_...
    ,约600字符):拥有完整管理员权限,绕过RLS,绝不能暴露给客户端

Database Trigger Integration

数据库触发器集成

When calling Edge Functions from database triggers using
pg_net
:
sql
SELECT net.http_post(
  url := 'https://project.supabase.co/functions/v1/function-name',
  headers := jsonb_build_object(
    'Content-Type', 'application/json',
    'Authorization', 'Bearer ' || v_service_role_key  -- Must be SERVICE ROLE KEY
  ),
  body := jsonb_build_object('run_id', p_run_id)
);
Critical: Database triggers MUST use service role key, not anon key.
当使用
pg_net
从数据库触发器调用Edge Functions时:
sql
SELECT net.http_post(
  url := 'https://project.supabase.co/functions/v1/function-name',
  headers := jsonb_build_object(
    'Content-Type', 'application/json',
    'Authorization', 'Bearer ' || v_service_role_key  -- 必须使用服务角色密钥
  ),
  body := jsonb_build_object('run_id', p_run_id)
);
重点注意: 数据库触发器必须使用服务角色密钥,而非匿名密钥。

Common Issues and Solutions

常见问题与解决方案

Issue 1: Auth Token Mismatch

问题1:认证令牌不匹配

Symptoms:
  • Logs show: "Token prefix: eyJ... Expected prefix: sb_secret_..."
  • Function returns 401 or auth errors
  • Database trigger calls fail silently
Root Cause: Anon key stored instead of service role key in database config.
Solution:
  1. Get service role key from Dashboard > Settings > API > service_role
  2. Verify key starts with
    sb_secret_
    and is ~600 characters
  3. Update database config:
sql
UPDATE private.config
SET value = 'sb_secret_YOUR_KEY_HERE'
WHERE key = 'service_role_key';
Verification:
sql
SELECT
  key,
  LEFT(value, 10) as value_preview,
  LENGTH(value) as key_length
FROM private.config
WHERE key = 'service_role_key';
-- Should show: sb_secret_... with length ~600
症状:
  • 日志显示:"Token prefix: eyJ... Expected prefix: sb_secret_..."
  • 函数返回401错误或认证相关报错
  • 数据库触发器调用无响应
根因: 数据库配置中存储的是匿名密钥而非服务角色密钥。
解决方案:
  1. 从Supabase控制台 > 设置 > API > service_role 获取服务角色密钥
  2. 验证密钥以
    sb_secret_
    开头,长度约为600字符
  3. 更新数据库配置:
sql
UPDATE private.config
SET value = 'sb_secret_YOUR_KEY_HERE'
WHERE key = 'service_role_key';
验证:
sql
SELECT
  key,
  LEFT(value, 10) as value_preview,
  LENGTH(value) as key_length
FROM private.config
WHERE key = 'service_role_key';
-- 应显示:sb_secret_... 且长度约为600

Issue 2: Function Deployment Errors

问题2:函数部署错误

Common Errors:
  • Import map not found
  • Module resolution failures
  • Type errors in Deno runtime
Solutions:
Import Maps: Use
deno.json
for dependencies:
json
{
  "imports": {
    "supabase": "jsr:@supabase/supabase-js@2",
    "postgres": "https://deno.land/x/postgres@v0.17.0/mod.ts"
  }
}
Type Safety: Import runtime types at top of function:
typescript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
Testing Locally:
bash
undefined
常见错误:
  • 未找到导入映射(Import map)
  • 模块解析失败
  • Deno运行时中的类型错误
解决方案:
导入映射: 使用
deno.json
管理依赖:
json
{
  "imports": {
    "supabase": "jsr:@supabase/supabase-js@2",
    "postgres": "https://deno.land/x/postgres@v0.17.0/mod.ts"
  }
}
类型安全: 在函数顶部导入运行时类型:
typescript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
本地测试:
bash
undefined

Install Supabase CLI

安装Supabase CLI

supabase functions serve function-name --env-file .env.local
supabase functions serve function-name --env-file .env.local

Test with curl

使用curl测试

curl -i --location --request POST 'http://localhost:54321/functions/v1/function-name'
--header 'Authorization: Bearer YOUR_ANON_KEY'
--header 'Content-Type: application/json'
--data '{"run_id":"test-uuid"}'
undefined
curl -i --location --request POST 'http://localhost:54321/functions/v1/function-name'
--header 'Authorization: Bearer YOUR_ANON_KEY'
--header 'Content-Type: application/json'
--data '{"run_id":"test-uuid"}'
undefined

Issue 3: Database Connection from Edge Functions

问题3:Edge Functions连接数据库

Pattern:
typescript
import { createClient } from "jsr:@supabase/supabase-js@2";

Deno.serve(async (req: Request) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!  // Service role for admin ops
  );

  // Now can bypass RLS for admin operations
  const { data, error } = await supabase
    .from('forecast_runs')
    .update({ status: 'processing' })
    .eq('id', runId);
});
Environment Variables: Edge Functions automatically have access to:
  • SUPABASE_URL
    - Project URL
  • SUPABASE_ANON_KEY
    - Public anon key
  • SUPABASE_SERVICE_ROLE_KEY
    - Admin service role key
实现模式:
typescript
import { createClient } from "jsr:@supabase/supabase-js@2";

Deno.serve(async (req: Request) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!  // 使用服务角色密钥执行管理员操作
  );

  // 此时可绕过RLS执行管理员操作
  const { data, error } = await supabase
    .from('forecast_runs')
    .update({ status: 'processing' })
    .eq('id', runId);
});
环境变量: Edge Functions自动可访问以下环境变量:
  • SUPABASE_URL
    - 项目URL
  • SUPABASE_ANON_KEY
    - 公开匿名密钥
  • SUPABASE_SERVICE_ROLE_KEY
    - 管理员服务角色密钥

Issue 4: Debugging Failed Triggers

问题4:调试失败的触发器

Check trigger configuration:
sql
SELECT * FROM private.config WHERE key IN ('supabase_url', 'service_role_key');
Check pg_net requests:
sql
SELECT * FROM net._http_response ORDER BY created DESC LIMIT 10;
Check Edge Function logs: Use Supabase MCP tool
get_logs
or Dashboard > Edge Functions > Logs
Enable verbose logging in function:
typescript
console.log('[function-name] Processing run_id:', runId);
console.log('[function-name] Request headers:', Object.fromEntries(req.headers));
console.log('[function-name] Environment check:', {
  hasUrl: !!Deno.env.get('SUPABASE_URL'),
  hasServiceKey: !!Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
});
检查触发器配置:
sql
SELECT * FROM private.config WHERE key IN ('supabase_url', 'service_role_key');
检查pg_net请求:
sql
SELECT * FROM net._http_response ORDER BY created DESC LIMIT 10;
查看Edge Functions日志: 使用Supabase MCP工具
get_logs
或控制台 > Edge Functions > 日志
在函数中启用详细日志:
typescript
console.log('[function-name] Processing run_id:', runId);
console.log('[function-name] Request headers:', Object.fromEntries(req.headers));
console.log('[function-name] Environment check:', {
  hasUrl: !!Deno.env.get('SUPABASE_URL'),
  hasServiceKey: !!Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
});

Deployment Workflow

部署工作流

1. Develop Locally

1. 本地开发

bash
supabase functions serve function-name
bash
supabase functions serve function-name

2. Test Locally

2. 本地测试

bash
curl -i --location --request POST 'http://localhost:54321/functions/v1/function-name' \
  --header 'Authorization: Bearer ANON_KEY' \
  --data '{"test": "data"}'
bash
curl -i --location --request POST 'http://localhost:54321/functions/v1/function-name' \
  --header 'Authorization: Bearer ANON_KEY' \
  --data '{"test": "data"}'

3. Deploy to Production

3. 部署到生产环境

bash
supabase functions deploy function-name
bash
supabase functions deploy function-name

4. Verify Deployment

4. 验证部署

bash
undefined
bash
undefined

Check function exists

检查函数是否存在

supabase functions list
supabase functions list

Test production endpoint

测试生产环境端点

curl -i --location --request POST 'https://PROJECT.supabase.co/functions/v1/function-name'
--header 'Authorization: Bearer ANON_KEY'
--data '{"test": "data"}'
undefined
curl -i --location --request POST 'https://PROJECT.supabase.co/functions/v1/function-name'
--header 'Authorization: Bearer ANON_KEY'
--data '{"test": "data"}'
undefined

5. Monitor Logs

5. 监控日志

bash
undefined
bash
undefined

Via CLI

通过CLI查看

supabase functions logs function-name
supabase functions logs function-name

Via Dashboard

通过控制台查看

Dashboard > Edge Functions > function-name > Logs
undefined
控制台 > Edge Functions > function-name > 日志
undefined

File Structure Best Practices

文件结构最佳实践

supabase/functions/
├── function-name/
│   ├── index.ts              # Main handler
│   ├── deno.json             # Import map
│   ├── parsers/              # Domain logic (separate from handler)
│   │   └── csv-parser.ts
│   └── _shared/              # Shared utilities (symlinked)
│       └── validation.ts
Shared Code: Use
_shared/
directory for code reused across functions. Supabase CLI automatically includes it.
supabase/functions/
├── function-name/
│   ├── index.ts              # 主处理函数
│   ├── deno.json             # 导入映射
│   ├── parsers/              # 领域逻辑(与处理函数分离)
│   │   └── csv-parser.ts
│   └── _shared/              # 共享工具类(软链接)
│       └── validation.ts
共享代码: 使用
_shared/
目录存储多个函数复用的代码。Supabase CLI会自动包含该目录下的文件。

Error Handling Pattern

错误处理模式

typescript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";

Deno.serve(async (req: Request) => {
  try {
    // Parse request
    const body = await req.json();

    // Validate input
    if (!body.run_id) {
      return new Response(
        JSON.stringify({ error: 'run_id required' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    // Process
    const result = await processData(body.run_id);

    // Success response
    return new Response(
      JSON.stringify({ success: true, data: result }),
      { status: 200, headers: { 'Content-Type': 'application/json' } }
    );

  } catch (error) {
    console.error('[function-name] Error:', error);

    return new Response(
      JSON.stringify({
        error: error.message,
        stack: error.stack  // Include for debugging, remove in production
      }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    );
  }
});
typescript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";

Deno.serve(async (req: Request) => {
  try {
    // 解析请求
    const body = await req.json();

    // 验证输入
    if (!body.run_id) {
      return new Response(
        JSON.stringify({ error: 'run_id 为必填项' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    // 处理业务逻辑
    const result = await processData(body.run_id);

    // 成功响应
    return new Response(
      JSON.stringify({ success: true, data: result }),
      { status: 200, headers: { 'Content-Type': 'application/json' } }
    );

  } catch (error) {
    console.error('[function-name] 错误:', error);

    return new Response(
      JSON.stringify({
        error: error.message,
        stack: error.stack  // 调试时保留,生产环境移除
      }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    );
  }
});

Security Best Practices

安全最佳实践

  1. Never expose service role key to client - Only use in Edge Functions or database
  2. Use RLS policies - Even with service role, validate permissions in function logic
  3. Validate all inputs - Never trust request data
  4. Rate limiting - Implement for public endpoints
  5. CORS configuration - Restrict origins in production
  6. Secrets management - Use Supabase secrets, not hardcoded values
  1. 绝不能向客户端暴露服务角色密钥 - 仅在Edge Functions或数据库中使用
  2. 使用RLS策略 - 即使使用服务角色密钥,也要在函数逻辑中验证权限
  3. 验证所有输入 - 绝不要信任请求数据
  4. 速率限制 - 为公开端点实现速率限制
  5. CORS配置 - 生产环境中限制允许的来源
  6. 密钥管理 - 使用Supabase密钥管理功能,不要硬编码密钥

References

参考资料