Learn how to build a secure and efficient donation system using Replit. Follow this step-by-step guide to accept payments and support your project.

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 realistic way to build a donation system on Replit is to create a small web app (for example, using Node.js with Express for the backend) and connect it to a real payment processor like Stripe. Stripe handles secure transactions, while your Replit server receives success/failure notifications. You’ll store your API keys in Replit’s Secrets panel so they aren’t exposed in code. When a donor clicks a “Donate” button, your front-end (HTML or React) sends a request to your Express route, which talks to Stripe to make a payment session. Stripe then redirects the donor to its hosted payment page — safer and compliant. This method works fully inside Replit’s free or paid environments.
Create a new Node.js Repl. Replit will automatically give you a main file called index.js. This will be your server’s entry point.
npm install express stripe
Open the Secrets panel in Replit (key icon on sidebar) and add:
These keys are critical — never hardcode them into your files. Secrets are injected automatically into process.env.
Inside index.js, set up your Express server and a “create checkout session” route that talks to Stripe. You can keep this at the top level of your project (not in a separate folder unless your structure grows).
// index.js
import express from "express";
import Stripe from "stripe";
import path from "path";
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.use(express.static("public")); // serve static HTML files from 'public'
app.use(express.json()); // parse incoming JSON requests
// donation checkout route
app.post("/create-checkout-session", async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
mode: "payment",
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "Donation",
},
unit_amount: req.body.amount * 100, // in cents
},
quantity: 1,
},
],
success_url: `${req.headers.origin}/success.html`,
cancel_url: `${req.headers.origin}/cancel.html`,
});
res.json({ url: session.url }); // send Stripe-hosted checkout URL back
} catch (error) {
console.error(error);
res.status(500).json({ error: "Something went wrong creating session." });
}
});
app.listen(3000, () => console.log("Server running on port 3000"));
Create a folder called public at the root of your project. Inside it, add:
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Donate</title>
</head>
<body>
<h1>Support Our Project 💖</h1>
<input id="amount" type="number" placeholder="Enter amount (USD)" />
<button id="donateBtn">Donate</button>
<script>
document.getElementById("donateBtn").addEventListener("click", async () => {
const amount = document.getElementById("amount").value;
if (!amount || amount <= 0) {
alert("Please enter a valid amount.");
return;
}
const response = await fetch("/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount }),
});
const data = await response.json();
if (data.url) {
window.location = data.url; // redirect to Stripe Checkout
} else {
alert("Error creating checkout session.");
}
});
</script>
</body>
</html>
Click “Run” in Replit. It will start your Express server. The webview should show your donation page. Try entering an amount (ex: 5) and clicking Donate — it should redirect you to a Stripe checkout session using test cards (like 4242 4242 4242 4242).
https://your-repl-name.username.repl.co.
Replit’s web servers are always running for paid plans (Boosted Repls), but will sleep on free ones. To create a persistent donation site:
Stripe is secure and PCI-compliant, so you don’t handle card data directly. That’s exactly what you want for donations — your Replit app never touches sensitive payment info.
Follow these exact steps and you’ll have a working, real-world donation system entirely inside Replit, with Stripe securely handling all transactions and your own code staying simple and maintainable.
<script type="module">
import express from "express";
import bodyParser from "body-parser";
import Database from "@replit/database";
const app = express();
const db = new Database();
app.use(bodyParser.json());
app.post("/api/donate", async (req, res) => {
try {
const { donor, amount, message } = req.body;
if (!donor || !amount) return res.status(400).json({ error: "Missing donor or amount" });
const donations = (await db.get("donations")) || [];
const newDonation = { id: Date.now(), donor, amount: Number(amount), message, time: new Date().toISOString() };
donations.push(newDonation);
await db.set("donations", donations);
// simple total recalculation stored for analytics or leaderboard
const total = donations.reduce((sum, d) => sum + d.amount, 0);
await db.set("total", total);
res.status(201).json({ success: true, donation: newDonation, total });
} catch (err) {
res.status(500).json({ error: "Server Error" });
}
});
app.get("/api/donations", async (req, res) => {
const donations = (await db.get("donations")) || [];
res.json(donations);
});
app.listen(3000, () => {
console.log("Donation API running on port 3000");
});
</script>
<script type="module">
import express from "express";
import fetch from "node-fetch";
import bodyParser from "body-parser";
const app = express();
app.use(bodyParser.json());
// Securely load your secret Stripe key from Replit Secrets
const STRIPE_SECRET = process.env.STRIPE_SECRET;
app.post("/api/create-checkout-session", async (req, res) => {
try {
const { amount, donorEmail } = req.body;
if (!amount || !donorEmail) {
return res.status(400).json({ error: "Missing amount or email" });
}
const response = await fetch("https://api.stripe.com/v1/checkout/sessions", {
method: "POST",
headers: {
"Authorization": `Bearer ${STRIPE_SECRET}`,
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
payment_method_types[]: "card",
mode: "payment",
line_items\[0]\[price_data][currency]: "usd",
line_items\[0]\[price_data]\[product\_data]\[name]: "Donation",
line_items\[0]\[price_data][unit\_amount]: amount \* 100,
line\_items\[0]\[quantity]: 1,
success\_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel\_url: `${req.headers.origin}/cancel`
})
});
const session = await response.json();
if (session.error) {
return res.status(500).json({ error: session.error.message });
}
res.json({ url: session.url });
} catch (err) {
console.error("Checkout session error:", err);
res.status(500).json({ error: "Server error creating Stripe session" });
}
});
app.listen(3000, () => {
console.log("Donation checkout API running on port 3000");
});
</script>
<script type="module">
import express from "express";
import Database from "@replit/database";
import bodyParser from "body-parser";
import crypto from "crypto";
const app = express();
const db = new Database();
app.use(bodyParser.json());
// Mock verification webhook endpoint for donation platform (e.g., Stripe/PayPal)
app.post("/api/webhook/donation", async (req, res) => {
try {
const signature = req.headers["x-donation-signature"];
const rawBody = JSON.stringify(req.body);
// Verify webhook signature before trusting payload
const expectedSig = crypto
.createHmac("sha256", process.env.WEBHOOK\_SECRET)
.update(rawBody)
.digest("hex");
if (signature !== expectedSig) {
return res.status(403).json({ error: "Invalid signature" });
}
const { donorId, amount, transactionId, status } = req.body;
if (status !== "completed") return res.sendStatus(200);
const donations = (await db.get("donations")) || [];
const existing = donations.find((d) => d.transactionId === transactionId);
if (existing) return res.sendStatus(200); // prevent double counting
const donation = {
donorId,
amount,
transactionId,
receivedAt: new Date().toISOString(),
};
donations.push(donation);
await db.set("donations", donations);
const total = donations.reduce((sum, d) => sum + Number(d.amount), 0);
await db.set("total", total);
res.status(201).json({ success: true });
} catch (err) {
console.error("Webhook error:", err);
res.status(500).json({ error: "Server error" });
}
});
app.get("/api/stats", async (req, res) => {
const total = (await db.get("total")) || 0;
const donations = (await db.get("donations")) || [];
res.json({ total, count: donations.length });
});
app.listen(3000, () => console.log("Secure Donation Webhook API running"));
</script>

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 donation system on Replit should be built around a secure backend, environment variables for secrets (to store private keys safely), and a verified payment API like Stripe. The best pattern is to run a Node.js (Express) backend that handles payments securely on the server-side and a small frontend (HTML, CSS, client JS) that sends donation data to your backend. Replit’s built-in web server and Secrets tab make this setup very straightforward but you must avoid processing payments directly in client-side code to stay secure.
Create a new Replit with the Node.js template. You’ll see two main files: index.js (your backend entry point) and index.html inside a public folder if you decide to use one for the frontend. Use Replit Secrets (the 🔒 icon in the left sidebar) to store environment variables like the Stripe secret key.
npm install express stripe
This is your secure server file. You can put this code inside index.js.
// index.js
const express = require("express")
const app = express()
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY) // stored in Replit Secrets
const path = require("path")
app.use(express.static("public")) // serve frontend files
app.use(express.json()) // parse JSON request bodies
// create a donation payment endpoint
app.post("/create-checkout-session", async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "Donation",
},
unit_amount: req.body.amount * 100, // convert to cents
},
quantity: 1,
},
],
mode: "payment",
success_url: `${req.headers.origin}/success.html`,
cancel_url: `${req.headers.origin}/cancel.html`,
})
res.json({ url: session.url })
} catch (err) {
res.status(500).json({ error: err.message })
}
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => console.log("Server running on port " + PORT))
Create a new folder named public in your Replit workspace. Inside, create an index.html file with this donation form. The form sends the donation amount to your backend. Add a script.js for handling payment requests.
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Donate</title>
</head>
<body>
<h1>Support Us</h1>
<input type="number" id="amount" placeholder="Enter amount in USD" />
<button id="donateBtn">Donate</button>
<script src="script.js"></script>
</body>
</html>
// public/script.js
document.getElementById("donateBtn").addEventListener("click", async () => {
const amount = document.getElementById("amount").value
if (!amount || amount <= 0) {
alert("Please enter a valid amount")
return
}
// send amount data to backend
const res = await fetch("/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount }),
})
const data = await res.json()
if (data.url) {
window.location.href = data.url // redirects to Stripe Checkout page
} else {
alert("Unable to create payment session")
}
})
Open the Replit left sidebar → click the 🔒 “Secrets” icon. Add a new secret:
sk_live_ or sk_test_)This keeps your secret key safe and out of your code.
When you click Run in Replit:
This flow is lightweight, safe to use, and respects Replit’s environment model without depending on local setups.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.