Learn how to build a habit tracker with Replit. Follow simple steps to code, track progress, and boost productivity through consistent daily habits.

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 simple and realistic way to build a Habit Tracker on Replit is to use a small Express.js backend (Node.js) to store your habits, a lightweight SQLite database file for persistence, and a minimal HTML + JS frontend to show and update habits. You can set this up directly inside one Repl, deploy it instantly, and even use Replit’s built-in Database or external SQLite depending on your comfort level. This approach works smoothly on Replit’s free tier and mirrors how real full-stack apps are structured.
npm install express better-sqlite3
Express handles your backend routes. better-sqlite3 is a fast, simple SQLite library that works perfectly on Replit.
In index.js (replace all the content with this):
// Load dependencies
const express = require('express');
const Database = require('better-sqlite3');
const path = require('path');
// Create Express app
const app = express();
// Middlewares
app.use(express.json());
app.use(express.static('public')); // serve the /public folder
// Initialize SQLite database file in Replit environment
const db = new Database('habits.db');
// Create table if doesn't exist
db.prepare(`
CREATE TABLE IF NOT EXISTS habits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
count INTEGER
)
`).run();
// Route: get all habits
app.get('/api/habits', (req, res) => {
const rows = db.prepare('SELECT * FROM habits').all();
res.json(rows);
});
// Route: add habit
app.post('/api/habits', (req, res) => {
const { name } = req.body;
db.prepare('INSERT INTO habits (name, count) VALUES (?, ?)').run(name, 0);
res.json({ success: true });
});
// Route: increment habit count
app.post('/api/habits/:id/increment', (req, res) => {
const { id } = req.params;
db.prepare('UPDATE habits SET count = count + 1 WHERE id = ?').run(id);
res.json({ success: true });
});
// Start server
app.listen(3000, () => {
console.log('Habit Tracker running on http://localhost:3000');
});
This script will create a small HTTP server that lets you list, add, and increment habits. On Replit, this automatically starts when you click “Run”. You’ll see a “🎧 Listening” URL in the webview panel.
Now open public/index.html and add this:
<!DOCTYPE html>
<html>
<head>
<title>Habit Tracker</title>
<meta charset="UTF-8" />
</head>
<body style="font-family: sans-serif; max-width: 600px; margin: 2rem auto;">
<h1>Habit Tracker</h1>
<form id="habitForm">
<input type="text" id="habitName" placeholder="New habit..." required />
<button>Add Habit</button>
</form>
<ul id="habitList"></ul>
<script>
const habitForm = document.getElementById('habitForm');
const habitList = document.getElementById('habitList');
// Load habits on page load
async function loadHabits() {
const res = await fetch('/api/habits');
const habits = await res.json();
habitList.innerHTML = '';
habits.forEach(habit => {
const li = document.createElement('li');
li.innerHTML = `${habit.name}: ${habit.count}
<button onclick="increment(${habit.id})">+1</button>`;
habitList.appendChild(li);
});
}
async function increment(id) {
await fetch('/api/habits/' + id + '/increment', { method: 'POST' });
loadHabits();
}
habitForm.addEventListener('submit', async e => {
e.preventDefault();
const name = document.getElementById('habitName').value;
await fetch('/api/habits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
document.getElementById('habitName').value = '';
loadHabits();
});
loadHabits();
</script>
</body>
</html>
This frontend uses simple HTML and JavaScript to interact with the backend API. The page lists your habits, allows adding new ones, and lets you click “+1” to log progress.
Your data will persist because SQLite writes a local file (habits.db) inside your Repl. If you restart the Repl, habits stay saved as long as you don’t delete that file.
This approach is 100% real, works fully in Replit, and stays faithful to how professional full-stack apps are built there — simple, modular, and resilient to Replit’s environment quirks.
import express from "express"
import Database from "@replit/database"
const app = express()
const db = new Database()
app.use(express.json())
// Structure habits by user -> date -> habit-name: status
app.post("/api/habits/update", async (req, res) => {
const { userId, date, habit, done } = req.body
if (!userId || !date || !habit)
return res.status(400).json({ error: "Missing required fields" })
const userHabits = (await db.get(userId)) || {}
const dayData = userHabits[date] || {}
dayData[habit] = done
userHabits[date] = dayData
await db.set(userId, userHabits)
res.json({ success: true })
})
app.get("/api/habits/:userId/:date", async (req, res) => {
const { userId, date } = req.params
const userData = (await db.get(userId)) || {}
res.json(userData[date] || {})
})
app.listen(3000, () => console.log("Habit Tracker API running on port 3000"))
import express from "express"
import fetch from "node-fetch"
import Database from "@replit/database"
const app = express()
const db = new Database()
app.use(express.json())
// Example: Send daily habit completion summary email via Mailgun API
app.post("/api/notify/daily-summary", async (req, res) => {
const { userId, email } = req.body
if (!userId || !email) return res.status(400).json({ error: "Missing userId or email" })
const userData = (await db.get(userId)) || {}
const today = new Date().toISOString().split("T")[0]
const todayHabits = userData[today] || {}
const completed = Object.keys(todayHabits).filter(h => todayHabits[h])
const missed = Object.keys(todayHabits).filter(h => !todayHabits[h])
const text = `Your habits for ${today}:\nCompleted: ${completed.join(", ") || "None"}\nMissed: ${missed.join(", ") || "None"}`
const mgDomain = process.env.MG\_DOMAIN
const mgKey = process.env.MG_API_KEY
const response = await fetch(`https://api.mailgun.net/v3/${mgDomain}/messages`, {
method: "POST",
headers: {
"Authorization": "Basic " + Buffer.from("api:" + mgKey).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
from: `Habit Tracker <no-reply@${mgDomain}>`,
to: email,
subject: "Your Daily Habit Summary",
text
})
})
if (response.ok) return res.json({ success: true })
const errText = await response.text()
res.status(500).json({ error: "Email failed", detail: errText })
})
app.listen(3000, () => console.log("Habit Tracker with Mailgun notifications running on port 3000"))
import express from "express"
import Database from "@replit/database"
import cron from "node-cron"
const app = express()
const db = new Database()
app.use(express.json())
// Automatically reset daily habits at midnight (UTC)
cron.schedule("0 0 _ _ \*", async () => {
const keys = await db.list()
for (const userId of keys) {
const userData = (await db.get(userId)) || {}
const today = new Date().toISOString().split("T")[0]
// Copy yesterday's habits but reset completion to false
const lastDayKey = Object.keys(userData).sort().pop()
const lastDayHabits = lastDayKey ? userData[lastDayKey] : {}
const resetHabits = Object.keys(lastDayHabits).reduce((a, h) => {
a[h] = false
return a
}, {})
userData[today] = resetHabits
await db.set(userId, userData)
}
console.log("Daily habit reset completed.")
})
// Endpoint to trigger manual reset (for debugging)
app.post("/admin/reset-now", async (req, res) => {
await (async () => {
const keys = await db.list()
for (const userId of keys) {
const userData = (await db.get(userId)) || {}
const today = new Date().toISOString().split("T")[0]
userData[today] = {}
await db.set(userId, userData)
}
})()
res.json({ success: true, message: "Manual reset done." })
})
app.listen(3000, () => console.log("Habit Tracker daily reset service running."))

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
When building a habit tracker on Replit, the most reliable setup is to use a simple Node.js + Express server with a lightweight SQLite database (through better-sqlite3 or sqlite package) and a React or vanilla JS frontend. Replit gives you an always-on web server with instant deployment, but you must manage data persistence carefully — local .db files are fine for small projects, but for real users or team projects, connect to an external database (like Replit DB, Supabase, or MongoDB Atlas). Always store API keys in the “Secrets” tab, never hardcode them. Your code should separate the backend and frontend clearly, and take advantage of Replit’s multiplayer feature for collaboration.
Create your Repl using the “Node.js” template. Then organize your files like this:
habit-tracker/
├── index.js // main backend file (Express server)
├── public/
│ ├── index.html // main HTML
│ ├── style.css // basic styling
│ └── script.js // frontend interaction logic
├── habit.db // SQLite database (auto-created)
├── package.json // dependencies
The backend handles data storage and API routes. Install dependencies in Replit’s Shell (bottom pane):
npm install express better-sqlite3
Now, inside index.js, paste this code:
const express = require('express')
const Database = require('better-sqlite3')
const app = express()
const db = new Database('habit.db')
// Middleware to parse JSON
app.use(express.json())
app.use(express.static('public'))
// Create table if it doesn't exist
db.prepare('CREATE TABLE IF NOT EXISTS habits (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, done INTEGER)').run()
// API to get all habits
app.get('/api/habits', (req, res) => {
const habits = db.prepare('SELECT * FROM habits').all()
res.json(habits)
})
// API to add new habit
app.post('/api/habits', (req, res) => {
const { name } = req.body
if(!name) return res.status(400).json({error: 'Name required'})
db.prepare('INSERT INTO habits (name, done) VALUES (?, 0)').run(name)
res.json({success: true})
})
// API to mark habit done
app.post('/api/habits/:id/done', (req, res) => {
const { id } = req.params
db.prepare('UPDATE habits SET done = 1 WHERE id = ?').run(id)
res.json({success: true})
})
const port = process.env.PORT || 3000
app.listen(port, () => console.log('Server running on port', port))
This creates a simple REST API and static file server. On Replit, it auto-exposes the public URL when you click “Run”.
This file displays habits and allows adding/checking them. Create a new file under public/ named index.html and add:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Habit Tracker</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>My Habit Tracker</h1>
<input id="habitName" placeholder="Add new habit..." />
<button id="addHabitBtn">Add Habit</button>
<ul id="habitList"></ul>
<script src="script.js"></script>
</body>
</html>
This file communicates with your Express API. Create it under public/script.js:
async function fetchHabits() {
const res = await fetch('/api/habits')
const habits = await res.json()
const list = document.getElementById('habitList')
list.innerHTML = ''
habits.forEach(h => {
const li = document.createElement('li')
li.textContent = h.name + (h.done ? ' ✅' : '')
if(!h.done) {
const btn = document.createElement('button')
btn.textContent = 'Mark Done'
btn.onclick = () => markDone(h.id)
li.appendChild(btn)
}
list.appendChild(li)
})
}
async function addHabit() {
const name = document.getElementById('habitName').value
if(!name.trim()) return
await fetch('/api/habits', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name})
})
document.getElementById('habitName').value = ''
fetchHabits()
}
async function markDone(id) {
await fetch(`/api/habits/${id}/done`, {method: 'POST'})
fetchHabits()
}
document.getElementById('addHabitBtn').onclick = addHabit
fetchHabits()
Create a public/style.css file for simple UI:
body {
font-family: system-ui;
margin: 40px;
}
button {
margin-left: 10px;
}
process.env.VARIABLE\_NAME in Node.curl commands for quick API testing:
curl -X POST -H "Content-Type: application/json" -d '{"name":"Read"}' https://your-repl-url/api/habits.
This exact setup gives you a working, persistent habit tracker inside Replit’s environment. It uses real, proven packages (Express + better-sqlite3), respects Replit’s file structure, and can be extended easily — for example, adding user login with external DB or adding analytics.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.