Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in Supabase Edge Functions

Create a Supabase Edge Function at supabase/functions/pay/index.ts. Store your Paystack secret key with supabase secrets set PAYSTACK_SECRET_KEY=sk_live_xxx. In the function, use Deno.env.get to read the key, call the Paystack initialize endpoint with native fetch, and return the authorization URL to your frontend.

Setup: Create the Edge Function

# Create the function
supabase functions new pay

# Store the Paystack secret key
supabase secrets set PAYSTACK_SECRET_KEY=sk_live_xxxxxxxxxxxx

# For test mode
supabase secrets set PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxx

This creates supabase/functions/pay/index.ts.

Initialize Transaction Function

// supabase/functions/pay/index.ts
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";

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

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

  if (req.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }

  try {
    const secretKey = Deno.env.get("PAYSTACK_SECRET_KEY");
    if (!secretKey) {
      return new Response(
        JSON.stringify({ error: "Paystack secret key not configured" }),
        { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    const { email, amount } = await req.json();

    if (!email || !amount) {
      return new Response(
        JSON.stringify({ error: "email and amount are required" }),
        { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    const reference = "edge_" + Date.now() + "_" + crypto.randomUUID().split("-")[0];

    const paystackResponse = await fetch(
      "https://api.paystack.co/transaction/initialize",
      {
        method: "POST",
        headers: {
          Authorization: "Bearer " + secretKey,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          email,
          amount: Math.round(Number(amount) * 100), // Convert to kobo
          reference,
        }),
      }
    );

    const data = await paystackResponse.json();

    if (!data.status) {
      return new Response(
        JSON.stringify({ error: data.message }),
        { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    return new Response(
      JSON.stringify({
        authorization_url: data.data.authorization_url,
        access_code: data.data.access_code,
        reference: data.data.reference,
      }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" } }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
    );
  }
});

Call from Frontend

// Using Supabase JS client
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
)

async function initializePayment(email: string, amount: number) {
  const { data, error } = await supabase.functions.invoke('pay', {
    body: { email, amount }
  })

  if (error) throw error
  return data // { authorization_url, access_code, reference }
}

// Or using plain fetch:
async function initializePaymentWithFetch(email: string, amount: number) {
  var response = await fetch(
    process.env.SUPABASE_URL + '/functions/v1/pay',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + process.env.SUPABASE_ANON_KEY,
      },
      body: JSON.stringify({ email, amount }),
    }
  )
  return response.json()
}

Deploy the Function

# Deploy to production
supabase functions deploy pay

# Test locally first
supabase functions serve pay

# Test locally with curl
curl -X POST http://localhost:54321/functions/v1/pay \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "amount": 5000}'

Learn More

Key Takeaways

  • Supabase Edge Functions run Deno TypeScript. They have access to native fetch, crypto, and Deno.env.
  • Store your Paystack secret key with: supabase secrets set PAYSTACK_SECRET_KEY=sk_live_xxx
  • Read the secret in the function with: Deno.env.get("PAYSTACK_SECRET_KEY")
  • Edge Functions are deployed with: supabase functions deploy pay
  • Call Edge Functions from your frontend via the Supabase JS client or directly via fetch.
  • Add CORS headers to your Edge Function for browser clients (use cors module from JSR or set headers manually).

Frequently Asked Questions

Can I access the Supabase database from a Paystack Edge Function?
Yes. Use the Supabase client inside Edge Functions. Import createClient from @supabase/supabase-js. Use the service role key (SUPABASE_SERVICE_ROLE_KEY) from secrets to bypass RLS for writes after payment confirmation. Do not use the anon key for server-side database operations.
How do I set environment variables for Paystack in Supabase Edge Functions?
Use the Supabase CLI: supabase secrets set PAYSTACK_SECRET_KEY=sk_live_xxx. Read them in the function with Deno.env.get("PAYSTACK_SECRET_KEY"). Never put secrets in your function source code.
What is the Deno equivalent of Node.js process.env?
Deno.env.get("KEY_NAME"). Edge Functions run Deno, not Node.js. Use Deno.env.get() for environment variables. The syntax is slightly different but the concept is the same.
Do Supabase Edge Functions have cold start issues?
Edge Functions run at the edge (Deno Deploy), so cold starts are generally fast (under 50ms). For Paystack initialization, which is not latency-critical, this is acceptable. If cold starts become an issue, keep functions warm with scheduled pings.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon