/how-to-build-replit

How to Build a Membership site with Replit

Learn how to build a membership site using Replit. Follow simple steps to create, manage, and grow your online community effectively.

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 Membership site with Replit

To build a membership site on Replit, start with a Node.js Express app for the server, use SQLite (or Replit’s built-in Database for simple cases) for storing user data, and enable authentication with hashed passwords using bcrypt. Set up routes to handle sign-up, login, logout, and protected content. Use Express sessions (stored in memory or with a store like SQLite) to keep users logged in. You’ll manage secrets (like session keys) using Replit’s “Secrets” panel. Deploy directly from Replit once it works. Everything runs inside one Repl, so keep your code organized: backend in server.js, static assets in a public folder, and views (HTML forms and pages) in a views folder. This approach gives a small, secure, and maintainable membership app that’s realistic for Replit’s environment.

 

Step 1: Setup project structure

 

Create a new Node.js Repl. In your main directory (where index.js usually is), rename it to server.js. Add:

  • server.js – your backend logic
  • views/ – HTML templates (for signup, login, dashboard)
  • public/ – static assets like CSS or images
  • database.sqlite – will be created automatically by the code

 

npm install express express-session sqlite3 bcrypt ejs

 

Step 2: Create the server (server.js)

 

This file is the heart of your app. It sets up your routes and database.

 

// server.js
const express = require('express');
const session = require('express-session');
const sqlite3 = require('sqlite3').verbose();
const bcrypt = require('bcrypt');
const path = require('path');

const app = express();
const db = new sqlite3.Database('database.sqlite');

// Use EJS for simple templating
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Middleware setup
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use(
  session({
    secret: process.env.SESSION_SECRET, // Store in Replit secrets
    resave: false,
    saveUninitialized: false
  })
);

// Create table if not exists
db.run(`CREATE TABLE IF NOT EXISTS users(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT UNIQUE,
  password TEXT
)`);

// Route: Home
app.get('/', (req, res) => {
  if (req.session.userId) {
    res.redirect('/dashboard');
  } else {
    res.render('login');
  }
});

// Route: Sign up page
app.get('/signup', (req, res) => {
  res.render('signup');
});

// Route: Handle sign up
app.post('/signup', async (req, res) => {
  const { username, password } = req.body;
  const hashed = await bcrypt.hash(password, 10);
  db.run(`INSERT INTO users(username, password) VALUES(?, ?)`, [username, hashed], err => {
    if (err) {
      return res.send('User already exists or error occurred.');
    }
    res.redirect('/');
  });
});

// Route: Handle login
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  db.get(`SELECT * FROM users WHERE username = ?`, [username], async (err, user) => {
    if (!user) return res.send('Invalid username or password');
    const match = await bcrypt.compare(password, user.password);
    if (match) {
      req.session.userId = user.id;
      res.redirect('/dashboard');
    } else {
      res.send('Invalid username or password');
    }
  });
});

// Route: Dashboard (protected)
app.get('/dashboard', (req, res) => {
  if (!req.session.userId) return res.redirect('/');
  res.render('dashboard', { username: req.session.username });
});

// Route: Logout
app.get('/logout', (req, res) => {
  req.session.destroy(() => {
    res.redirect('/');
  });
});

app.listen(3000, () => console.log('Server running on port 3000'));

 

Step 3: Create your views

 

Inside your views folder, create three EJS files: login.ejs, signup.ejs, and dashboard.ejs.

 

<!-- views/login.ejs -->
<!DOCTYPE html>
<html>
<head><title>Login</title></head>
<body>
<h2>Login</h2>
<form action="/login" method="POST">
  <input name="username" placeholder="Username" required>
  <input name="password" type="password" placeholder="Password" required>
  <button type="submit">Login</button>
</form>
<a href="/signup">Sign up</a>
</body>
</html>

 

<!-- views/signup.ejs -->
<!DOCTYPE html>
<html>
<head><title>Sign Up</title></head>
<body>
<h2>Create Account</h2>
<form action="/signup" method="POST">
  <input name="username" placeholder="Username" required>
  <input name="password" type="password" placeholder="Password" required>
  <button type="submit">Sign Up</button>
</form>
<a href="/">Back to Login</a>
</body>
</html>

 

<!-- views/dashboard.ejs -->
<!DOCTYPE html>
<html>
<head><title>Member Dashboard</title></head>
<body>
<h2>Welcome to the members area!</h2>
<a href="/logout">Logout</a>
</body>
</html>

 

Step 4: Manage Secrets

 

In Replit, go to the Secrets (lock icon) on the left sidebar, and add a new secret:

  • Key: SESSION\_SECRET
  • Value: any random long string (for example from a password generator)

Replit automatically makes this available as process.env.SESSION\_SECRET in your code.

 

Step 5: Test and deploy

 

