Stripe Subscription Integration in Node.js [2024 Ultimate Guide]
Ivan Ivanov
Posted on November 21, 2024
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!
Posted on November 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.