Edge Functions
Serverless functions for operations that must happen on the server — payments, emails, sensitive API calls.
Why First: The API Key in the Browser
You're integrating Razorpay into your app to accept payments. The Razorpay documentation gives you two keys:
- A public key (
rzp_live_xxxxx) — safe to put in the browser - A secret key (
your-secret-key) — must never leave your server
You think: "I'll just put the secret key in my .env file with a VITE_ prefix and use it in my React code."
This is wrong. Every variable with the VITE_ prefix is bundled into your JavaScript and sent to every browser that opens your app. Anyone can open the browser DevTools, look at your JavaScript bundle, search for rzp_, and find your secret key. Within hours, they are using your Razorpay account to process fraudulent transactions.
Edge functions are the solution. They run on Supabase's servers — not in the browser. Your secret keys live there, invisible to any user.
What Edge Functions Are
An edge function is a small server-side function that:
- Runs close to your users (on servers distributed globally — "edge" means near the network edge, not the user's device)
- Has access to secrets that never reach the browser
- Can call any external API with credentials you'd never expose client-side
- Is deployed and managed by Supabase — you don't configure servers
When to Use Edge Functions
Use an edge function when the operation:
- Requires a secret key — payment processing, email sending, third-party API calls (Razorpay, Resend, Google Maps, Gemini AI)
- Must happen server-side for security — verifying a payment signature, checking webhook authenticity
- Requires the service_role key — bypassing RLS for admin operations, processing background jobs
- Should never be trusted from the client — calculating prices, granting credits, changing sensitive user data
Do NOT use an edge function for:
- Simple CRUD operations that RLS can protect
- Data that doesn't involve secrets or privilege escalation
- Anything you can do safely with the anon key + RLS
The Structure of an Edge Function
Edge functions run on Deno (a secure JavaScript runtime) rather than Node.js — but you write them in the same TypeScript you already know. Claude handles any Deno-specific differences.
Here is the structure of the actual Razorpay payment verification function from Udyogaseva (supabase/functions/razorpay-verify-payment/index.ts).
Every edge function follows this exact pattern:
Deno.env.get() — never hardcoded, never from the request body.The Two Supabase Clients in an Edge Function
Inside an edge function, you create two different Supabase clients with different purposes:
Client 1: The caller's client (anon key + user's JWT)
Client 2: The admin client (service_role key)
The order matters:
- Verify who the caller is (client 1)
- Verify they are allowed to do what they're requesting (application logic)
- Do the privileged operation (client 2)
Never use the admin client before verifying the caller's identity and permissions.
Udyogaseva's Edge Functions
Udyogaseva has 25 edge functions. Here are the categories:
Payment processing:
razorpay-create-order— Creates a Razorpay order (secret key stays server-side)razorpay-verify-payment— Verifies the payment signature after completionrazorpay-webhook— Processes Razorpay webhook events for payment failures
Email:
send-email— Sends transactional emails via Resendemail-application-received— Notifies recruiters of new applicationsemail-interview-scheduled— Notifies candidates when an interview is bookedemail-status-update— Application status change notifications
AI:
generate-candidate-summary— Calls Google Gemini API to write a profile summarygemini-proxy— Proxy for Gemini API calls (keeps the API key server-side)
Admin operations:
admin-permanent-delete— Permanently deletes soft-deleted records (super_admin only)process-account-deletion— Handles GDPR-style account deletion requestssentry-issues— Proxies the Sentry Issues API (keeps the Sentry token server-side)
Notice: every operation that requires a secret key or elevated privilege is in an edge function. Everything else — CRUD that RLS can protect — is done directly with the Supabase JavaScript client.
Deploying Edge Functions
supabase/functions/your-function-name/index.ts.npx supabase secrets set YOUR_SECRET_KEY=value. This stores it encrypted in Supabase's infrastructure — not in your codebase.npx supabase functions deploy your-function-name.supabase.functions.invoke('your-function-name', { body: { ... } }).supabase secrets set are stored encrypted in Supabase's infrastructure — never in your codebase, never in .env, never in Git. This is the only correct way to store API keys used by edge functions. If you put them in .env and reference them from edge function code, they would appear in your codebase and potentially in your Git history.Calling an Edge Function from React
supabase.functions.invoke automatically includes the user's JWT in the request — so your edge function receives the user's authentication without you having to manually attach it.
The Signature Verification Pattern
The Razorpay verification function shows a critical security pattern you will use with any payment provider: cryptographic signature verification.
Razorpay sends a signature with every payment. This signature is a hash of (order_id + "|" + payment_id) using your secret key. You verify it by computing the same hash server-side and comparing. If they match, the payment data is genuine and was not tampered with.
This function runs on the server. The secret key is loaded from Deno.env.get("RAZORPAY_KEY_SECRET"). It never touches the browser. This is why payment integrations require edge functions.
Edge Function File Structure
Each function is in its own folder with an index.ts. Shared code goes in _shared/ and is imported using relative paths.
Keep functions focused. One function, one responsibility. If a function grows beyond ~100 lines, consider splitting it.
supabase/functions/function-name/index.ts is not optional. Supabase's deploy command looks for index.ts in each subfolder. Naming it anything else and it will not deploy. Shared utilities go in supabase/functions/_shared/ and are imported with relative paths like ../shared/cors.ts.