Getting Stripe subscriptions working with backend services can be tricky and often leads to what developers call the dreaded “brain split” - managing both Stripe's logic and your own backend data in sync.
At Vratix, we’ve tackled this problem head-on while building our Open Source Stripe Subscriptions API Module. Here's how we approach Stripe subscription billing in Node.js to keep things simple, scalable, and developer-friendly.
Core Principle: Let Stripe Be the Source of Truth
The key is to shift as much of the logic to Stripe while keeping your database minimal. We only store:
- Customer ID
- Subscription ID
- Plan
This way, we avoid:
- Overcomplicated backend logic
- Error-prone webhook implementations for syncing dashboard changes
- Data redundancy
With this approach, you still have a fully functional subscription billing system while relying on Stripe as the single source of truth.
Features of Our Implementation
By the end of this guide, you’ll have a subscription-based app supporting:
- User subscription plans
- Checkout sessions
- Subscription upsells
- Available plan listing
Tech Stack
- PostgreSQL
- Node.js + Express.js
- TypeScript
Step 1: Database Design
We start by designing a clean, minimal database table:
CREATE TABLE user_subscriptions (
"id" SERIAL PRIMARY KEY,
"plan" VARCHAR NOT NULL,
"user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
"customer_id" VARCHAR,
"subscription_id" VARCHAR NOT NULL,
"is_owner" BOOLEAN NOT NULL DEFAULT TRUE,
"created_at" TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (user_id, subscription_id)
);
Key points:
-
user_id
: References your internal user table -
plan
: Tracks the subscription plan -
subscription_id
: The Stripe subscription ID -
is_owner
: Flags the primary subscription holder
Step 2: Controllers
We use a factory function to keep the business logic modular and testable. Here's a snippet from our Stripe Subscription Controller:
async getSubscriptions() {
const stripePrices = await stripe.prices.list({
active: true,
type: "recurring",
expand: ["data.product"],
});
return stripePrices.data.map((price) => {
const product = price.product as Stripe.Product;
return {
plan: price.lookup_key || product.name.toLowerCase().replaceAll(" ", "_"),
name: product.name,
priceId: price.id,
interval: price.recurring!.interval,
price: { currency: price.currency, amount: price.unit_amount },
};
});
}
Key highlights:
-
Custom subscription keys: Derived from the product name or
lookup_key
for clean plan checks (user.plan === 'pro_plan'
). - Stripe-first approach: We fetch subscription data directly from Stripe, avoiding the “brain split.”
Step 3: Streamlined Stripe Checkout
Our createCheckout
function sets up a subscription checkout session:
const checkout = await stripe.checkout.sessions.create({
line_items: [
{
price: priceId,
adjustable_quantity: { enabled: true },
quantity: seats || 1,
},
],
mode: "subscription",
subscription_data: { metadata: { userId } },
success_url: CHECKOUT_SUCCESS_URL,
cancel_url: CHECKOUT_CANCEL_URL,
});
return { url: checkout.url! };
Want to Skip All This?
We’ve packaged everything into a ready-to-go Open Source module. In less than 30 seconds, you can set up:
- Stripe integration
- Authentication
- Database configuration
- Prebuilt routes and SQL queries
Run this:
npx vratix init
Check out our Stripe Subscriptions Module Docs for more details.
The full code is available on our GitHub repo.
See a demo video how to do all of this with a working UI here.
I’d love to hear your thoughts - does this make building subscription APIs easier? Let us know what features you’d like to see next!
Top comments (0)