Build a personal productivity app in Replit with notes, tasks, bookmarks, and a daily planner — a Notion-lite — in 1-2 hours. Replit Agent scaffolds the Express backend with PostgreSQL, Replit Auth handles login, and each user gets a fully isolated workspace. No external API keys needed.
What you're building
A personal productivity app is one of the highest-value builds for a solo founder — it combines notes, task management, bookmarks, and a daily planner into a single workspace. Unlike general-purpose tools like Notion, your version is tuned exactly to how you work, hosted on your own infrastructure, and free to customize without API limits or subscription fees.
Replit Agent generates the complete backend with five PostgreSQL tables and a full REST API in one prompt. Replit Auth provides zero-config login — users sign in with Google, GitHub, or email, and every database query is scoped to their user_id so no user ever sees another's data. The most technically interesting structure is the nested folder tree: a self-referential folders table with a parent_id column, queried via a recursive CTE that returns the entire tree in a single database call.
The daily planner is the glue feature that makes this more than a simple to-do app. Each date has a daily_plans row that stores an ordered array of task IDs — the user drags tasks from their general list onto today's plan, and the planner shows only what needs doing today. Deploy on Autoscale — personal productivity apps have low concurrent traffic and the 10GB PostgreSQL free tier is more than enough for text-heavy note and bookmark data.
Final result
A personal productivity workspace with nested-folder notes, prioritized tasks, tagged bookmarks, and daily planning — all scoped to individual users via Replit Auth.
Tech stack
Prerequisites
- A Replit account (Free tier is sufficient)
- No external API keys needed — Replit Auth and PostgreSQL are built in
- Basic familiarity with what you want to track (notes, tasks, bookmarks) to customize the schema
Build steps
Scaffold the project with Replit Agent
Create a new Replit App and paste this prompt. Agent builds the complete productivity backend with all five tables and a React frontend with the three-panel layout.
1// Build a personal productivity app with Express and PostgreSQL using Drizzle ORM.2// Use Replit Auth for user authentication — every query must scope data by user_id.3//4// Tables:5// 1. folders: id serial primary key, user_id text not null, name text not null,6// parent_id integer references folders (self-referential nesting),7// color text, position integer default 08// 2. notes: id serial primary key, user_id text not null, title text not null,9// content text (markdown body), folder_id integer references folders,10// is_pinned boolean default false, created_at timestamp default now(),11// updated_at timestamp default now()12// 3. tasks: id serial primary key, user_id text not null, title text not null,13// description text, due_date timestamp, priority text default 'medium'14// (enum: low/medium/high/urgent), status text default 'todo'15// (enum: todo/in_progress/done), recurrence text default 'none'16// (enum: none/daily/weekly/monthly), completed_at timestamp, created_at timestamp default now()17// 4. bookmarks: id serial primary key, user_id text not null, url text not null,18// title text, description text, tags text[] (PostgreSQL text array),19// favicon_url text, created_at timestamp default now()20// 5. daily_plans: id serial primary key, user_id text not null, date date not null,21// task_ids jsonb (ordered array of task IDs), notes text,22// unique(user_id, date)23//24// Routes:25// GET/POST /api/notes, PUT/DELETE /api/notes/:id26// GET/POST /api/folders (GET returns tree structure via recursive CTE)27// GET/POST /api/tasks, PATCH /api/tasks/:id/status, PUT /api/tasks/:id28// GET/POST /api/bookmarks, DELETE /api/bookmarks/:id29// GET/PUT /api/daily/:date (get or create daily plan for a date)30//31// React frontend: three-panel layout with folder tree sidebar,32// center content list, right detail/editor panel.33// Bind server to 0.0.0.0:3000.Pro tip: After Agent scaffolds the app, open the Database tab (Drizzle Studio) and insert a few test notes and tasks to verify the schema before building the frontend. It's much easier to debug schema issues before the UI is wired up.
Expected result: Running Express app with all five tables created. Replit Auth login page appears on first visit.
Build the folder tree with recursive CTE
The nested folder structure is the most technically complex part. This route uses a PostgreSQL recursive CTE to fetch the entire folder tree in a single query instead of multiple round-trips.
1const express = require('express');2const { db } = require('../db');3const { folders } = require('../schema');4const { eq, isNull } = require('drizzle-orm');56const router = express.Router();78// Get entire folder tree for the current user using recursive CTE9router.get('/api/folders', async (req, res) => {10 if (!req.user) return res.status(401).json({ error: 'Login required' });1112 // Recursive CTE: start with root folders, then recursively include children13 const result = await db.execute({14 sql: `15 WITH RECURSIVE tree AS (16 SELECT id, name, parent_id, color, position, 0 AS depth17 FROM folders18 WHERE user_id = $1 AND parent_id IS NULL19 UNION ALL20 SELECT f.id, f.name, f.parent_id, f.color, f.position, t.depth + 121 FROM folders f22 JOIN tree t ON f.parent_id = t.id23 WHERE f.user_id = $124 )25 SELECT * FROM tree ORDER BY depth, position, name26 `,27 params: [req.user.id],28 });2930 // Build nested tree structure from flat rows31 const folderMap = {};32 const roots = [];3334 for (const row of result.rows) {35 folderMap[row.id] = { ...row, children: [] };36 }37 for (const row of result.rows) {38 if (row.parent_id) {39 folderMap[row.parent_id]?.children.push(folderMap[row.id]);40 } else {41 roots.push(folderMap[row.id]);42 }43 }4445 return res.json({ folders: roots });46});4748router.post('/api/folders', express.json(), async (req, res) => {49 if (!req.user) return res.status(401).json({ error: 'Login required' });50 const { name, parentId, color } = req.body;51 if (!name) return res.status(400).json({ error: 'name is required' });5253 const row = await db.insert(folders)54 .values({ userId: req.user.id, name, parentId: parentId || null, color: color || null })55 .returning();56 return res.status(201).json(row[0]);57});5859module.exports = router;Pro tip: The self-referential folders table can create infinite loops if parent_id somehow points to a descendant. Prevent this in the create/update routes by checking that the new parent_id is not already a child of the folder being moved.
Expected result: GET /api/folders returns a nested tree: [{id:1, name:'Work', children:[{id:2, name:'Projects', children:[]}]}]. Root folders have no parent_id.
Add the tasks API with priority and status filtering
Tasks are the most-used feature. This route supports filtering by status, priority, and due date — essential for showing 'what's overdue today' and 'what's urgent'.
1const express = require('express');2const { db } = require('../db');3const { tasks } = require('../schema');4const { eq, and, lte, gte, inArray } = require('drizzle-orm');56const router = express.Router();78router.get('/api/tasks', async (req, res) => {9 if (!req.user) return res.status(401).json({ error: 'Login required' });10 const { status, priority, dueBefore, dueAfter } = req.query;1112 const conditions = [eq(tasks.userId, req.user.id)];13 if (status) conditions.push(eq(tasks.status, status));14 if (priority) conditions.push(eq(tasks.priority, priority));15 if (dueBefore) conditions.push(lte(tasks.dueDate, new Date(dueBefore)));16 if (dueAfter) conditions.push(gte(tasks.dueDate, new Date(dueAfter)));1718 const rows = await db.select().from(tasks)19 .where(and(...conditions))20 .orderBy(tasks.dueDate, tasks.priority);2122 return res.json({ tasks: rows });23});2425router.post('/api/tasks', express.json(), async (req, res) => {26 if (!req.user) return res.status(401).json({ error: 'Login required' });27 const { title, description, dueDate, priority, recurrence } = req.body;28 if (!title) return res.status(400).json({ error: 'title is required' });2930 const row = await db.insert(tasks).values({31 userId: req.user.id,32 title,33 description: description || null,34 dueDate: dueDate ? new Date(dueDate) : null,35 priority: priority || 'medium',36 recurrence: recurrence || 'none',37 }).returning();38 return res.status(201).json(row[0]);39});4041router.patch('/api/tasks/:id/status', express.json(), async (req, res) => {42 if (!req.user) return res.status(401).json({ error: 'Login required' });43 const { status } = req.body;44 const validStatuses = ['todo', 'in_progress', 'done'];45 if (!validStatuses.includes(status)) {46 return res.status(400).json({ error: 'Invalid status' });47 }48 const updated = await db.update(tasks)49 .set({ status, completedAt: status === 'done' ? new Date() : null, updatedAt: new Date() })50 .where(and(eq(tasks.id, parseInt(req.params.id)), eq(tasks.userId, req.user.id)))51 .returning();52 if (!updated[0]) return res.status(404).json({ error: 'Not found' });53 return res.json(updated[0]);54});5556module.exports = router;Pro tip: Add a GET /api/tasks?status=todo&dueBefore=2024-12-31 endpoint call on the dashboard 'Today' view to show overdue tasks — tasks where due_date is in the past and status is not 'done'. This is the most useful view for daily planning.
Expected result: GET /api/tasks returns user's tasks. PATCH /api/tasks/1/status with {status:'done'} marks it complete and sets completed_at.
Add bookmarks with auto-fetch and the daily planner
Bookmarks auto-fetch page titles server-side (avoiding CORS issues), and the daily planner stores an ordered list of task IDs for each date.
1// Ask Agent to add bookmarks and daily planner with this prompt:2// Add to the Express app:3//4// 1. POST /api/bookmarks:5// - Require Replit Auth6// - Accept {url, tags} in request body7// - Server-side fetch the URL using node-fetch: GET the HTML, extract <title> tag8// - Try to find favicon at /favicon.ico on the domain9// - Insert into bookmarks with fetched title, favicon_url, and tags10// - If fetch fails (CORS, timeout), store with title=null and continue11// - Set a 5-second timeout on the title fetch to not block the response12//13// 2. GET /api/bookmarks:14// - Require auth15// - Accept optional ?tag= query param to filter by tag16// - For tag filter, use PostgreSQL array contains: WHERE $1 = ANY(tags)17// - Return bookmarks ordered by created_at desc18//19// 3. GET /api/daily/:date:20// - Require auth21// - Accept date in YYYY-MM-DD format22// - If daily_plan exists for user+date, return it with task details joined23// - If not, INSERT a new empty daily_plan and return it24// - Join task_ids array against tasks table to return full task objects in order25//26// 4. PUT /api/daily/:date:27// - Require auth28// - Accept {task_ids: [array of IDs], notes: string}29// - Upsert: INSERT ... ON CONFLICT (user_id, date) DO UPDATE30// - Return the updated planPro tip: The server-side title fetch in POST /api/bookmarks is important — doing it client-side would hit CORS errors on most websites. In the Express handler, use Promise.race([fetchTitle(), timeoutAfter5Seconds()]) so a slow website doesn't block the entire bookmark save.
Expected result: POST /api/bookmarks with {url: 'https://example.com', tags: ['reading']} returns a bookmark with title auto-populated. GET /api/daily/2024-12-25 returns an empty plan or existing plan with full task objects.
Build the React three-panel layout
Ask Agent to create the React frontend with the signature three-panel layout: folder sidebar, content list, and editor/detail panel.
1// Ask Agent to build the React frontend with this prompt:2// Build a React productivity app with a three-panel layout:3//4// Left sidebar (240px fixed):5// - Section navigation icons at top: Notes, Tasks, Bookmarks, Today (icons with labels)6// - Below icons: folder tree rendered from GET /api/folders7// Each folder is a collapsible row with indentation based on depth8// An 'Add folder' button creates a subfolder via POST /api/folders9//10// Center panel (flex-grow):11// - Notes view: list of notes for selected folder, sorted by is_pinned desc, updated_at desc12// Each row shows title, first line of content, and timestamp13// Click row to open in right panel14// - Tasks view: list with checkbox, title, priority dot (urgent=red, high=orange, medium=blue, low=gray),15// due date badge (red if overdue, orange if today). Click checkbox calls PATCH /api/tasks/:id/status16// - Bookmarks view: list with favicon, title, domain, and tag chips17// - Today view: date header with today's date, ordered list of today's tasks from GET /api/daily/:date18//19// Right panel (320px fixed):20// - Notes: textarea with live markdown preview toggle (toggle between edit and preview modes)21// Auto-save on 2-second debounce via PUT /api/notes/:id22// - Tasks: edit form with all fields23// - Bookmarks: full URL, description, tag editor24//25// Use React Router. Add keyboard shortcut Cmd+N to create a new note.Expected result: The app shows the three-panel layout after login. Notes appear in the center panel, markdown renders in the right panel. Clicking a task checkbox toggles it complete.
Complete code
1const express = require('express');2const { db } = require('../db');3const { tasks } = require('../schema');4const { eq, and, lte, gte, desc } = require('drizzle-orm');5const { withDbRetry } = require('../lib/retryDb');67const router = express.Router();89const VALID_STATUSES = ['todo', 'in_progress', 'done'];10const VALID_PRIORITIES = ['low', 'medium', 'high', 'urgent'];1112router.get('/api/tasks', async (req, res) => {13 if (!req.user) return res.status(401).json({ error: 'Login required' });14 const { status, priority, dueBefore, dueAfter } = req.query;15 const conditions = [eq(tasks.userId, req.user.id)];16 if (status && VALID_STATUSES.includes(status)) conditions.push(eq(tasks.status, status));17 if (priority && VALID_PRIORITIES.includes(priority)) conditions.push(eq(tasks.priority, priority));18 if (dueBefore) conditions.push(lte(tasks.dueDate, new Date(dueBefore)));19 if (dueAfter) conditions.push(gte(tasks.dueDate, new Date(dueAfter)));20 const rows = await db.select().from(tasks)21 .where(and(...conditions))22 .orderBy(desc(tasks.priority), tasks.dueDate);23 return res.json({ tasks: rows });24});2526router.post('/api/tasks', express.json(), async (req, res) => {27 if (!req.user) return res.status(401).json({ error: 'Login required' });28 const { title, description, dueDate, priority, recurrence } = req.body;29 if (!title?.trim()) return res.status(400).json({ error: 'title is required' });30 const row = await withDbRetry(() =>31 db.insert(tasks).values({32 userId: req.user.id,33 title: title.trim(),34 description: description || null,35 dueDate: dueDate ? new Date(dueDate) : null,36 priority: VALID_PRIORITIES.includes(priority) ? priority : 'medium',37 recurrence: recurrence || 'none',38 }).returning()39 );40 return res.status(201).json(row[0]);41});4243router.put('/api/tasks/:id', express.json(), async (req, res) => {44 if (!req.user) return res.status(401).json({ error: 'Login required' });45 const { title, description, dueDate, priority, status } = req.body;46 const updated = await db.update(tasks)47 .set({48 ...(title && { title }),49 ...(description !== undefined && { description }),50 ...(dueDate !== undefined && { dueDate: dueDate ? new Date(dueDate) : null }),51 ...(priority && { priority }),52 ...(status && { status }),53 updatedAt: new Date(),54 })55 .where(and(eq(tasks.id, parseInt(req.params.id)), eq(tasks.userId, req.user.id)))56 .returning();57 if (!updated[0]) return res.status(404).json({ error: 'Not found' });58 return res.json(updated[0]);59});6061module.exports = router;Customization ideas
Full-text search across all content
Add a GET /api/search?q= endpoint that queries notes, tasks, and bookmarks simultaneously using PostgreSQL's ILIKE on title and content fields. Return results grouped by type with matched text highlighted.
Recurring task generation
Add a Scheduled Deployment that runs daily at midnight, finds tasks with recurrence='daily'/'weekly'/'monthly' and status='done', and creates new task instances with the next due date — automating repeating to-do items.
Note sharing via public link
Add a share_token column to notes. A GET /public/notes/:token endpoint returns the markdown content without auth. The note owner can toggle sharing from the editor panel and copy a shareable URL.
Pomodoro timer integration
Add a pomodoro_sessions table tracking work sessions per task. The React frontend shows a 25-minute timer that links to the active task — after each session, automatically log time spent against the task.
Common pitfalls
Pitfall: Not scoping every query by user_id
How to avoid: Every SELECT, UPDATE, and DELETE must include WHERE user_id = req.user.id. Add a middleware that returns 401 if req.user is not set, applied to all productivity routes.
Pitfall: Fetching bookmark titles client-side with the browser's fetch()
How to avoid: Always fetch the page title server-side in the Express POST /api/bookmarks handler using node-fetch. Wrap it in a 5-second timeout so slow or broken sites don't block the response.
Pitfall: Loading all notes content for the list view
How to avoid: In GET /api/notes, SELECT only id, title, is_pinned, updated_at and the first 150 characters of content for the preview. Load the full content only when a specific note is opened.
Pitfall: Infinite recursion in the folder tree CTE
How to avoid: Add a CYCLE DETECTION clause to the CTE: WITH RECURSIVE tree AS (...) CYCLE id SET is_cycle USING path. This automatically stops infinite loops.
Best practices
- Scope every database query by user_id — Replit Auth provides req.user.id, use it on every route as the first filter.
- Use PostgreSQL's WITH RECURSIVE CTE for the folder tree instead of making N database calls to fetch nested levels — one query is always faster.
- Auto-save notes with a 2-second debounce in React — users expect changes to persist without clicking a save button.
- Use Drizzle Studio (Database tab in Replit sidebar) to quickly inspect and edit your data during development without writing SQL queries.
- Snapshot the unit_price (for bookmarks) or content at time of creation — this prevents corrupted data if the source URL changes later.
- Add a daily_plans upsert with ON CONFLICT DO UPDATE — the user should be able to freely update their day plan without creating duplicate rows.
- Deploy on Autoscale — personal productivity apps have irregular usage patterns with long idle periods between bursts of note-taking activity.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a personal productivity app with Express and PostgreSQL using Drizzle ORM. I have a folders table with a self-referential parent_id column and a notes table with a folder_id. Help me write a PostgreSQL recursive CTE query that fetches the entire folder tree for a given user_id, returning rows ordered by depth and position. Then help me write a Drizzle ORM wrapper function getFolderTree(userId) that calls this query and returns a nested JavaScript object tree.
Add a full-text search feature to the productivity app. Build a GET /api/search?q= route that queries notes (searching title and content), tasks (searching title and description), and bookmarks (searching title and url) simultaneously using PostgreSQL ILIKE. Return results as an object with three arrays: {notes: [], tasks: [], bookmarks: []}. Each result includes the relevant fields plus a snippet: the first 200 characters of content that contains the search term, with the matched text wrapped in ** markers. Add a search input in the React nav bar that opens a search results panel showing grouped results from all three types.
Frequently asked questions
Can multiple users share the same productivity workspace?
The default build creates fully isolated personal workspaces — no user sees another's data. To add collaboration, you'd extend the schema with a shared_notes table or an org_id column on the folders table. That's covered in the team-workspace build guide.
What Replit plan do I need?
Free tier is sufficient. Replit Auth is built-in and free, the PostgreSQL database is free with 10GB storage (plenty for text-heavy productivity data), and Autoscale deployment is included. No external API keys are needed for the core build.
How do I handle markdown rendering in the notes editor?
Ask Agent to add a markdown preview toggle using a library like marked or markdown-it (npm packages). The editor shows raw markdown in a textarea, and a Preview button renders the HTML using marked(content). Both panels can be shown side-by-side in wide screen layouts.
Will notes auto-save if I close the browser mid-edit?
With a 2-second debounce auto-save, any changes you've paused typing on will have saved. To protect against browser crashes mid-keystroke, save a draft copy in localStorage as a backup — on load, compare localStorage draft against the server version and prompt the user to restore if the draft is newer.
How deep can I nest folders?
The recursive CTE has no hard depth limit. Practically, deeply nested trees become hard to navigate in a UI. Consider limiting nesting to 3-4 levels in the frontend by disabling the 'Create subfolder' option beyond a certain depth.
Should I deploy on Autoscale or Reserved VM?
Autoscale is the right choice for a personal productivity app. You'll use it throughout the day in bursts — it handles the first-request cold start quickly, and the $0 idle cost makes sense for a personal tool that's offline much of the time.
Can RapidDev help build a custom productivity workspace for my team?
Yes. RapidDev can extend this personal workspace into a team collaboration tool with shared workspaces, real-time editing, and role-based access — drawing from 600+ apps built for clients. Free consultation available.
How does the daily planner connect tasks to specific dates?
The daily_plans table stores an ordered array of task IDs as JSONB for each (user_id, date) pair. When you drag tasks onto today's plan, the frontend sends PUT /api/daily/:date with the new task_ids array. The response joins those IDs against the tasks table to return full task objects in the saved order.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation