supabase-functions
SKILL.md
Supabase Edge Functions Skill
Serverless functions with Deno runtime.
Quick Reference
| Task | Command |
|---|---|
| Create function | supabase functions new <name> |
| Serve locally | supabase functions serve |
| Deploy | supabase functions deploy <name> |
| Delete | supabase functions delete <name> |
| Set secrets | supabase secrets set KEY=value |
| List secrets | supabase secrets list |
Create Function
supabase functions new hello-world
Creates: supabase/functions/hello-world/index.ts
Basic Function Structure
// supabase/functions/hello-world/index.ts
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
Deno.serve(async (req: Request) => {
const { name } = await req.json()
return new Response(
JSON.stringify({ message: `Hello ${name}!` }),
{ headers: { "Content-Type": "application/json" } }
)
})
Local Development
Serve All Functions
supabase functions serve
Serve with Environment Variables
supabase functions serve --env-file .env
Serve without JWT Verification
supabase functions serve --no-verify-jwt
Test Function
curl -i --request POST \
'http://localhost:54321/functions/v1/hello-world' \
--header 'Authorization: Bearer <ANON_KEY>' \
--header 'Content-Type: application/json' \
--data '{"name":"World"}'
CORS Handling
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
Deno.serve(async (req: Request) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const { name } = await req.json()
return new Response(
JSON.stringify({ message: `Hello ${name}!` }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400 }
)
}
})
Using Supabase Client
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
// Create Supabase client with service role
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Or with user's JWT (respects RLS)
const authHeader = req.headers.get('Authorization')!
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
)
// Query with user context
const { data, error } = await supabaseClient
.from('posts')
.select('*')
return new Response(
JSON.stringify({ data, error }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
Get User from JWT
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: req.headers.get('Authorization')! } } }
)
const { data: { user }, error } = await supabaseClient.auth.getUser()
if (error || !user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
return new Response(
JSON.stringify({ userId: user.id, email: user.email }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
Environment Variables & Secrets
Set Secrets
# Single secret
supabase secrets set API_KEY=abc123
# Multiple secrets
supabase secrets set API_KEY=abc123 DB_PASSWORD=secret
# From file
supabase secrets set --env-file .env
Access in Function
const apiKey = Deno.env.get('API_KEY')
const supabaseUrl = Deno.env.get('SUPABASE_URL')
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
Built-in Variables
SUPABASE_URL- Project URLSUPABASE_ANON_KEY- Anon keySUPABASE_SERVICE_ROLE_KEY- Service role keySUPABASE_DB_URL- Direct database connection
Deploy Function
Deploy Single Function
supabase functions deploy hello-world
Deploy without JWT Verification
supabase functions deploy webhook-handler --no-verify-jwt
Deploy All Functions
supabase functions deploy
Invoke from JavaScript
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'World' }
})
With Custom Headers
const { data, error } = await supabase.functions.invoke('api-handler', {
body: { action: 'create' },
headers: { 'x-custom-header': 'value' }
})
Webhook Function
// supabase/functions/stripe-webhook/index.ts
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import Stripe from 'npm:stripe@14'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
apiVersion: '2023-10-16'
})
const cryptoProvider = Stripe.createSubtleCryptoProvider()
Deno.serve(async (req: Request) => {
const signature = req.headers.get('Stripe-Signature')!
const body = await req.text()
let event: Stripe.Event
try {
event = await stripe.webhooks.constructEventAsync(
body,
signature,
Deno.env.get('STRIPE_WEBHOOK_SECRET')!,
undefined,
cryptoProvider
)
} catch (err) {
return new Response(
JSON.stringify({ error: `Webhook Error: ${err.message}` }),
{ status: 400 }
)
}
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object
// Handle successful checkout
break
default:
console.log(`Unhandled event type ${event.type}`)
}
return new Response(JSON.stringify({ received: true }))
})
Database Webhook
Trigger function on database changes:
-- Create webhook trigger
CREATE OR REPLACE FUNCTION notify_function()
RETURNS trigger AS $$
BEGIN
PERFORM net.http_post(
url := 'https://<project>.supabase.co/functions/v1/handle-insert',
headers := '{"Authorization": "Bearer <SERVICE_KEY>"}'::jsonb,
body := row_to_json(NEW)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_insert
AFTER INSERT ON posts
FOR EACH ROW
EXECUTE FUNCTION notify_function();
Scheduled Functions (Cron)
-- Using pg_cron extension
SELECT cron.schedule(
'daily-cleanup',
'0 0 * * *', -- Every day at midnight
$$
SELECT net.http_post(
url := 'https://<project>.supabase.co/functions/v1/cleanup',
headers := '{"Authorization": "Bearer <SERVICE_KEY>"}'::jsonb
)
$$
);
Configuration (config.toml)
[functions.hello-world]
verify_jwt = true
[functions.webhook-handler]
verify_jwt = false
[functions.api-handler]
verify_jwt = true
import_map = "./supabase/functions/import_map.json"
Shared Code
Create _shared folder for reusable code:
supabase/functions/
├── _shared/
│ ├── cors.ts
│ └── supabase.ts
├── hello-world/
│ └── index.ts
└── api-handler/
└── index.ts
// _shared/cors.ts
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
// _shared/supabase.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
export const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// hello-world/index.ts
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import { corsHeaders } from '../_shared/cors.ts'
import { supabaseAdmin } from '../_shared/supabase.ts'
Deno.serve(async (req: Request) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// Use shared client
const { data } = await supabaseAdmin.from('posts').select('*')
return new Response(
JSON.stringify({ data }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
Limits
| Limit | Value |
|---|---|
| CPU Time | 2 seconds |
| Request Timeout | 150 seconds |
| Wall Clock (Pro) | 400 seconds |
| Bundle Size | 20 MB |
References
- function-patterns.md - Common patterns
- deno-imports.md - Import syntax