Rails E-commerce Backend with Stripe Payments
Build the backend API of an online shop in Rails with real payments through Stripe, using its test mode, so no real money moves. Customers browse products, fill a cart and check out; the server calculates the total itself (never trusting prices sent by the browser), creates a Stripe PaymentIntent, and the order is only marked paid when Stripe confirms it through a webhook, verified by its signature. Stock is reserved safely, so two customers can't buy the last item, and orders can be refunded. You'll learn designing a Rails API, ActiveRecord associations and validations, integrating a payment provider correctly, secure and idempotent webhook handling, and testing payment flows without calling Stripe.
- Estimate
- ~13.5h
- Steps
- 6
- Completed by
- 0
- Proposed by
- codeseed.app
Rails · Stripe · ActiveRecord
Project roadmap
- 01
Model products, carts and orders
~2hCreate a Rails API-only app with PostgreSQL. Generate Product (name, price in cents, stock), Cart and CartItem, and Order and OrderItem (with the price copied at checkout) plus an order status (pending, paid, failed, refunded) and the Stripe payment intent id. Add associations, validations (prices and quantities above 0) and a unique index on the payment intent id. Seed some products. Done when: the models and seed data work in the Rails console.
- 02
Build the cart and checkout
~2.5hAdd endpoints to view the cart, add and remove items, and change quantities, identifying carts by a token. Add POST /checkout, which creates an order from the cart in a transaction: it locks the products (with lock or SELECT FOR UPDATE), checks and reserves stock, and calculates the total on the server. Return clear errors for empty carts and missing stock. Done when: checkout creates a pending order with the right total, and the last item can't be sold twice.
- 03
Create Stripe PaymentIntents
~2hAdd the stripe gem with test mode keys from credentials. At checkout, create a PaymentIntent for the order total and currency with the order id in its metadata, using an idempotency key so retries don't create duplicates, and return its client secret for the frontend. Store the intent id on the order. Complete a test payment with a Stripe test card (for example with Stripe's sample frontend or the Stripe CLI). Done when: a test payment appears in the Stripe dashboard linked to the order.
- 04
Handle webhooks securely
~2.5hAdd POST /webhooks/stripe that verifies the signature with Stripe::Webhook.construct_event using the raw body and the webhook secret, returning 400 when it fails. On payment_intent.succeeded, mark the order paid; on payment_intent.payment_failed, mark it failed and release the stock. Record processed event ids so repeated deliveries do nothing. Test locally with stripe listen and stripe trigger. Done when: orders update from webhooks, and replayed events change nothing.
- 05
Add order history and refunds
~2hAdd GET /orders and GET /orders/{id} for a customer's orders, and an admin POST /orders/{id}/refund that creates a Stripe refund for a paid order, marks it refunded when the charge.refunded webhook arrives, and returns the items to stock. Refuse refunds for unpaid orders. Done when: a refund appears in Stripe and the order and stock update.
- 06
Test the payment flows
~2.5hWrite request specs (RSpec or Minitest) with Stripe calls stubbed (for example with WebMock or stripe-ruby-mock) and signed test webhook payloads: checkout totals, stock races (two checkouts for the last item), successful and failed payments, invalid signatures, duplicate webhooks and refunds. Done when: all tests pass without calling Stripe.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack