Bỏ qua

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

  1. External APIs nào cần integrate?
  2. Có webhooks từ services nào?
  3. Cần scheduled tasks không?