Skip to Content
Course content

164: Building a Subscription Billing System with Stripe

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually link my local users to Stripe customers?

This is where most people overthink things. You don't want to mirror the entire Stripe database in your own PostgreSQL or MySQL instance—that's a synchronization nightmare. Instead, just treat Stripe as the source of truth for billing and keep a reference pointer in your users table.

I always add a stripe_customer_id string column to my User model. When a user signs up for a plan, you check if they already have a customer ID. If they don't, you hit the Stripe API, create the customer, and save that ID locally. It looks something like this:

def stripe_customer
  if user.stripe_customer_id.present?
    Stripe::Customer.retrieve(user.stripe_customer_id)
  else
    customer = Stripe::Customer.create(
      email: user.email,
      name: user.full_name
    )
    user.update(stripe_customer_id: customer.id)
    customer
  end
end

Simple, right? Now you have a bridge between your application's identity and Stripe's billing identity without duplicating sensitive data.

Should I build my own credit card forms or use Stripe Checkout?

Unless you have a massive team of security engineers and a lot of free time to deal with PCI compliance, use Stripe Checkout. Seriously. I've seen developers spend weeks building custom payment forms only to realize they're accidentally handling raw card data on their servers, which is a legal and security minefield.

With Checkout, you just redirect the user to a Stripe-hosted page. They handle the UI, the validation, and the security. Once the payment is done, they send the user back to your success_url. Here is how you'd trigger that redirect in a Ruby controller:

session = Stripe::Checkout::Session.create({
  customer: current_user.stripe_customer_id,
  payment_method_types: ['card'],
  line_items: [{
    price: 'price_H5ggY9IzS2', # The ID of your product in the Stripe Dashboard
    quantity: 1,
  }],
  mode: 'subscription',
  success_url: 'https://your-app.com/success',
  cancel_url: 'https://your-app.com/billing',
})

redirect_to session.url, allow_other_host: true

It’s cleaner, it’s safer, and it converts better because users trust the Stripe brand more than a random form on a new site.

How do I handle "payment failed" or "subscription cancelled" without constantly polling the API?

You use webhooks. If you try to check the subscription status every time a user loads a page, you'll hit API rate limits and slow your app to a crawl. Instead, you tell Stripe: "Hey, whenever something happens to a subscription, send an HTTP POST request to this specific URL on my server."

You'll need a dedicated controller endpoint that doesn't require CSRF protection (since Stripe is the one calling it). I usually create a WebhooksController. The trick is to verify the signature so you know the request actually came from Stripe and not some random person trying to grant themselves a free subscription.

def create
  payload = request.body.read
  sig_header = request.env['HTTP_STRIPE_SIGNATURE']
  endpoint_secret = ENV['STRIPE_WEBHOOK_SECRET']

  event = Stripe::Webhook.construct_event(payload, sig_header, endpoint_secret)

  case event.type
  when 'customer.subscription.deleted'
    subscription = event.data.object
    user = User.find_by(stripe_customer_id: subscription.customer)
    user.update(active_subscription: false)
  when 'invoice.payment_failed'
    # This is where you'd trigger an email to the user saying "Your card was declined!"
    NotificationService.payment_failed(event.data.object.customer)
  end

  head :ok
rescue Stripe::SignatureVerificationError
  head :bad_request
end

This turns your app from a "pull" system into a "push" system. Your database stays updated in near real-time, and your users get immediate feedback if their payment fails.




📋 Practical Task

Implement the DevDocs Pro Subscription Webhook

You are building a subscription service called DevDocs Pro. You have already set up the User model with a stripe_customer_id and a boolean premium_member column.

Your task is to create a StripeWebhooksController with a create action that handles the invoice.paid event. When this event is received, the code should:

  1. Extract the customer ID from the event object.
  2. Find the corresponding User in your database.
  3. Update that user's premium_member attribute to true.
  4. Return a 200 OK response to Stripe to acknowledge receipt.

Make sure to include a basic rescue block for Stripe::SignatureVerificationError to ensure the app doesn't crash when receiving malformed requests.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.