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 information is collected before starting checkout.
Secure subscription checkout hosted by Stripe.
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
- The user selects a subscription plan.
- The front end sends
emailandplan_idto the API. - The API creates a Stripe Checkout Session.
- The user completes payment on Stripe.
- Stripe sends a signed webhook to the API.
- The webhook handler validates and publishes the event to RabbitMQ.
- A worker processes the event asynchronously.
- The subscription state is updated in PostgreSQL.
Architectural Decisions
| Area | Decision | Reason |
|---|---|---|
| HTTP API | Fastify 5 | High performance and native TypeScript support |
| Database | PostgreSQL + Drizzle | Relational integrity with a lightweight ORM |
| Payments | Stripe Checkout | Secure, PCI-compliant hosted checkout |
| Messaging | RabbitMQ | Decoupled and reliable event processing |
| Validation | Zod | Runtime 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.