Click “Run”. Replit will open a web view of your app. Try signing up and logging in. The first time you run it, notice that database.sqlite appears in your file tree; that’s where user info is stored. Once it’s working, click the “Deploy” button in the top-right to host it. In Replit’s free tier, the Repl will sleep when inactive — that’s fine for learning and demos, but use Replit Deployments or an external host if you need it online reliably 24/7.

 

Practical Notes

 

  • Replit’s SQLite database persists across runs, but avoid large datasets – it’s not meant for high traffic.
  • Never commit your secrets; use Replit Secrets instead.
  • For teams, Replit’s “Collaboration” lets others edit code in real-time, but only share with people you trust as all can access the Secrets while running.
  • Use console.log() for debugging – Replit’s console works well for that.

 

This pattern — Express + SQLite + Sessions — is small, solid, and fully works inside Replit’s environment for real, practical membership sites.

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 Build a Simple Authentication Server for Your Membership Site on Replit


<!-- server.js -->
<script type="module">
import express from "express";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import Database from "@replit/database";

const app = express();
const db = new Database();
app.use(express.json());

const SECRET = process.env.JWT\_SECRET || "dev-secret";

app.post("/signup", async (req, res) => {
  const { username, password } = req.body;
  const existingUser = await db.get(`user_${username}`);
  if (existingUser) return res.status(400).json({ error: "User exists" });
  const hash = await bcrypt.hash(password, 10);
  await db.set(`user_${username}`, { hash, createdAt: Date.now() });
  res.status(201).json({ message: "User created" });
});

app.post("/login", async (req, res) => {
  const { username, password } = req.body;
  const user = await db.get(`user_${username}`);
  if (!user) return res.status(400).json({ error: "Invalid credentials" });
  const match = await bcrypt.compare(password, user.hash);
  if (!match) return res.status(400).json({ error: "Invalid credentials" });
  const token = jwt.sign({ username }, SECRET, { expiresIn: "1h" });
  res.json({ token });
});

app.get("/members", async (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) return res.status(403).json({ error: "No token" });
  try {
    const token = authHeader.split(" ")[1];
    const data = jwt.verify(token, SECRET);
    const memberData = await db.get(`user_${data.username}`);
    res.json({ username: data.username, joined: memberData.createdAt });
  } catch {
    res.status(403).json({ error: "Invalid token" });
  }
});

app.listen(3000, () => console.log("Server running on port 3000"));
</script>

How to Handle Stripe Webhooks for Membership Verification


<!-- webhooks.js -->
<script type="module">
import express from "express";
import crypto from "crypto";
import Database from "@replit/database";

const app = express();
const db = new Database();
app.use(express.json());

const STRIPE_SECRET = process.env.STRIPE_SECRET;
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK\_SECRET;

app.post("/stripe/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const sig = req.headers["stripe-signature"];
  let event;

  try {
    const hmac = crypto.createHmac("sha256", WEBHOOK\_SECRET);
    const digest = hmac.update(req.body).digest("hex");
    if (digest !== sig) throw new Error("Invalid signature");

    event = JSON.parse(req.body);
  } catch (err) {
    console.error("Webhook error:", err.message);
    return res.status(400).send(`Webhook Error`);
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    const username = session.metadata.username;
    await db.set(`premium_${username}`, {
      active: true,
      startedAt: Date.now(),
      sessionId: session.id
    });
  }

  res.status(200).json({ received: true });
});

app.get("/verify-membership/:username", async (req, res) => {
  const user = await db.get(`premium_${req.params.username}`);
  res.json({ active: !!user?.active });
});

app.listen(3001, () => console.log("Webhook server running"));
</script>

How to Verify User Membership and Protect Premium Routes in Replit


<!-- verifySession.js -->
<script type="module">
import express from "express";
import jwt from "jsonwebtoken";
import Database from "@replit/database";

const app = express();
const db = new Database();
app.use(express.json());

const SECRET = process.env.JWT\_SECRET || "dev-secret";

// Middleware to verify token and membership status
async function verifyMembership(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader) return res.status(403).json({ error: "Missing token" });
  try {
    const token = authHeader.split(" ")[1];
    const decoded = jwt.verify(token, SECRET);
    const membership = await db.get(`membership_${decoded.username}`);
    if (!membership || !membership.active) {
      return res.status(403).json({ error: "Inactive membership" });
    }
    req.user = decoded;
    next();
  } catch (err) {
    res.status(403).json({ error: "Invalid or expired token" });
  }
}

// Example protected route for premium content
app.get("/protected-content", verifyMembership, async (req, res) => {
  const data = {
    message: `Welcome back, ${req.user.username}!`,
    tips: ["Exclusive tutorial access", "Community meetups", "Priority support"]
  };
  res.json(data);
});

