/how-to-build-replit

How to Build a Donation system with Replit

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.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Build a Donation system with Replit

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.

 

Step 1: Set up your Repl

 

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.

  • Install Express (for your server framework) and Stripe (for handling payments).

 

npm install express stripe

 

Step 2: Configure secrets

 

Open the Secrets panel in Replit (key icon on sidebar) and add:

  • STRIPE_SECRET_KEY: your Stripe secret key (starts with sk_test_...)
  • STRIPE_PUBLIC_KEY: your Stripe publishable key (starts with pk_test_...)

These keys are critical — never hardcode them into your files. Secrets are injected automatically into process.env.

 

Step 3: Write the server

 

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"));

 

Step 4: Add frontend files

 

Create a folder called public at the root of your project. Inside it, add:

  • index.html — the donation page with a simple form.
  • success.html — the thank-you page after a donation.
  • cancel.html — what users see if they cancel payment.

 

<!-- 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>

 

Step 5: Test it in Replit

 

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).

  • Use Stripe test mode first — no real money involved.
  • Your app URL will look like https://your-repl-name.username.repl.co.

 

Step 6: Secure and deploy

 

Replit’s web servers are always running for paid plans (Boosted Repls), but will sleep on free ones. To create a persistent donation site:

  • Upgrade the Repl to a Hacker or Core plan so it stays online.
  • Use Replit’s built-in environment variables for secrets only, never push them to GitHub.

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.

 

Common pitfalls to avoid

 

  • Do not expose secret keys in your client-side script.
  • Don’t rely on local file paths in Replit — always use relative paths or Express static serving.
  • Don’t test with live keys until after deploying to a stable Repl (so keys aren’t leaked in logs).

 

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.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Contact Us

How to Create a Simple Donation API with Express and Replit Database


<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>

How to Create a Secure Stripe Checkout API for Donations on Replit


<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>

How to Handle Secure Donation Webhooks in Replit


<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>

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Contact Us
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Best Practices for Building a Donation system with Replit

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.

 

Project Setup

 

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.

  • Server side (index.js): runs Express server, talks to Stripe API.
  • Frontend side (public/index.html and public/script.js): handles user input, fetches from your backend.
  • Replit Secrets: store payment keys safely (never commit them).

 

npm install express stripe

 

Backend (index.js)

 

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))

 

Frontend (public/index.html)

 

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>

 

Frontend Logic (public/script.js)

 

// 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")
  }
})

 

Secrets in Replit

 

Open the Replit left sidebar → click the 🔒 “Secrets” icon. Add a new secret:

  • Key: STRIPE_SECRET_KEY
  • Value: your real Stripe secret key (starts with sk_live_ or sk_test_)

This keeps your secret key safe and out of your code.

 

Deployment and Pitfalls

 

  • Always keep API keys in Secrets. Never hardcode them.
  • Use HTTPS (Stripe requires secure URLs). Replit’s live preview URL is HTTPS by default, so you’re fine.
  • Don’t run payment logic in the frontend. Always go through Express endpoints.
  • Keep logs clean: use Replit’s “Console” to debug server messages. Avoid logging secret data.
  • For persistent donation records, connect a database (Replit DB, MongoDB, or Supabase). But store only minimal info — never full card data (Stripe handles that safely).

 

What Happens When You Run It

 

When you click Run in Replit:

  • The server starts on a Replit-provided HTTPS URL.
  • Opening that URL loads your public/index.html.
  • Users can input a donation amount. On clicking Donate, Stripe Checkout opens for payment.

This flow is lightweight, safe to use, and respects Replit’s environment model without depending on local setups.

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022