Functions and production
Add a trusted Edge Function
Use a server-side function for secrets and privileged integrations while still validating identity, authorization, input, and external responses.
9 minute lesson
An Edge Function can keep provider secrets and trusted integration code out of the browser. It does not become safe merely because it runs on the server — a function that trusts its input is just a new attack surface with lower latency.
The checklist for every function is the same: verify the caller, authorize the requested resource, validate input, set timeouts, and handle retries deliberately. Skip any one of them and the function undoes protection the rest of the platform provides.
Build one that performs one narrow operation on the caller’s note: emailing it to the caller.
supabase functions new send-note
supabase functions serve send-note
The function establishes the caller’s identity first, using the request’s own authorization header:
import { createClient } from 'npm:@supabase/supabase-js@2'
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: req.headers.get('Authorization')! } } },
)
const { data: { user } } = await supabase.auth.getUser()
if (!user) return new Response('Unauthorized', { status: 401 })
const { noteId } = await req.json()
const { data: note } = await supabase
.from('notes').select().eq('id', noteId).single()
if (!note) return new Response('Not found', { status: 404 })
// call the email provider here, with a timeout
return new Response('Sent', { status: 200 })
})
Because this client carries the caller’s token, RLS still applies: asking for another user’s noteId returns nothing, and the function answers 404. Prefer this pattern — keep the user-scoped client for reads and writes so the database keeps enforcing the boundary for you.
Sometimes a step genuinely needs privilege. A service-role client inside the function bypasses RLS, so use it only after the function establishes the same boundary itself: verify the caller, authorize the requested resource, validate input, then perform the one privileged statement and nothing broader.
Secrets live in function configuration, never in code:
supabase secrets set RESEND_API_KEY=re_8fKm2Vw...
Read it with Deno.env.get('RESEND_API_KEY'). For the outbound call, set timeouts (AbortSignal.timeout(5000) on fetch), check the provider’s response status, and decide what a retry means before adding one — an email sent twice is a bug you cannot unsend.
Test the denied paths before the happy one. A request with a missing session must get 401. A valid session sending another user’s note ID must get 404. Only when both hold does the success case mean anything.
Lesson completed