DEV Community

Cover image for Stripe / Supabase Billing Hell (And What I Did About It)
Emmett Miller
Emmett Miller

Posted on

2

Stripe / Supabase Billing Hell (And What I Did About It)

If you’ve ever tried adding billing to a Supabase project, you probably understand why I’m writing this.

I love Supabase. But when it came time to let users pay for something, everything kind of fell apart.

Stripe: The dream and the reality

Stripe is the go-to solution for payments. Everyone uses it. Tons of libraries. Clean dashboard. But integrating it? That’s where the pain begins.

Here’s what I needed just to get basic billing working:

  • Webhook server to handle Stripe events
  • Logic for trials, grace periods, downgrades
  • UI to show invoices, plans, and billing status
  • Mapping Stripe subscriptions to user permissions
  • Handling canceled payments and past-due states

Stripe might be the most documented API on the planet, but setup was still a slog.

So we built Update

We didn’t set out to build a Stripe wrapper. I just wanted a way to say:

"Only show this page if the user is on a paid plan."

Without it exploding into 17 webhook events and 400 lines of boilerplate.

So we built Update, a drop-in billing system that plays nicely with Supabase. It’s not a replacement. It fills in the parts Supabase doesn’t handle: billing, permissions, and (eventually) multi-tenancy.

This post isn’t a promotion. I just want to show how I wired it up.

How I integrated Update with Supabase

1. Initialized Update alongside Supabase

Update wraps the Supabase client and adds billing + entitlements:

import { createClient } from "@updatedev/js/supabase";

const client = createClient({
  process.env.NEXT_PUBLIC_UPDATE_PUBLIC_KEY!,
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
});
Enter fullscreen mode Exit fullscreen mode

All the usual Supabase functions still work:

const { data } = await client.from("profiles").select("*");
await client.auth.signInWithPassword(credentials)
Enter fullscreen mode Exit fullscreen mode

Update just layers on top.

2. Fetched plans and create checkout sessions

First, I fetched the available products:

const { data: productData, error } = await client.billing.getProducts();

const proPlan = productData.find(p => p.name === "Pro");
const priceId = proPlan?.prices?.[0]?.id;
Enter fullscreen mode Exit fullscreen mode

Then I created a checkout session for the selected plan:

const { data: session } = await client.billing.createCheckoutSession(
  priceId,
  {
    redirect_url: "http://localhost:3000/billing/success",
  }
);

window.location.href = session.url;
Enter fullscreen mode Exit fullscreen mode

No webhooks. No syncing with Stripe. Subscriptions are automatically kept up to date for me.

  1. Checked entitlements in the UI

Once subscribed, I can gate features like this:

const { data } = await client.entitlements.check("pro");

if (!data.hasAccess) {
  // show the premium UI
  redirect('/upgrade')
}
Enter fullscreen mode Exit fullscreen mode

This checks individual or organization level access based on payment status.

The result

I went from tangled webhook logic to a setup where billing just... works. Subscriptions, permissions, and entitlements update in real time. Stripe is still there—but abstracted away.

If you’re in the same boat, you might find Update useful. Or maybe you’ll just write another wrapper, like we did.

Either way, billing shouldn’t be this hard.

Top comments (0)