DEV Community

kanta13jp1
kanta13jp1

Posted on

Supabase Edge Functions Patterns: JWT Auth, CORS, and Error Handling

Supabase Edge Functions Patterns: JWT Auth, CORS, and Error Handling

Three production-ready patterns for Deno-powered Edge Functions.

1. JWT Auth: Authenticated Users Only

// supabase/functions/secure-action/index.ts
import { createClient } from "npm:@supabase/supabase-js";

Deno.serve(async (req) => {
  const authHeader = req.headers.get("Authorization");
  if (!authHeader) {
    return new Response("Unauthorized", { status: 401 });
  }

  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_ANON_KEY")!,
    { global: { headers: { Authorization: authHeader } } }
  );

  const { data: { user }, error } = await supabase.auth.getUser();
  if (error || !user) {
    return new Response("Unauthorized", { status: 401 });
  }

  const { data } = await supabase
    .from("user_data")
    .select("*")
    .eq("user_id", user.id);

  return new Response(JSON.stringify(data), {
    headers: { "Content-Type": "application/json" },
  });
});
Enter fullscreen mode Exit fullscreen mode

Calling from Flutter (SDK auto-attaches Authorization):

final response = await supabase.functions.invoke('secure-action');
Enter fullscreen mode Exit fullscreen mode

2. CORS: Direct Browser Calls

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type",
};

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

  try {
    const { name } = await req.json();

    return new Response(
      JSON.stringify({ message: `Hello, ${name}!` }),
      {
        headers: { ...corsHeaders, "Content-Type": "application/json" },
      }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      {
        status: 400,
        headers: { ...corsHeaders, "Content-Type": "application/json" },
      }
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

3. Structured Error Handling

type AppError = {
  code: string;
  message: string;
  status: number;
};

function errorResponse(err: AppError): Response {
  return new Response(
    JSON.stringify({ error: err.code, message: err.message }),
    {
      status: err.status,
      headers: { "Content-Type": "application/json" },
    }
  );
}

Deno.serve(async (req) => {
  try {
    const body = await req.json();

    if (!body.user_id) {
      return errorResponse({
        code: "MISSING_PARAM",
        message: "user_id is required",
        status: 400,
      });
    }

    const result = await processAction(body.user_id);
    return new Response(JSON.stringify(result), {
      headers: { "Content-Type": "application/json" },
    });
  } catch (err) {
    console.error("Unexpected error:", err);
    return errorResponse({
      code: "INTERNAL_ERROR",
      message: "An unexpected error occurred",
      status: 500,
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Local Development

supabase start
supabase functions serve secure-action --env-file .env.local

curl -X POST http://localhost:54321/functions/v1/secure-action \
  -H "Authorization: Bearer <local-anon-key>" \
  -H "Content-Type: application/json" \
  -d '{"action": "test"}'
Enter fullscreen mode Exit fullscreen mode

Summary

JWT auth      → Authorization header → supabase.auth.getUser()
CORS          → handle OPTIONS preflight + corsHeaders on all responses
Error handling → typed AppError + centralized try/catch
Local dev      → supabase functions serve with hot reload
Enter fullscreen mode Exit fullscreen mode

Edge Functions cold-start in tens of milliseconds — much faster than Lambda.
The Flutter SDK's functions.invoke() attaches the Authorization header automatically.

Top comments (0)