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!
});
All the usual Supabase functions still work:
const { data } = await client.from("profiles").select("*");
await client.auth.signInWithPassword(credentials)
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;
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;
No webhooks. No syncing with Stripe. Subscriptions are automatically kept up to date for me.
- 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')
}
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)