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_keyfor 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 (1)
Absolutely incredible! 😻.