Logo Relancio Borges
All projects

↘ Case study · 2025

Subscription Platform

Full SaaS subscription platform powered by Fastify, React, PostgreSQL, and Stripe

Project cover image for Subscription Platform

Subtrack is a full-stack subscription platform built with React, Fastify, PostgreSQL, RabbitMQ, and Stripe. It covers the complete recurring-payment flow, from selecting a plan to checkout, webhook processing, asynchronous events, and user feedback.

Customer data screen Customer information is collected before starting checkout.

Stripe checkout Secure subscription checkout hosted by Stripe.

Confirmation screen Confirmation after a successful subscription.

Why I Built This Project

Recurring billing appears simple from the user’s perspective, but it involves payment lifecycle management, signed webhooks, asynchronous processing, and careful error handling. I built Subtrack to understand:

  • Recurring billing and automated payment cycles.
  • Webhooks for real-time payment events.
  • Event-driven architecture with queues for decoupling.
  • Stripe integration, one of the most widely used online payment platforms.
  • Clean Architecture and clear back-end responsibilities.

Project Architecture

The React front end loads plans and submits the customer email and selected plan to the Fastify API. The API creates a Stripe Checkout Session and redirects the customer to Stripe. Signed webhook events are validated, stored, and published to RabbitMQ. A worker then processes subscription and payment events asynchronously.

Data Flow

  1. The user selects a subscription plan.
  2. The front end sends email and plan_id to the API.
  3. The API creates a Stripe Checkout Session.
  4. The user completes payment on Stripe.
  5. Stripe sends a signed webhook to the API.
  6. The webhook handler validates and publishes the event to RabbitMQ.
  7. A worker processes the event asynchronously.
  8. The subscription state is updated in PostgreSQL.

Architectural Decisions

AreaDecisionReason
HTTP APIFastify 5High performance and native TypeScript support
DatabasePostgreSQL + DrizzleRelational integrity with a lightweight ORM
PaymentsStripe CheckoutSecure, PCI-compliant hosted checkout
MessagingRabbitMQDecoupled and reliable event processing
ValidationZodRuntime validation with TypeScript inference

Technologies and Tools

Core Stack

  • Node.js 22, TypeScript 5, and Fastify 5.

Front end

  • React 19, Vite 7, Tailwind CSS 4, React Router 7, Axios, and Lucide React.

Back end

  • Fastify, Drizzle ORM, PostgreSQL, RabbitMQ, Stripe SDK, and Zod.

Infrastructure

  • Docker, PostgreSQL 16, and RabbitMQ 3.

Features

  • Dynamic subscription plan catalog.
  • Complete Stripe Checkout flow.
  • Signed Stripe webhook handling.
  • Subscription lifecycle management.
  • Asynchronous RabbitMQ processing.
  • Success and cancellation feedback pages.

Event System

  • subscription_created – A new subscription was created.
  • payment_success – A payment completed successfully.
  • payment_failed – A payment could not be completed.

Technical Challenges

1. Stripe Checkout Integration

Problem: Build a secure, PCI-compliant checkout flow.

Solution: Stripe Checkout Sessions manages the payment UI, card tokenization, and validation so sensitive card data never reaches the application server.

const session = await stripe.checkout.sessions.create({
  line_items: [{ price: plan.stripe_price_id!, quantity: 1 }],
  mode: "subscription",
  payment_method_types: ["card"],
  customer_email: customerEmail,
  success_url: successUrl,
  cancel_url: cancelUrl,
});

2. Webhook Processing

Problem: Verify that events actually came from Stripe and process them reliably.

Solution: Validate Stripe’s signature before parsing the event, then publish accepted events to RabbitMQ.

const event = stripe.webhooks.constructEvent(
  req.body as string,
  signature,
  env.STRIPE_WEBHOOK_SECRET,
);

switch (event.type) {
  case "checkout.session.completed":
    await publishMessage({
      queue: "events",
      message: { type: "subscription_created", data: dbEvent },
    });
}

3. Event-Driven Processing

Problem: Process events asynchronously without coupling every handler to payment infrastructure.

Solution: A worker consumes messages and delegates each event type to an isolated handler.

private async processMessage(message: EventMessage) {
  switch (message.type) {
    case "subscription_created":
      await this.handleSubscriptionCreated(message.data);
      break;
    case "payment_success":
      await this.handlePaymentSuccess(message.data);
      break;
    case "payment_failed":
      await this.handlePaymentFailed(message.data);
      break;
  }
}

4. Data Validation with Zod

Zod is integrated through fastify-type-provider-zod, providing automatic request validation and inferred TypeScript types.

What I Learned

Technical Skills

  • Fastify plugins, hooks, schema validation, and rate limiting.
  • Drizzle queries, migrations, and schema design.
  • React 19 and Tailwind CSS 4.
  • RabbitMQ queues, exchanges, and consumers.
  • Stripe Checkout, webhooks, and subscriptions.
  • Docker Compose and Clean Architecture.

Engineering Skills

  • Designing complete payment flows.
  • Building event-driven distributed systems.
  • Documenting APIs with Swagger and OpenAPI.
  • Implementing robust validation and error handling.

API Endpoints

# Plans
GET /api/plans

# Subscriptions
POST /api/subscriptions

# Stripe webhooks
POST /api/webhooks

# Processed events
GET /api/events

Available Scripts

# Back end
cd back
npm run dev
npm run dev:worker
npm run build
npm run start
npm run seed:stripe
npx drizzle-kit push
npx drizzle-kit generate

# Front end
cd front
npm run dev
npm run build
npm run preview
npm run lint

# Infrastructure
cd back
docker-compose up -d
docker-compose logs -f

Why This Project Matters

Subtrack demonstrates my ability to integrate a React interface with a clean Fastify back end, implement real payment infrastructure, process distributed events with RabbitMQ, validate signed webhooks, and maintain clear, testable boundaries throughout a full-stack system.