Common Edge Function Patterns
Pattern 1: External API Integration
Stripe Payment
// supabase/functions/create-payment/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import Stripe from "https://esm.sh/stripe@13.0.0?target=deno";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!, {
apiVersion: "2023-10-16",
});
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, content-type",
};
serve(async (req: Request) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const { priceId, userId } = await req.json();
// Create Supabase client to get user data
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{
global: {
headers: { Authorization: req.headers.get("Authorization")! },
},
}
);
// Get user email
const { data: { user } } = await supabase.auth.getUser();
// Create Stripe checkout session
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
mode: "subscription",
success_url: `${req.headers.get("origin")}/success`,
cancel_url: `${req.headers.get("origin")}/cancel`,
customer_email: user?.email,
metadata: { userId },
});
return new Response(
JSON.stringify({ sessionId: session.id, url: session.url }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});
Pattern 2: Webhook Handler
Stripe Webhook
// supabase/functions/stripe-webhook/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import Stripe from "https://esm.sh/stripe@13.0.0?target=deno";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!, {
apiVersion: "2023-10-16",
});
const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET")!;
serve(async (req: Request) => {
const signature = req.headers.get("stripe-signature")!;
const body = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error("Webhook signature verification failed:", err.message);
return new Response(JSON.stringify({ error: "Invalid signature" }), {
status: 400,
});
}
// Use service role to bypass RLS
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const userId = session.metadata?.userId;
// Update user subscription in database
await supabase
.from("subscriptions")
.upsert({
user_id: userId,
stripe_customer_id: session.customer,
stripe_subscription_id: session.subscription,
status: "active",
});
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await supabase
.from("subscriptions")
.update({ status: "canceled" })
.eq("stripe_subscription_id", subscription.id);
break;
}
}
return new Response(JSON.stringify({ received: true }), { status: 200 });
});
Pattern 3: Send Email
SendGrid Integration
// supabase/functions/send-email/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
const SENDGRID_API_KEY = Deno.env.get("SENDGRID_API_KEY")!;
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, content-type",
};
serve(async (req: Request) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const { to, subject, html, text } = await req.json();
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${SENDGRID_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [{ to: [{ email: to }] }],
from: { email: "noreply@myapp.com", name: "My App" },
subject,
content: [
{ type: "text/plain", value: text },
{ type: "text/html", value: html },
],
}),
});
if (!response.ok) {
const error = await response.text();
return new Response(
JSON.stringify({ error }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
return new Response(
JSON.stringify({ success: true }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
});
Pattern 4: Image Processing
Generate Thumbnail
// supabase/functions/generate-thumbnail/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { Image } from "https://deno.land/x/imagescript@1.2.15/mod.ts";
serve(async (req: Request) => {
const { bucket, path, width = 200, height = 200 } = await req.json();
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
// Download original image
const { data: fileData, error: downloadError } = await supabase.storage
.from(bucket)
.download(path);
if (downloadError) {
return new Response(JSON.stringify({ error: downloadError.message }), {
status: 400,
});
}
// Process image
const arrayBuffer = await fileData.arrayBuffer();
const image = await Image.decode(new Uint8Array(arrayBuffer));
// Resize
image.resize(width, height);
// Encode as JPEG
const thumbnail = await image.encodeJPEG(80);
// Upload thumbnail
const thumbnailPath = path.replace(/\.[^.]+$/, "_thumb.jpg");
const { error: uploadError } = await supabase.storage
.from(bucket)
.upload(thumbnailPath, thumbnail, {
contentType: "image/jpeg",
upsert: true,
});
if (uploadError) {
return new Response(JSON.stringify({ error: uploadError.message }), {
status: 400,
});
}
return new Response(
JSON.stringify({ thumbnailPath }),
{ headers: { "Content-Type": "application/json" } }
);
});
Pattern 5: Service Role Operations
Admin Tasks
// supabase/functions/admin-delete-user/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req: Request) => {
// Verify admin (check from regular client)
const authClient = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{
global: {
headers: { Authorization: req.headers.get("Authorization")! },
},
}
);
const { data: { user } } = await authClient.auth.getUser();
// Check if admin
const { data: profile } = await authClient
.from("profiles")
.select("role")
.eq("id", user?.id)
.single();
if (profile?.role !== "admin") {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 403,
});
}
// Use service role for admin operations
const adminClient = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const { targetUserId } = await req.json();
// Delete user data (bypasses RLS)
await adminClient.from("tasks").delete().eq("user_id", targetUserId);
await adminClient.from("profiles").delete().eq("id", targetUserId);
// Delete auth user
const { error } = await adminClient.auth.admin.deleteUser(targetUserId);
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
});
}
return new Response(JSON.stringify({ success: true }));
});
Pattern 6: Scheduled Function (Cron)
Daily Cleanup
// supabase/functions/daily-cleanup/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req: Request) => {
// Verify this is from cron (check header)
const authHeader = req.headers.get("Authorization");
if (authHeader !== `Bearer ${Deno.env.get("CRON_SECRET")}`) {
return new Response("Unauthorized", { status: 401 });
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
// Delete old records
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const { data, error } = await supabase
.from("audit_logs")
.delete()
.lt("created_at", thirtyDaysAgo.toISOString());
if (error) {
console.error("Cleanup error:", error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
}
console.log(`Deleted ${data?.length || 0} old audit logs`);
return new Response(
JSON.stringify({ deleted: data?.length || 0 }),
{ headers: { "Content-Type": "application/json" } }
);
});
Tổng kết
Common Patterns
| Pattern |
Use Case |
| External API |
Stripe, SendGrid, Twilio |
| Webhook |
Payment, CI/CD callbacks |
| Email |
Notifications, confirmations |
| Image Processing |
Thumbnails, watermarks |
| Admin Operations |
User management, cleanup |
| Scheduled Tasks |
Cron jobs, maintenance |
Best Practices
✅ Use service role only when needed
✅ Verify caller identity
✅ Handle errors properly
✅ Set appropriate CORS headers
✅ Validate webhook signatures
✅ Log important events
Q&A
- External APIs nào cần integrate?
- Có webhooks từ services nào?
- Cần scheduled tasks không?