To integrate Supabase with Stripe, create a Supabase Edge Function that receives Stripe webhooks, verifies the webhook signature, and syncs payment events to your database. Store your Stripe secret key and webhook signing secret as Supabase secrets. Create a subscriptions table to track customer plans, and use database triggers or the Edge Function to update user access based on payment status. This approach keeps sensitive payment logic server-side while your frontend reads subscription data from Supabase.
Building a Stripe Payment Integration with Supabase Edge Functions
Stripe handles payments; Supabase handles your data and auth. The bridge between them is a webhook: Stripe sends payment events to your Supabase Edge Function, which updates your database accordingly. This tutorial walks you through the complete setup: creating the database schema for subscriptions, building a webhook Edge Function, verifying Stripe signatures, and reading subscription status from your frontend with proper RLS policies.
Prerequisites
- A Supabase project with Authentication configured
- A Stripe account with API keys (Dashboard > Developers > API keys)
- Supabase CLI installed for Edge Function development
- Basic understanding of webhooks and async event processing
Step-by-step guide
Create the subscriptions table and RLS policies
Create the subscriptions table and RLS policies
Create a table to store customer subscription data synced from Stripe. Link it to auth.users so each subscription belongs to a Supabase user. Enable RLS so users can only read their own subscription data. The Edge Function will use the service role key to write subscription data, bypassing RLS for server-side operations.
1-- Create the subscriptions table2create table public.subscriptions (3 id uuid primary key default gen_random_uuid(),4 user_id uuid references auth.users(id) on delete cascade not null,5 stripe_customer_id text unique,6 stripe_subscription_id text unique,7 plan text not null default 'free',8 status text not null default 'inactive',9 current_period_end timestamptz,10 created_at timestamptz default now(),11 updated_at timestamptz default now()12);1314-- Enable RLS15alter table public.subscriptions enable row level security;1617-- Users can read their own subscription18create policy "Users can view own subscription"19on public.subscriptions for select20to authenticated21using (auth.uid() = user_id);2223-- Only service role can insert/update (Edge Function)24-- No INSERT/UPDATE policies for authenticated role25-- The Edge Function uses SUPABASE_SERVICE_ROLE_KEY which bypasses RLS2627-- Create index for fast lookups28create index idx_subscriptions_user_id on public.subscriptions(user_id);29create index idx_subscriptions_stripe_customer on public.subscriptions(stripe_customer_id);Expected result: The subscriptions table is created with RLS policies that allow users to read but not modify their subscription.
Store Stripe secrets in Supabase
Store Stripe secrets in Supabase
Set your Stripe secret key and webhook signing secret as Supabase secrets. These are available inside Edge Functions as environment variables. Never hardcode these values in your function code. The webhook signing secret is used to verify that incoming webhooks actually came from Stripe.
1# Get your keys from Stripe Dashboard > Developers > API keys2# Webhook signing secret from Stripe Dashboard > Developers > Webhooks > endpoint > Signing secret34# Set secrets in Supabase5supabase secrets set STRIPE_SECRET_KEY=sk_live_your_key_here6supabase secrets set STRIPE_WEBHOOK_SIGNING_SECRET=whsec_your_secret_here78# Verify secrets are set9supabase secrets list1011# For local development, add to supabase/functions/.env:12# STRIPE_SECRET_KEY=sk_test_your_test_key13# STRIPE_WEBHOOK_SIGNING_SECRET=whsec_your_test_secretExpected result: Stripe secrets are stored securely and accessible inside Edge Functions.
Create the Stripe webhook Edge Function
Create the Stripe webhook Edge Function
Create an Edge Function that receives Stripe webhook events, verifies the signature, and processes payment events. The function must be deployed with --no-verify-jwt because Stripe sends unauthenticated HTTP requests. Use the Stripe library to verify the webhook signature, which ensures the request genuinely came from Stripe.
1// supabase/functions/stripe-webhook/index.ts2import Stripe from 'npm:stripe@14'3import { createClient } from 'npm:@supabase/supabase-js@2'45const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {6 apiVersion: '2024-12-18.acacia',7})89const corsHeaders = {10 'Access-Control-Allow-Origin': '*',11 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, stripe-signature',12}1314Deno.serve(async (req) => {15 if (req.method === 'OPTIONS') {16 return new Response('ok', { headers: corsHeaders })17 }1819 try {20 const body = await req.text()21 const signature = req.headers.get('stripe-signature')!22 const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!2324 // Verify the webhook signature25 const event = await stripe.webhooks.constructEventAsync(26 body,27 signature,28 webhookSecret29 )3031 // Create Supabase client with service role (bypasses RLS)32 const supabase = createClient(33 Deno.env.get('SUPABASE_URL')!,34 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!35 )3637 console.log('Processing event:', event.type)3839 // Handle the event40 switch (event.type) {41 case 'checkout.session.completed': {42 const session = event.data.object as Stripe.Checkout.Session43 // Handle in next step44 break45 }46 case 'customer.subscription.updated':47 case 'customer.subscription.deleted': {48 const subscription = event.data.object as Stripe.Subscription49 // Handle in next step50 break51 }52 }5354 return new Response(JSON.stringify({ received: true }), {55 headers: { ...corsHeaders, 'Content-Type': 'application/json' },56 })57 } catch (err) {58 console.error('Webhook error:', err.message)59 return new Response(60 JSON.stringify({ error: err.message }),61 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }62 )63 }64})Expected result: The Edge Function receives and verifies Stripe webhook events.
Handle subscription events and sync to database
Handle subscription events and sync to database
Add event handlers inside the webhook function to create and update subscription records in your database. The checkout.session.completed event fires when a customer completes payment. The customer.subscription.updated and customer.subscription.deleted events fire when subscriptions change status. Use upsert to handle both new and existing subscriptions.
1// Add these handlers inside the switch statement:23case 'checkout.session.completed': {4 const session = event.data.object as Stripe.Checkout.Session5 const userId = session.metadata?.user_id // Set when creating checkout67 if (userId && session.subscription) {8 const subscription = await stripe.subscriptions.retrieve(9 session.subscription as string10 )1112 await supabase.from('subscriptions').upsert({13 user_id: userId,14 stripe_customer_id: session.customer as string,15 stripe_subscription_id: subscription.id,16 plan: subscription.items.data[0]?.price?.lookup_key || 'pro',17 status: subscription.status,18 current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),19 updated_at: new Date().toISOString(),20 }, { onConflict: 'user_id' })2122 console.log('Subscription created for user:', userId)23 }24 break25}2627case 'customer.subscription.updated': {28 const subscription = event.data.object as Stripe.Subscription2930 await supabase31 .from('subscriptions')32 .update({33 status: subscription.status,34 plan: subscription.items.data[0]?.price?.lookup_key || 'pro',35 current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),36 updated_at: new Date().toISOString(),37 })38 .eq('stripe_subscription_id', subscription.id)3940 console.log('Subscription updated:', subscription.id)41 break42}4344case 'customer.subscription.deleted': {45 const subscription = event.data.object as Stripe.Subscription4647 await supabase48 .from('subscriptions')49 .update({50 status: 'canceled',51 plan: 'free',52 updated_at: new Date().toISOString(),53 })54 .eq('stripe_subscription_id', subscription.id)5556 console.log('Subscription canceled:', subscription.id)57 break58}Expected result: Subscription data is automatically synced from Stripe events to your Supabase database.
Deploy the webhook and configure Stripe
Deploy the webhook and configure Stripe
Deploy the Edge Function with --no-verify-jwt (required because Stripe sends unauthenticated requests), then register the endpoint URL in the Stripe Dashboard as a webhook endpoint. Select the events you want to receive.
1# Deploy the Edge Function (no JWT verification for webhooks)2supabase functions deploy stripe-webhook --no-verify-jwt34# The function URL will be:5# https://your-project-ref.supabase.co/functions/v1/stripe-webhook67# Configure in Stripe Dashboard:8# 1. Go to Stripe Dashboard > Developers > Webhooks9# 2. Click 'Add endpoint'10# 3. URL: https://your-project-ref.supabase.co/functions/v1/stripe-webhook11# 4. Select events:12# - checkout.session.completed13# - customer.subscription.updated14# - customer.subscription.deleted15# - invoice.payment_succeeded16# - invoice.payment_failed17# 5. Copy the Signing secret (whsec_...)18# 6. Update your Supabase secret if needed:19# supabase secrets set STRIPE_WEBHOOK_SIGNING_SECRET=whsec_new_secretExpected result: The webhook endpoint is deployed and registered in Stripe, receiving payment events.
Read subscription status from the frontend
Read subscription status from the frontend
On the frontend, query the subscriptions table to check the user's current plan and subscription status. RLS ensures each user can only see their own subscription. Use this data to conditionally render premium features, show upgrade prompts, or restrict access to paid content.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 'https://your-project.supabase.co',5 'your-anon-key'6)78// Check current user's subscription9async function getSubscription() {10 const { data: { user } } = await supabase.auth.getUser()11 if (!user) return null1213 const { data, error } = await supabase14 .from('subscriptions')15 .select('plan, status, current_period_end')16 .eq('user_id', user.id)17 .single()1819 if (error || !data) {20 return { plan: 'free', status: 'inactive', current_period_end: null }21 }2223 return data24}2526// Check if user has active premium27async function isPremium(): Promise<boolean> {28 const sub = await getSubscription()29 return sub?.plan !== 'free' && sub?.status === 'active'30}3132// Usage in React:33// const subscription = await getSubscription()34// if (subscription.plan === 'pro') { show premium features }Expected result: The frontend reads subscription data from Supabase to control access to premium features.
Complete working example
1// Supabase Edge Function: Stripe Webhook Handler2// Deploy: supabase functions deploy stripe-webhook --no-verify-jwt34import Stripe from 'npm:stripe@14'5import { createClient } from 'npm:@supabase/supabase-js@2'67const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {8 apiVersion: '2024-12-18.acacia',9})1011const corsHeaders = {12 'Access-Control-Allow-Origin': '*',13 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, stripe-signature',14}1516Deno.serve(async (req) => {17 if (req.method === 'OPTIONS') {18 return new Response('ok', { headers: corsHeaders })19 }2021 try {22 // Verify webhook signature23 const body = await req.text()24 const signature = req.headers.get('stripe-signature')!25 const event = await stripe.webhooks.constructEventAsync(26 body,27 signature,28 Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!29 )3031 // Service role client bypasses RLS for server-side writes32 const supabase = createClient(33 Deno.env.get('SUPABASE_URL')!,34 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!35 )3637 switch (event.type) {38 case 'checkout.session.completed': {39 const session = event.data.object as Stripe.Checkout.Session40 const userId = session.metadata?.user_id41 if (userId && session.subscription) {42 const sub = await stripe.subscriptions.retrieve(session.subscription as string)43 await supabase.from('subscriptions').upsert({44 user_id: userId,45 stripe_customer_id: session.customer as string,46 stripe_subscription_id: sub.id,47 plan: sub.items.data[0]?.price?.lookup_key || 'pro',48 status: sub.status,49 current_period_end: new Date(sub.current_period_end * 1000).toISOString(),50 updated_at: new Date().toISOString(),51 }, { onConflict: 'user_id' })52 }53 break54 }55 case 'customer.subscription.updated': {56 const sub = event.data.object as Stripe.Subscription57 await supabase.from('subscriptions').update({58 status: sub.status,59 plan: sub.items.data[0]?.price?.lookup_key || 'pro',60 current_period_end: new Date(sub.current_period_end * 1000).toISOString(),61 updated_at: new Date().toISOString(),62 }).eq('stripe_subscription_id', sub.id)63 break64 }65 case 'customer.subscription.deleted': {66 const sub = event.data.object as Stripe.Subscription67 await supabase.from('subscriptions').update({68 status: 'canceled',69 plan: 'free',70 updated_at: new Date().toISOString(),71 }).eq('stripe_subscription_id', sub.id)72 break73 }74 }7576 return new Response(JSON.stringify({ received: true }), {77 headers: { ...corsHeaders, 'Content-Type': 'application/json' },78 })79 } catch (err) {80 return new Response(81 JSON.stringify({ error: (err as Error).message }),82 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }83 )84 }85})Common mistakes when integrating Supabase with Stripe
Why it's a problem: Not deploying the webhook function with --no-verify-jwt, causing Stripe webhooks to be rejected with 401
How to avoid: Deploy with: supabase functions deploy stripe-webhook --no-verify-jwt. Stripe sends unauthenticated HTTP requests, so JWT verification must be disabled for webhook endpoints.
Why it's a problem: Creating INSERT/UPDATE RLS policies on the subscriptions table for the authenticated role, allowing users to fake premium status
How to avoid: Only the Edge Function (using SUPABASE_SERVICE_ROLE_KEY) should write to the subscriptions table. Create only a SELECT policy for the authenticated role.
Why it's a problem: Not verifying the Stripe webhook signature, making the endpoint vulnerable to forged requests
How to avoid: Always verify the signature using stripe.webhooks.constructEventAsync() with your webhook signing secret. This ensures the event actually came from Stripe.
Why it's a problem: Forgetting to pass user_id as metadata when creating Stripe Checkout Sessions
How to avoid: Include metadata: { user_id: supabaseUserId } when creating Checkout Sessions. Without this, you cannot link the Stripe customer to your Supabase user.
Best practices
- Always verify Stripe webhook signatures to prevent forged events from modifying your database
- Use the service role key in the Edge Function to bypass RLS for server-side subscription writes
- Create only SELECT policies for the authenticated role on the subscriptions table — never allow client-side writes
- Pass user_id as metadata when creating Stripe Checkout Sessions to link Stripe customers to Supabase users
- Handle idempotency — Stripe may send the same webhook event multiple times, so use upsert instead of insert
- Return a 200 response quickly and process heavy logic asynchronously to avoid Stripe timeout retries
- Use Stripe test mode and Stripe CLI for local development before going live
- Log all webhook events with event type and relevant IDs for debugging payment issues
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to integrate Stripe subscriptions with my Supabase project. Show me how to create a Supabase Edge Function that handles Stripe webhooks, verifies signatures, and updates a subscriptions table. Include the SQL for the table with RLS policies, the Deno Edge Function code, and how to create a Stripe Checkout Session from my Next.js frontend.
Build a complete Stripe integration for my Supabase project: create a subscriptions table with RLS, write an Edge Function webhook handler that syncs checkout.session.completed and subscription lifecycle events, and show me how to check subscription status from the frontend to gate premium features.
Frequently asked questions
Why do I need an Edge Function for Stripe instead of handling payments on the frontend?
Stripe webhook verification requires your secret key, which must never be exposed in browser code. The Edge Function receives webhooks server-side, verifies signatures, and writes to your database using the service role key — all securely on the server.
How do I test Stripe webhooks locally?
Use the Stripe CLI: stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook. This forwards test webhook events to your local Edge Function. Run supabase functions serve in another terminal.
Can I use Stripe Checkout with Supabase?
Yes. Create a Stripe Checkout Session from an Edge Function, passing the Supabase user_id as metadata. Redirect the user to the Checkout URL. When payment completes, Stripe sends a checkout.session.completed webhook to your Edge Function.
What if a webhook event is delivered multiple times?
Stripe may retry failed webhooks up to 3 days. Use upsert with onConflict to handle duplicate events safely. Check event.type and subscription status before making changes to avoid incorrect state transitions.
How do I handle failed payments?
Listen for the invoice.payment_failed event in your webhook. Update the subscription status to 'past_due' in your database. Show a banner to the user asking them to update their payment method. Stripe sends customer.subscription.deleted if payment is not resolved.
Can I restrict database access based on subscription status?
Yes. Write RLS policies that check the subscriptions table. For example: using (exists (select 1 from subscriptions where user_id = auth.uid() and plan = 'pro' and status = 'active')). This restricts table access to premium subscribers.
Can RapidDev help build a Stripe integration for my Supabase project?
Yes. RapidDev can build complete payment integrations including Stripe Checkout flows, webhook handlers, subscription management, and access control using Supabase Edge Functions and RLS.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation