Supabase + Stripe Integration Security

Share

To secure Supabase + Stripe integration, you need to: (1) handle all Stripe webhooks in Supabase Edge Functions with signature verification, (2) use the service role key only in Edge Functions for subscription updates, (3) link checkout sessions to Supabase user IDs via client_reference_id, (4) enforce RLS on subscription tables, and (5) never trust client-side payment confirmations. This blueprint ensures payment state stays server-authoritative.

Setup Time2-3 hours

TL;DR

Stripe webhooks are your source of truth for payment state-never trust client-side payment confirmations. Use Supabase Edge Functions for webhook handling, verify signatures on every request, and use the service key only in Edge Functions to update subscription status.

Webhook Handler (Edge Function) Supabase Stripe

supabase/functions/stripe-webhook/index.ts
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'

const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)
const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!

Deno.serve(async (req) => {
  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')
    return new Response('Invalid signature', { status: 400 })
  }

  // Use service key for admin operations
  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
      await supabase.from('subscriptions').upsert({
        user_id: session.client_reference_id,
        stripe_customer_id: session.customer,
        status: 'active',
      })
      break
    }
    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription
      await supabase.from('subscriptions').update({
        status: 'canceled',
      }).eq('stripe_customer_id', subscription.customer)
      break
    }
  }

  return new Response(JSON.stringify({ received: true }))
})

Creating Checkout Sessions

supabase/functions/create-checkout/index.ts
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'

const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)

Deno.serve(async (req) => {
  // Verify user auth
  const authHeader = req.headers.get('Authorization')!
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  )

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

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: 'price_xxx', quantity: 1 }],
    success_url: `${req.headers.get('origin')}/success`,
    cancel_url: `${req.headers.get('origin')}/cancel`,
    client_reference_id: user.id,  // Link to Supabase user
  })

  return new Response(JSON.stringify({ url: session.url }))
})

Never trust client payment confirmations. Users can manipulate client-side code. Only update subscription status from verified webhook events.

Security Checklist

Pre-Launch Checklist

Webhook signatures verified

Service key only in Edge Functions

Subscription status from webhooks only

RLS on subscriptions table

Checkout session linked to user

Stripe Webhooks Deep Dive Firebase + Stripe Alternative OAuth Security Patterns

Check Your Payment Integration

Scan for webhook and payment security issues.

Start Free Scan
Security Blueprints

Supabase + Stripe Integration Security