Supabase Edge Functions Invocation Pattern
This skill ensures consistent and secure invocation of Supabase Edge Functions using the official SDK.
When to Apply
Use the Supabase SDK pattern for any code that:
- Calls a Supabase Edge Function
- Sends data to an edge function endpoint
- Receives data from an edge function
Why Use the SDK
The SDK pattern is preferred over raw fetch because:
- Automatic authentication: JWT tokens are automatically attached
- Type safety: Better TypeScript integration
- Error handling: Structured error types for granular handling
- Consistency: Matches patterns used throughout the codebase
- Maintainability: Base URL and auth configured once
Implementation Requirements
1. Import the Supabase Client
For client components:
import { supabaseBrowser } from '@/supabase/utils/client';
For server components/actions:
import { supabaseServer } from '@/supabase/utils/server';
2. Basic Invocation Pattern
const supabase = supabaseBrowser();
const { data, error } = await supabase.functions.invoke('function-name', {
body: { key: 'value' }
});
if (error) {
throw new Error(error.message || 'Function call failed');
}
// Use data
console.log(data);
3. Function Signature
const { data, error } = await supabase.functions.invoke(functionName, options);
Parameters:
functionName(string, required): Name of the edge function to invokeoptions(object, optional): Configuration object
Options Properties:
body: Request payload (auto-serialized as JSON for objects)headers: Custom HTTP headers (Record<string, string>)method: HTTP method ('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE')region: FunctionRegion to specify deployment region
4. Content-Type Handling
The SDK automatically handles Content-Type headers:
Blob,ArrayBuffer,File,FormData,String: Auto-detected- Objects: Serialized as JSON with
application/jsonheader
FormData Example (file uploads):
const formData = new FormData();
formData.append('file', file);
formData.append('organization_id', organizationId.toString());
const { data, error } = await supabase.functions.invoke('parse-lease', {
body: formData
});
JSON Example:
const { data, error } = await supabase.functions.invoke('send-notification', {
body: {
userId: user.id,
message: 'Hello world'
}
});
5. Error Handling
The SDK provides three error types for granular handling:
import {
FunctionsHttpError,
FunctionsRelayError,
FunctionsFetchError
} from '@supabase/supabase-js';
const { data, error } = await supabase.functions.invoke('my-function', {
body: { key: 'value' }
});
if (error) {
if (error instanceof FunctionsHttpError) {
// Function returned an error response (4xx, 5xx)
const errorMessage = await error.context.json();
console.error('Function error:', errorMessage);
} else if (error instanceof FunctionsRelayError) {
// Relay infrastructure issue
console.error('Relay error:', error.message);
} else if (error instanceof FunctionsFetchError) {
// Network/fetch failure
console.error('Fetch error:', error.message);
}
throw error;
}
Simple Error Handling:
if (error) {
throw new Error(error.message || 'Failed to call function');
}
6. Response Parsing
Responses are automatically parsed based on Content-Type:
application/json: Parsed as JSONblob: Returned as Blobform-data: Parsed as FormData- Default: Returned as text
Common Patterns
API Call with Loading State
const [buttonState, setButtonState] = useState<ButtonLoadingState>('default');
const handleAction = async () => {
setButtonState('loading');
const supabase = supabaseBrowser();
try {
const { data, error } = await supabase.functions.invoke('process-data', {
body: { itemId }
});
if (error) {
throw new Error(error.message);
}
setButtonState('success');
return data;
} catch (err) {
setButtonState('error');
Sentry.captureException(err);
throw err;
}
};
File Upload
const handleUpload = async (file: File, organizationId: string) => {
const supabase = supabaseBrowser();
const formData = new FormData();
formData.append('file', file);
formData.append('organization_id', organizationId);
const { data, error } = await supabase.functions.invoke('parse-document', {
body: formData
});
if (error) {
throw new Error(error.message || 'Upload failed');
}
return data;
};
GET Request
const { data, error } = await supabase.functions.invoke('get-status', {
method: 'GET'
});
Custom Headers
const { data, error } = await supabase.functions.invoke('external-api', {
body: { query: 'search term' },
headers: {
'X-Custom-Header': 'value'
}
});
Migration from Raw Fetch
When you see edge function calls using raw fetch:
Before (Anti-pattern):
const response = await fetch(
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/send-notification`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY}`
},
body: JSON.stringify({
userId,
message
})
}
);
if (!response.ok) {
throw new Error('Failed to send notification');
}
const data = await response.json();
After (Correct pattern):
import { supabaseBrowser } from '@/supabase/utils/client';
const supabase = supabaseBrowser();
const { data, error } = await supabase.functions.invoke('send-notification', {
body: {
userId,
message
}
});
if (error) {
throw new Error(error.message || 'Failed to send notification');
}
What to Check
When reviewing code or implementing edge function calls:
- Is the Supabase client imported correctly?
- Is
supabase.functions.invokebeing used instead of rawfetch? - Is the function name passed as the first argument (not a full URL)?
- Is the body being passed in the options object?
- Is error handling in place?
- Are errors being reported to Sentry for production issues?
Common Mistakes to Avoid
Using raw fetch with manual auth:
// Wrong - bypasses SDK benefits
const response = await fetch(
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/my-function`,
{
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY}`
},
body: JSON.stringify(data)
}
);
Forgetting to check for errors:
// Wrong - error not handled
const { data } = await supabase.functions.invoke('my-function', {
body: payload
});
// data could be undefined if there was an error
Including full URL in function name:
// Wrong
await supabase.functions.invoke('/functions/v1/my-function', { body });
// Correct
await supabase.functions.invoke('my-function', { body });
Manually setting Content-Type for JSON:
// Unnecessary - SDK handles this automatically
await supabase.functions.invoke('my-function', {
body: data,
headers: {
'Content-Type': 'application/json' // Not needed for objects
}
});
Implementation Checklist
When implementing or reviewing edge function calls:
- Imported correct Supabase client (browser or server)
- Using
supabase.functions.invokeinstead of raw fetch - Function name is just the name (not full URL path)
- Body is passed in options object
- Error is checked after invocation
- Appropriate error message provided to user
- Sentry error reporting for production debugging
- Loading states managed correctly (if UI involved)
Authoring a Supabase Edge Function
Edge functions live under supabase/functions/<name> and execute on Deno, so the source file boots via Deno.serve. Follow the mcp function pattern:
- Initialize the Supabase client from environment values at module scope:
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { persistSession: false } }); - Respond to
OPTIONSpreflights immediately with a204and shared CORS headers, and reject non-POST methods before any heavy work. - Parse the incoming body inside a try/catch (
await req.json()), return a structured error response (use helpers likerpcError/rpcResultwhen available) when JSON is invalid. - Resolve and validate bearer tokens early (see
resolveToken(req, supabase)), send401/-32600errors when the token is missing/invalid, then hand the sanitized token context to downstream handlers. - Wrap each logical branch in
try/catch, log the error, and return a JSON payload with the right status + CORS headers so Supabase and clients can surface failures cleanly. - Keep shared helpers for CORS headers, RPC serialization, and tool dispatching so each handler stays focused on business logic.
Always prefer the Supabase SDK over manual HTTP calls inside the edge function as well: once the Supabase client is available, call supabase.functions.invoke for any outbound edge function requests rather than fetch, and reuse the same auth/configuration so tokens, regions, and headers stay consistent.
Reference
Supabase Client Locations:
- Browser client:
supabase/utils/client.ts - Server client:
supabase/utils/server.ts
Documentation: