Edge Functions vs Database Functions
Comparison Overview
Key Differences
┌─────────────────────────────────────────────────────────────┐
│ EDGE FUNCTIONS vs DATABASE FUNCTIONS │
├──────────────────────────┬──────────────────────────────────┤
│ Edge Functions │ Database Functions │
│ (Deno/TS) │ (PL/pgSQL) │
├──────────────────────────┼──────────────────────────────────┤
│ Runs at: Edge network │ Runs in: PostgreSQL │
│ Language: TypeScript │ Language: PL/pgSQL, SQL │
│ HTTP calls: Easy │ HTTP calls: Limited │
│ Latency: + network hop │ Latency: Direct DB access │
│ Timeout: 30 seconds │ Timeout: Configurable │
│ RLS: Manual check │ RLS: Native support │
├──────────────────────────┼──────────────────────────────────┤
│ Best for: │ Best for: │
│ - External APIs │ - Data operations │
│ - Webhooks │ - Triggers │
│ - Complex JS logic │ - RLS helper functions │
│ - File processing │ - Computed columns │
└──────────────────────────┴──────────────────────────────────┘
When to Use Edge Functions
✅ External API Integration
// Edge Function - Calling external API
serve(async (req: Request) => {
// Easy to call any external API
const response = await fetch('https://api.stripe.com/v1/customers', {
method: 'POST',
headers: {
'Authorization': `Bearer ${STRIPE_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'email=user@example.com',
});
return new Response(JSON.stringify(await response.json()));
});
✅ Complex Business Logic
// Edge Function - Complex validation/transformation
serve(async (req: Request) => {
const { order } = await req.json();
// Complex business logic
const discount = calculateDiscount(order);
const tax = calculateTax(order, order.shippingAddress);
const shipping = await getShippingRate(order);
// Multiple external calls
const inventory = await checkInventory(order.items);
const fraud = await checkFraudRisk(order);
// Hard to do in PL/pgSQL
return new Response(JSON.stringify({
discount,
tax,
shipping,
inventory,
fraudScore: fraud.score,
}));
});
✅ Webhook Processing
// Edge Function - Webhook handler
serve(async (req: Request) => {
// Verify signature
const signature = req.headers.get('X-Webhook-Signature');
const body = await req.text();
if (!verifySignature(body, signature)) {
return new Response('Invalid signature', { status: 401 });
}
// Process webhook payload
const event = JSON.parse(body);
await handleWebhookEvent(event);
return new Response('OK');
});
When to Use Database Functions
✅ Data Operations
-- Database Function - Complex query
CREATE FUNCTION get_user_dashboard(user_uuid UUID)
RETURNS TABLE (
total_tasks BIGINT,
completed_tasks BIGINT,
pending_tasks BIGINT,
overdue_tasks BIGINT
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(*),
COUNT(*) FILTER (WHERE status = 'done'),
COUNT(*) FILTER (WHERE status = 'pending'),
COUNT(*) FILTER (WHERE due_date < NOW() AND status != 'done')
FROM tasks
WHERE user_id = user_uuid;
END;
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
✅ Triggers
-- Database Trigger - Auto-actions
CREATE FUNCTION handle_task_completion()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.status = 'done' AND OLD.status != 'done' THEN
NEW.completed_at = NOW();
-- Update project progress
UPDATE projects
SET completed_tasks = completed_tasks + 1
WHERE id = NEW.project_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
✅ RLS Helper Functions
-- Database Function - RLS helper
CREATE FUNCTION is_project_member(p_project_id UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM project_members
WHERE project_id = p_project_id
AND user_id = auth.uid()
);
$$ LANGUAGE sql STABLE SECURITY DEFINER;
-- Used in RLS policy
CREATE POLICY "Project access" ON tasks
USING (is_project_member(project_id));
Hybrid Pattern
Combine Both
┌─────────────────────────────────────────────────────────────┐
│ HYBRID PATTERN │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client │
│ │ │
│ ├──▶ Edge Function (external APIs, validation) │
│ │ │ │
│ │ ▼ │
│ │ Supabase Client │
│ │ │ │
│ │ ├──▶ Database Function (data aggregation) │
│ │ │ │
│ │ └──▶ Direct table access (CRUD) │
│ │ │
│ └──▶ Direct to Supabase (simple operations) │
│ │
└─────────────────────────────────────────────────────────────┘
Example: Order Processing
// Edge Function - Orchestrate order
serve(async (req: Request) => {
const { items, paymentMethodId } = await req.json();
// 1. Call Stripe (external API)
const paymentIntent = await stripe.paymentIntents.create({
amount: calculateTotal(items),
payment_method: paymentMethodId,
confirm: true,
});
if (paymentIntent.status !== 'succeeded') {
return new Response(JSON.stringify({ error: 'Payment failed' }), {
status: 400,
});
}
// 2. Call Database Function (atomic order creation)
const { data: order, error } = await supabase.rpc('create_order', {
items: JSON.stringify(items),
payment_id: paymentIntent.id,
});
// 3. Send confirmation email (external API)
await sendOrderConfirmation(order);
return new Response(JSON.stringify({ order }));
});
-- Database Function - Atomic order creation
CREATE FUNCTION create_order(items JSONB, payment_id TEXT)
RETURNS orders AS $$
DECLARE
new_order orders;
BEGIN
-- Insert order
INSERT INTO orders (user_id, payment_id, status)
VALUES (auth.uid(), payment_id, 'paid')
RETURNING * INTO new_order;
-- Insert order items
INSERT INTO order_items (order_id, product_id, quantity, price)
SELECT
new_order.id,
(item->>'product_id')::UUID,
(item->>'quantity')::INT,
(item->>'price')::NUMERIC
FROM jsonb_array_elements(items) AS item;
-- Update inventory
UPDATE products p
SET stock = stock - (item->>'quantity')::INT
FROM jsonb_array_elements(items) AS item
WHERE p.id = (item->>'product_id')::UUID;
RETURN new_order;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Decision Matrix
Quick Reference
| Scenario |
Use |
| Call external API |
Edge Function |
| Send email/SMS |
Edge Function |
| Handle webhook |
Edge Function |
| Complex JS/TS logic |
Edge Function |
| Aggregate data |
Database Function |
| Trigger on change |
Database Function |
| RLS helper |
Database Function |
| Atomic transactions |
Database Function |
| Computed column |
Database Function |
Latency
┌─────────────────────────────────────────────────────────────┐
│ LATENCY COMPARISON │
├─────────────────────────────────────────────────────────────┤
│ │
│ Direct Query (RPC to DB function): │
│ Client ──▶ Supabase API ──▶ PostgreSQL │
│ ~50-100ms │
│ │
│ Edge Function then DB: │
│ Client ──▶ Edge ──▶ Supabase API ──▶ PostgreSQL │
│ ~100-200ms (extra hop) │
│ │
│ Edge Function for external API: │
│ Client ──▶ Edge ──▶ External API │
│ Faster than: Client ──▶ External API (CORS issues) │
│ │
└─────────────────────────────────────────────────────────────┘
Tổng kết
Use Edge Functions When
✅ Calling external APIs (Stripe, SendGrid, etc.)
✅ Processing webhooks
✅ Complex TypeScript/JavaScript logic
✅ File/image processing
✅ Multi-step orchestration
Use Database Functions When
✅ Data aggregation/transformation
✅ Triggers on data changes
✅ RLS policy helpers
✅ Atomic transactions
✅ Performance-critical data operations
Golden Rule
Data operations → Database Function
Everything else → Edge Function
Q&A
- Có operations nào đang dùng sai chỗ?
- External APIs nào cần integrate?
- Cần triggers cho events nào?