Learn how to build a subscription system with Replit step by step. Create recurring payments and manage subscribers easily for your app or website.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
To build a subscription system on Replit, you’ll use a backend (like Express in Node.js) to handle payment logic, connect it with a payment provider such as Stripe, and store user subscription data in a database (Replit DB or external DB like Supabase). The workflow is simple: the frontend calls your backend when someone clicks “Subscribe”, the backend creates a checkout session using Stripe, and once payment succeeds, Stripe calls your backend webhook to confirm the subscription. This system runs fine inside Replit if you handle secrets properly and understand Replit’s always-on limits.
Select the “Node.js” template. This will give you a index.js starter file. Your main backend code will go here. Replit automatically runs index.js when you click Run.
npm install express stripe body-parser
This installs Express (for HTTP handling), Stripe (for subscription billing), and Body-parser (for request parsing).
In Replit’s left sidebar, click the 🔒 Secrets icon. Add:
Never hardcode these directly in your code; Replit Secrets keep them safe even if your code is public.
Inside index.js, set up a small Express server. This handles routing (URLs) for checkout and webhook events.
import express from "express";
import Stripe from "stripe";
import bodyParser from "body-parser";
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Reads your Replit secret
app.use(bodyParser.json());
// Route for homepage or frontend
app.get("/", (req, res) => {
res.send("Welcome! You can POST to /create-checkout-session to subscribe.");
});
// Route to start checkout session
app.post("/create-checkout-session", async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [
{
price: "price_1234567890", // Replace with your Stripe Price ID
quantity: 1
},
],
success_url: `${req.headers.origin}/success`,
cancel_url: `${req.headers.origin}/cancel`,
});
res.json({ url: session.url });
} catch (err) {
console.error(err);
res.status(500).json({ error: "server_error" });
}
});
Place this code directly in index.js. Replace the price\_1234567890 with an actual price ID from your Stripe dashboard (Products → Prices).
You’ll need a webhook route to confirm successful subscriptions. Stripe will send a POST request when subscription events happen. You’ll store or update this data in your database.
// Raw body parser required for Stripe signature validation
import { buffer } from "micro";
app.post("/webhook", bodyParser.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error("Webhook signature failed.", err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
// Save subscription data (user id, stripeCustomerId, etc.)
console.log("Subscription successful for:", session.customer_email);
}
res.sendStatus(200);
});
You can temporarily test webhooks locally using a tool like stripe listen CLI, but on Replit you can just use your public URL (visible as soon as you click Run). Copy that URL into your Stripe Dashboard Webhooks endpoint (e.g., https://your-repl-name.your-username.repl.co/webhook).
If you just want to track who subscribed, you can use Replit DB. Create a new file db.js containing:
import Database from "@replit/database";
const db = new Database();
export default db;
Then in index.js inside your webhook after a successful payment:
import db from "./db.js";
// inside if (event.type === "checkout.session.completed")
await db.set(session.customer_email, {
stripeCustomerId: session.customer,
status: "active",
});
This will store the subscriber information in Replit DB. You can later check it with:
const data = await db.getAll();
console.log(data);
If you have a frontend (React, HTML, etc.), your “Subscribe” button should call the backend endpoint you made earlier (/create-checkout-session) and redirect users to Stripe checkout.
// Example frontend fetch
async function subscribe() {
const res = await fetch("/create-checkout-session", { method: "POST" });
const data = await res.json();
window.location.href = data.url; // Redirect to Stripe
}
This setup gives you a **fully functional subscription flow** on Replit — from a subscribe button on your frontend, to payment processing in your backend, to webhook handling and data persistence. It’s lightweight, reliable for development, and a solid approach before moving to a more robust hosting environment.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
A reliable subscription system on Replit usually combines a secure backend (Node/Express), a payment provider (like Stripe), and a simple frontend for users to sign up and manage subscriptions. The best practice is not to store payment or subscription logic directly in the client-side code — instead, keep it in your backend, hide all sensitive keys in Replit Secrets, and handle communication between your frontend and Stripe safely through your backend routes.
Let’s imagine you have a regular Node.js + Express setup in Replit (which most full-stack Repls use). You’ll handle subscriptions in these steps:
index.js.stripe.js./create-checkout-session) that your frontend can call to start a subscription checkout.
On the left sidebar, click the “🔒 Secrets (Environment Variables)” icon and add your key pair:
Replit will automatically make this available as process.env.STRIPE_SECRET_KEY in your code. This ensures you never accidentally publish sensitive credentials in a public Repl.
npm install express stripe body-parser
index.js
This is your Replit server entry file. It will serve both your frontend and backend routes.
// index.js
import express from "express"
import bodyParser from "body-parser"
import { createCheckoutSession } from "./stripe.js"
const app = express()
app.use(bodyParser.json())
// Simple health route (optional)
app.get("/", (req, res) => {
res.send("Server is running and ready for subscriptions!")
})
// Route to start subscription checkout
app.post("/create-checkout-session", async (req, res) => {
try {
const session = await createCheckoutSession(req.body.priceId)
res.json({ url: session.url })
} catch (err) {
console.error(err)
res.status(500).json({ error: "Something went wrong" })
}
})
// Replit automatically provides the port
app.listen(3000, () => console.log("Server running on port 3000"))
stripe.js
This file will handle all your Stripe logic separately to keep your main server file cleaner.
// stripe.js
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
// Create a new checkout session for a subscription
export async function createCheckoutSession(priceId) {
// priceId is the Stripe Price object ID for your plan, e.g. "price_12345"
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [
{ price: priceId, quantity: 1 }
],
success_url: "https://your-repl-name.username.repl.co/success",
cancel_url: "https://your-repl-name.username.repl.co/cancel"
})
return session
}
On your frontend (for example, inside src/App.js or script.js), you’ll call this API to start the checkout flow. This part can differ depending on your stack, but the idea is the same.
// Example: src/App.js (React)
async function handleSubscribe() {
const priceId = "price_12345" // Replace with your real Stripe price ID
const res = await fetch("/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId })
})
const data = await res.json()
window.location.href = data.url // redirect to Stripe checkout
}
Webhooks allow Stripe to notify your app about subscription events (like when a payment succeeds or a user cancels). You’ll create a route like /webhook in index.js.
// In index.js (add after other routes)
app.post("/webhook", bodyParser.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["stripe-signature"]
let event
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET)
} catch (err) {
console.error("Webhook signature failed:", err)
return res.sendStatus(400)
}
// Handle successful subscription creation, etc.
if (event.type === "customer.subscription.created") {
console.log("New subscription created:", event.data.object.id)
}
res.json({ received: true })
})
Remember: add STRIPE_WEBHOOK_SECRET to Replit Secrets.
For storing user status after checkout (like “premium” flag), use a simple database such as SQLite (comes with Replit), or connect to an external one like Supabase. Keep your DB connection logic in a separate file db.js and update it from your webhook route when Stripe confirms a subscription.
process.env.REPL_SLUG and process.env.REPL_OWNER.
In short: The winning Replit pattern is — Stripe logic isolated in stripe.js, backend in index.js, keys hidden in Secrets, frontend only triggers API calls, and database or webhook keeps track of subscription state. That’s the clean and real way to build a subscription system on Replit that actually works reliably and safely.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.