Learn how to build a powerful productivity app with Replit. Follow our step-by-step guide to create, test, and launch your app efficiently.

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 but real-world way to build a Productivity App in Replit is to use a full-stack setup with Node.js for the backend and React for the frontend. You’ll create a small task manager where users can add, mark done, and remove tasks. Replit can host both the frontend and backend in the same Repl, and you can use MongoDB Atlas for a persistent database. You’ll store your database connection string safely using Replit Secrets, and you can push your code to GitHub from within Replit once it’s working.
Create a new Repl with the template “Node.js”. You’ll add React manually so both backend and frontend live together. In the left sidebar, you’ll see a file tree — that’s where we’ll add new files/folders for structure:
// Inside Replit Shell, install dependencies
npm init -y
npm install express cors mongoose
That gives you backend basics: Express for server creation and Mongoose for MongoDB connection.
Edit index.js at root. Here’s your initial working backend code:
// index.js
const express = require('express')
const mongoose = require('mongoose')
const cors = require('cors')
// Environment variable secret (set in Replit Secrets)
const MONGO_URI = process.env.MONGO_URI
const app = express()
app.use(cors())
app.use(express.json())
// Connect to MongoDB
mongoose.connect(MONGO_URI)
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error(err))
// Define schema for tasks
const taskSchema = new mongoose.Schema({
title: String,
done: { type: Boolean, default: false }
})
const Task = mongoose.model('Task', taskSchema)
// Route: Get all tasks
app.get('/tasks', async (req, res) => {
const tasks = await Task.find()
res.json(tasks)
})
// Route: Add new task
app.post('/tasks', async (req, res) => {
const newTask = new Task({ title: req.body.title })
await newTask.save()
res.json(newTask)
})
// Route: Toggle done
app.put('/tasks/:id', async (req, res) => {
const updated = await Task.findByIdAndUpdate(req.params.id, { done: req.body.done }, { new: true })
res.json(updated)
})
// Route: Delete task
app.delete('/tasks/:id', async (req, res) => {
await Task.findByIdAndDelete(req.params.id)
res.json({ message: 'Deleted' })
})
const PORT = 3000
app.listen(PORT, () => console.log(`Server running on port ${PORT}`))
This file runs your backend API. You’ll see output in the Replit console when you click "Run". Before running, you must set a Replit Secret named MONGO\_URI (on left sidebar → "Secrets" icon → add key/value). Use a connection string from MongoDB Atlas.
Now create your React client manually inside client/ folder. In the Shell:
npx create-react-app client
Once it finishes, Replit adds a nested package.json for the client. You now have two package.json files: one in root (backend) and one in client/ (frontend). You’ll run each separately when testing locally, but Replit can serve your frontend from the same port in production.
In your root folder, add a new script in the backend package.json to build React once:
"scripts": {
"start": "node index.js",
"build-client": "npm run build --prefix client"
}
Inside client/src/, open App.js and replace all its content with this simple connected frontend:
// client/src/App.js
import React, { useState, useEffect } from 'react'
const API_URL = 'https://your-repl-url.repl.co' // Change it to your Replit web URL
function App() {
const [tasks, setTasks] = useState([])
const [title, setTitle] = useState('')
// Fetch all tasks
useEffect(() => {
fetch(`${API_URL}/tasks`)
.then(res => res.json())
.then(setTasks)
}, [])
// Add new task
const addTask = async () => {
const res = await fetch(`${API_URL}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
})
const data = await res.json()
setTasks([...tasks, data])
setTitle('')
}
// Toggle done
const toggleDone = async (id, done) => {
const res = await fetch(`${API_URL}/tasks/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done })
})
const updated = await res.json()
setTasks(tasks.map(t => (t._id === id ? updated : t)))
}
// Delete task
const deleteTask = async (id) => {
await fetch(`${API_URL}/tasks/${id}`, { method: 'DELETE' })
setTasks(tasks.filter(t => t._id !== id))
}
return (
<div style={{ padding: 20 }}>
<h1>Productivity App</h1>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="New task" />
<button onClick={addTask}>Add</button>
<ul>
{tasks.map(task => (
<li key={task._id}>
<input type="checkbox" checked={task.done} onChange={() => toggleDone(task._id, !task.done)} />
{task.title}
<button onClick={() => deleteTask(task._id)}>X</button>
</li>
))}
</ul>
</div>
)
}
export default App
You can build the React frontend once and serve static files from the backend so that Replit runs both together. After your React app is ready, run this in the Shell:
npm run build-client
This creates a client/build folder. Now modify your index.js in the backend to serve that folder before your API routes:
// Add this line after express setup but before routes
app.use(express.static('client/build'))
This allows Replit to serve your React app and backend together at your Replit URL (like https://your-repl-name.username.repl.co). When you click "Run", the server starts and users can see the React UI hitting the same backend API.
npm install nodemon and update your start script.
Following this step-by-step structure gives you a complete productivity app directly within Replit — backend with Express, persistent storage via MongoDB Atlas, and a React frontend. It runs fully in the Replit editor, no local system required.
<!-- server.js -->
<script type="module">
import express from "express";
import Database from "@replit/database";
const app = express();
const db = new Database();
app.use(express.json());
// Task schema stored as JSON per user
const normalizeTask = (task) => ({
id: task.id || Date.now().toString(),
title: task.title?.trim() || "Untitled",
done: !!task.done,
createdAt: task.createdAt || new Date().toISOString()
});
// Create or update a task
app.post("/api/tasks", async (req, res) => {
const user = req.query.user || "default";
const task = normalizeTask(req.body);
const tasks = (await db.get(user)) || [];
const updated = [...tasks.filter(t => t.id !== task.id), task];
await db.set(user, updated);
res.json({ ok: true, task });
});
// Retrieve all tasks
app.get("/api/tasks", async (req, res) => {
const user = req.query.user || "default";
const tasks = (await db.get(user)) || [];
res.json(tasks.sort((a,b)=>new Date(b.createdAt)-new Date(a.createdAt)));
});
// Toggle task done
app.post("/api/tasks/:id/toggle", async (req, res) => {
const user = req.query.user || "default";
const id = req.params.id;
const tasks = (await db.get(user)) || [];
const updated = tasks.map(t => t.id === id ? { ...t, done: !t.done } : t);
await db.set(user, updated);
res.json({ ok: true });
});
app.listen(3000, () => console.log("✅ API running on port 3000"));
</script>
<!-- server.js: Sync completed tasks to external Notion workspace using secret API key -->
<script type="module">
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());
app.post("/api/syncCompletedTasks", async (req, res) => {
const NOTION_API_KEY = process.env.NOTION_API_KEY;
const NOTION_DATABASE_ID = process.env.NOTION_DATABASE_ID;
if (!NOTION_API_KEY || !NOTION_DATABASE_ID)
return res.status(400).json({ error: "Missing Notion configuration" });
const { user = "default" } = req.query;
const tasks = (await db.get(user)) || [];
const completed = tasks.filter(t => t.done && !t.synced);
const results = [];
for (const task of completed) {
const response = await fetch("https://api.notion.com/v1/pages", {
method: "POST",
headers: {
"Authorization": `Bearer ${NOTION_API_KEY}`,
"Content-Type": "application/json",
"Notion-Version": "2022-06-28"
},
body: JSON.stringify({
parent: { database_id: NOTION_DATABASE\_ID },
properties: {
Name: { title: [{ text: { content: task.title } }] },
Status: { select: { name: "Done" } },
CompletedAt: { date: { start: new Date().toISOString() } }
}
})
});
if (response.ok) {
task.synced = true;
results.push({ id: task.id, status: "synced" });
} else {
results.push({ id: task.id, status: "failed" });
}
}
if (results.length) await db.set(user, tasks);
res.json({ synced: results.length, details: results });
});
app.listen(3000, () => console.log("✅ Sync API running on port 3000"));
</script>
<!-- server.js: Background email reminder queue using Replit's Database and Cron job -->
<script type="module">
import express from "express";
import Database from "@replit/database";
import fetch from "node-fetch";
const app = express();
const db = new Database();
app.use(express.json());
// Add task with reminder time
app.post("/api/taskWithReminder", async (req, res) => {
const { title, remindAt, email } = req.body;
if (!title || !remindAt || !email)
return res.status(400).json({ error: "Missing required fields" });
const task = {
id: Date.now().toString(),
title,
remindAt,
email,
notified: false
};
const tasks = (await db.get("reminders")) || [];
tasks.push(task);
await db.set("reminders", tasks);
res.json({ ok: true, task });
});
// Cron-safe background reminder check
app.get("/cron/sendReminders", async (req, res) => {
const tasks = (await db.get("reminders")) || [];
const now = Date.now();
const pending = tasks.filter(t => !t.notified && new Date(t.remindAt).getTime() <= now);
const results = [];
for (const task of pending) {
try {
// Example fake email API call
const r = await fetch("https://api.example-mailer.com/send", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": process.env.MAIL_API_KEY },
body: JSON.stringify({
to: task.email,
subject: `Reminder: ${task.title}`,
text: `It's time for "${task.title}".`
})
});
if (r.ok) {
task.notified = true;
results.push({ id: task.id, status: "sent" });
} else {
results.push({ id: task.id, status: "failed" });
}
} catch {
results.push({ id: task.id, status: "error" });
}
}
await db.set("reminders", tasks);
res.json({ sent: results.length, details: results });
});
app.listen(3000, () => console.log("✅ Reminder API running on port 3000"));
</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 productivity app built on Replit works best when you treat Replit like a real, cloud-hosted workspace — not just an online code sandbox. The key is to separate front-end (React or simple HTML/JS), back-end (Node.js or Python Flask/FastAPI), and persistent storage (Replit Database, SQLite, or an external DB). Keep your secrets in Replit’s Secrets tab, not inside code. Use replit.yaml for custom run commands, commit meaningful changes with Git, and watch out for Replit’s limits: the environment sleeps if inactive (unless you have Always On), and file writes go to ephemeral storage in some templates. Build modularly — one Repl per purpose (API vs client), then connect them using the web URLs Replit provides.
Create a new Node.js Repl named productivity-app. Inside, create:
// In the Replit shell:
mkdir public
touch server.js database.js public/index.html public/script.js public/style.css
Go to server.js and build a small Express server that serves your frontend and exposes simple productivity APIs (for tasks, notes, etc):
// server.js
import express from "express";
import { db } from "./database.js"; // import our database module
const app = express();
app.use(express.json());
app.use(express.static("public")); // serve files from /public
// Example API: Add a task
app.post("/api/tasks", async (req, res) => {
const { text } = req.body;
if (!text) return res.status(400).json({ error: "Task text required" });
const id = Date.now().toString();
await db.set(id, { text, done: false });
res.json({ id, text, done: false });
});
// Example API: Get all tasks
app.get("/api/tasks", async (req, res) => {
const keys = await db.list();
const tasks = [];
for (const key of keys) {
tasks.push(await db.get(key));
}
res.json(tasks);
});
app.listen(3000, () => console.log("Server running on port 3000"));
Explanation: this runs your web server inside Replit. The express.static line makes Replit serve whatever’s inside your public folder automatically at the root URL. The other routes handle simple APIs. The port 3000 is correct for Replit because it auto-detects that port for web previews.
Replit provides a built-in lightweight key-value store called @replit/database. Install it using the Replit Packages tool or run this in Replit shell:
npm install @replit/database
Inside database.js add:
// database.js
import Database from "@replit/database";
export const db = new Database(); // default uses Replit's auto-connected DB
This requires no setup — the data lives in your Replit account (persistent across runs). For real production-like storage, later you can switch to external databases (Postgres, MongoDB, etc.) using connection strings stored in Replit Secrets.
Inside public/index.html, put the minimal HTML skeleton that connects to your backend APIs:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>My Productivity App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Tasks</h1>
<form id="task-form">
<input id="task-input" placeholder="New Task..." />
<button>Add</button>
</form>
<ul id="task-list"></ul>
<script src="script.js"></script>
</body>
</html>
Then in public/script.js add this code to call your backend APIs:
// public/script.js
const form = document.getElementById("task-form");
const input = document.getElementById("task-input");
const list = document.getElementById("task-list");
// Load existing tasks on page load
window.addEventListener("load", async () => {
const res = await fetch("/api/tasks");
const tasks = await res.json();
for (const t of tasks) addTaskToDOM(t);
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
const res = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text })
});
const task = await res.json();
addTaskToDOM(task);
input.value = "";
});
function addTaskToDOM(task) {
const li = document.createElement("li");
li.textContent = task.text;
list.appendChild(li);
}
That’s enough to create and list tasks dynamically from the server. When you run the Repl, Replit will show a URL like https://productivity-app.
If you connect external APIs (e.g. Google Calendar or OpenAI), never paste tokens in code. In Replit left sidebar, open Tools → Secrets and add them there (e.g. API\_KEY). You can then access them safely in code like this:
const apiKey = process.env.API_KEY; // available from Replit Secrets
run: npm startThen in package.json set:
"scripts": { "start": "node server.js" }
By following this structure — backend in server.js, frontend static files in public/, and environment-safe secrets setup — you’ll have a solid, maintainable productivity app running entirely inside Replit that behaves closely to a small-scale production app.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.