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

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 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.
Create a new Node.js Repl. In your main directory (where index.js usually is), rename it to server.js. Add:
npm install express express-session sqlite3 bcrypt ejs
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'));
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>
In Replit, go to the Secrets (lock icon) on the left sidebar, and add a new secret:
Replit automatically makes this available as process.env.SESSION\_SECRET in your code.
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.
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.
<!-- 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>
<!-- 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>
<!-- 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>

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 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.
A simple structure that works well for Replit:
Replit automatically runs the file you specify in the “Run” button (by default usually index.js).
npm init -y
npm install express bcryptjs jsonwebtoken dotenv
This installs:
In the left sidebar, click the lock icon → add a secret key like:
These can be accessed in code via process.env.JWT\_SECRET safely, without exposing them publicly.
// 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.
<!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"))
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()
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.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.