app.listen(3002, () => console.log("Membership verification server 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 Membership site with Replit

A solid membership site on Replit should use an Express (Node.js) backend for user handling, a database connection through Replit’s built-in Database or an external DB (like MongoDB Atlas), and proper auth and secrets handling via Replit’s Secrets tab. Don’t store API keys or passwords directly in your code. Keep users’ passwords hashed (never plain-text). Structure the app so that your backend handles login/signup, your frontend (can be static HTML or React) fetches data only through API endpoints, and your logic is simple enough to avoid Replit’s storage and runtime limits. Host persistent assets on external storage or GitHub if needed and always test when you fork or reboot the Repl.

 

Project Structure

 

A simple structure that works well for Replit:

  • index.js → main server file (Express app)
  • views/ → HTML templates or front-end React build output
  • public/ → static assets like CSS and images
  • db.js → database connection (if external)
  • utils/hash.js → helper for password hashing

Replit automatically runs the file you specify in the “Run” button (by default usually index.js).

 

Recommended Dependencies

 

npm init -y
npm install express bcryptjs jsonwebtoken dotenv

This installs:

  • express for the web server
  • bcryptjs for password hashing
  • jsonwebtoken for user session tokens
  • dotenv for loading environment secrets from Replit

 

Setting up Secrets in Replit

 

In the left sidebar, click the lock icon → add a secret key like:

  • JWT\_SECRET: something unique (used for token signing)
  • DB\_URL: if using external database

These can be accessed in code via process.env.JWT\_SECRET safely, without exposing them publicly.

 

Backend Setup (index.js)

 

// index.js
import express from "express"
import bcrypt from "bcryptjs"
import jwt from "jsonwebtoken"

const app = express()
app.use(express.json())

// Temporary demo storage: use this only for learning! Replace with a DB in real projects.
let users = []

// Signup route
app.post("/signup", async (req, res) => {
  const { username, password } = req.body
  const existing = users.find(u => u.username === username)
  if (existing) return res.status(400).json({ error: "User already exists" })

  const hashed = await bcrypt.hash(password, 10)
  users.push({ username, password: hashed })
  res.json({ success: true })
})

// Login route
app.post("/login", async (req, res) => {
  const { username, password } = req.body
  const user = users.find(u => u.username === username)
  if (!user) return res.status(400).json({ error: "Invalid credentials" })

  const match = await bcrypt.compare(password, user.password)
  if (!match) return res.status(400).json({ error: "Invalid credentials" })

  const token = jwt.sign({ username }, process.env.JWT_SECRET, { expiresIn: "2h" })
  res.json({ token })
})

// Protected route
app.get("/profile", (req, res) => {
  const auth = req.headers.authorization
  if (!auth) return res.status(401).json({ error: "No token" })

  const token = auth.split(" ")[1]
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    res.json({ message: `Welcome ${decoded.username}!` })
  } catch {
    res.status(401).json({ error: "Invalid token" })
  }
})

// Port for Replit environment
const port = process.env.PORT || 3000
app.listen(port, () => console.log("Server running on port " + port))

Place this file in the root of your Repl (it’s the main entry). When you click “Run,” Replit will start this Express server.

 

Frontend Example (views/signup.html)

 

<!DOCTYPE html>
<html>
  <body>
    <h2>Sign Up</h2>
    <form id="signup-form">
      <input name="username" placeholder="Username" required />
      <input name="password" type="password" placeholder="Password" required />
      <button>Sign Up</button>
    </form>
    <script>
      const form = document.getElementById("signup-form")
      form.addEventListener("submit", async (e) => {
        e.preventDefault()
        const data = { username: form.username.value, password: form.password.value }
        const res = await fetch("/signup", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(data)
        })
        const json = await res.json()
        alert(JSON.stringify(json))
      })
    </script>
  </body>
</html>

If you use static HTML pages like this, keep them under a views/ or public/ folder and let Express serve them by adding in index.js before your routes:

 

app.use(express.static("public"))

 

Database (Optional but Recommended)

 

For production-grade persistence, use MongoDB Atlas (free tier). Create a db.js file in the root:

 

// db.js
import mongoose from "mongoose"

export async function connectDB() {
  await mongoose.connect(process.env.DB_URL)
  console.log("Connected to database")
}

Then in index.js, at the top:

 

import { connectDB } from "./db.js"
connectDB()

 

Replit-Specific Tips

 

  • Avoid using Replit’s built-in Database for serious membership sites — it’s fine for testing, but not reliable for scaling or security.
  • Use Secrets for private information. Never commit credentials or tokens.
  • Enable Always On (if you have Replit Core) so that the membership site stays available.
  • Collaborate safely — sharing your Repl exposes code but not your Secrets; still, handle tokens with care.
  • Deploy by using Replit’s “Deployments” tab if you need a stable public URL; free Repls sleep otherwise.

This setup is enough for a real, working membership base that can evolve into a full project — secure, sharable, and compatible with Replit’s environment.

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