Experiments >

Webhooks with supabase, svelte-kit and Stripe

Experiment #6521st March, 2021by Joshua Nussbaum

Did some digging into setting up webhooks with stripe.

When Stripe sends the checkout.session.completed event, it passes along the metadata.userId that was attached when the checkout was created. That is the link back to user’s record in supabase. We use it to locate the user and mark their payment_status to paid. We also save the customer_id and subscription_id, just in case we need them in the future.

It’s really cool that we can define this logic once in svelte-kit and deploy it either as a serverless function or as a dedicated backend server without any editing.

Code

// in src/routes/webhooks/stripe.js
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'

const {env} = process

// construct stripe and supabase object
const stripe = new Stripe(env.STRIPE_PRIVATE_KEY)
const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_PRIVATE_KEY)

// handle POST /webhooks/stripe
export async function post(req) {
  let data
  let eventType

  const webhookSecret = env.STRIPE_WEBHOOK_SECRET

  // verify the signature when possible
  if (webhookSecret) {
    let signature = req.headers["stripe-signature"]
    let event

    try {
      // query stripe to get event using signature
      event = stripe.webhooks.constructEvent(
        req.rawBody,
        signature,
        webhookSecret
      )
    } catch (err) {
      console.log(`⚠️  Webhook signature verification failed.`)
      return { status: 400 }
    }

    // extract data from query result
    data = event.data
    eventType = event.type
  } else {
    // fallback to extracting from payload
    data = req.body.data
    eventType = req.body.type
  }

  const { object } = data

  // handle different cases
  switch (eventType) {
    case 'checkout.session.completed':
      // subscription creation was successful, so provision:

      await updateUser('id', object.metadata.userId, {
        customer_id: object.customer,
        subscription_id: object.subscription,
        active: true,
        payment_status: 'paid'
      })
      break
    case 'invoice.paid':
      // recent invoice was paid, keep user active
      await updateUser('customer_id', object.customer_id, {active: true, payment_status: 'paid'})
      break
    case 'invoice.payment_failed':
      // a payment failed, mark the user as having a failed payment
      // that way we can notify them in the UI
      await updateUser('customer_id', object.customer_id, {payment_status: 'failed'})
      break
  }

  return {
    status: 200,
    body: { message: 'ok' }
  }
}

// helper to update use in supabase
function updateUser(keyName, key, updates) {
  return supabase
    .from('users')
    .update(updates)
    .match({[keyName]: key})
}

Demo

animation
view all experiments

Stay tuned in

Learn how to add more experimentation to your workflow