Build a Twitter/Instagram-style social feed with Replit in 1-2 hours. You'll create an Express API with PostgreSQL (Drizzle ORM) for posts, followers, likes, and comments, plus a React frontend with a timeline, profile pages, and a post composer. Replit Agent scaffolds the full app from one prompt. Deploy on Autoscale.
What you're building
A social media feed lets users share content publicly, follow people they care about, and engage through likes and comments. Without this infrastructure, founders who want community features in their apps have to bolt on third-party widgets that don't match their brand or store behavior they can't analyze.
Replit Agent scaffolds the entire Express + PostgreSQL app from a single prompt. You describe the features — posts, follows, likes, comments — and Agent generates the Drizzle schema, REST routes, and a React frontend with all the expected UI patterns. What used to take days of setup is done in minutes.
The architecture is straightforward: an Express server handles all API routes, Drizzle ORM manages a PostgreSQL database with a compound index on `(author_id, created_at DESC)` for fast timeline queries, and PostgreSQL triggers maintain denormalized like/comment/follow counts so the frontend never has to compute aggregates. Deploy on Autoscale — social feeds have variable traffic and the timeline query is fast with proper indexes.
Final result
A full social media feed with user profiles, a chronological timeline of followed users' posts, likes, comments, follow/unfollow, and an explore page — all backed by a PostgreSQL database with optimized indexes and trigger-maintained counters.
Tech stack
Prerequisites
- A Replit account (Free tier is sufficient)
- Basic understanding of what APIs and databases do (no coding experience needed)
- An idea of what content type your feed will display (text, images, or both)
- Optional: an image storage service like Cloudflare R2 if you want image uploads (API key stored in Secrets)
Build steps
Scaffold the social feed app with Replit Agent
Open Replit, click Create Repl, and use the Agent prompt below. Agent will generate the full Express server, Drizzle schema for profiles, posts, follows, likes, and comments, plus a React frontend with a two-column layout. This single prompt gives you a working skeleton.
1// Paste this into Replit Agent:2// Build a social media feed app with Express and PostgreSQL using Drizzle ORM.3// Schema: profiles (id, user_id unique, username unique, display_name, bio, avatar_url, follower_count default 0, following_count default 0, created_at),4// posts (id, author_id references profiles, content, image_url, like_count default 0, comment_count default 0, created_at),5// follows (id, follower_id references profiles, following_id references profiles, created_at, UNIQUE follower_id+following_id),6// likes (id, post_id references posts, user_id references profiles, created_at, UNIQUE post_id+user_id),7// comments (id, post_id references posts, author_id references profiles, content, created_at).8// Add a compound index on posts(author_id, created_at DESC).9// API routes: GET /api/feed (timeline from followed users, cursor pagination by post ID),10// GET /api/feed/explore (all recent posts), POST /api/posts, DELETE /api/posts/:id,11// POST /api/posts/:id/like (toggle), GET /api/posts/:id/comments, POST /api/posts/:id/comments,12// GET /api/profiles/:username, POST /api/profiles/:id/follow (toggle),13// GET /api/profiles/:id/followers, GET /api/profiles/:id/following.14// On first Replit Auth login, auto-create a profile with a generated username.15// React frontend: two-column layout with main feed and suggested profiles sidebar,16// post cards with avatar, username, timestamp, content, like button with count, comment icon with count,17// post composer at the top of feed, profile pages with follower/following counts and post grid.18// Bind server to 0.0.0.0. Use process.env.DATABASE_URL for DB connection.Pro tip: If you want image uploads, add 'image upload button that stores to Cloudflare R2 using a pre-signed URL from the server' to the prompt. Store the R2 credentials in Replit Secrets (lock icon) before running the app.
Expected result: Agent creates the full project structure with server/index.js, server/schema.js, server/routes/, and a React frontend. The preview shows a working feed UI.
Review and push the Drizzle schema
After Agent finishes, open the Database tool in Replit's sidebar to verify the tables were created. If they weren't, you'll push the schema manually. Also review the schema file to ensure the unique constraints on follows and likes are present — these prevent duplicate follows and double-likes at the database level.
1// server/schema.js — key parts to verify2const { pgTable, serial, text, integer, timestamp, uniqueIndex, index } = require('drizzle-orm/pg-core');34const profiles = pgTable('profiles', {5 id: serial('id').primaryKey(),6 userId: text('user_id').notNull().unique(),7 username: text('username').notNull().unique(),8 displayName: text('display_name'),9 bio: text('bio'),10 avatarUrl: text('avatar_url'),11 followerCount: integer('follower_count').default(0).notNull(),12 followingCount: integer('following_count').default(0).notNull(),13 createdAt: timestamp('created_at').defaultNow().notNull(),14});1516const posts = pgTable('posts', {17 id: serial('id').primaryKey(),18 authorId: integer('author_id').references(() => profiles.id).notNull(),19 content: text('content').notNull(),20 imageUrl: text('image_url'),21 likeCount: integer('like_count').default(0).notNull(),22 commentCount: integer('comment_count').default(0).notNull(),23 createdAt: timestamp('created_at').defaultNow().notNull(),24}, (t) => [25 index('posts_author_created_idx').on(t.authorId, t.createdAt),26]);2728const follows = pgTable('follows', {29 id: serial('id').primaryKey(),30 followerId: integer('follower_id').references(() => profiles.id).notNull(),31 followingId: integer('following_id').references(() => profiles.id).notNull(),32 createdAt: timestamp('created_at').defaultNow().notNull(),33}, (t) => [34 uniqueIndex('follows_pair_idx').on(t.followerId, t.followingId),35]);Pro tip: Open Drizzle Studio from the Database tool to visually inspect your tables and run test queries before writing any frontend code.
Implement the timeline feed route with cursor pagination
The feed route is the performance-critical path. It queries posts from users the viewer follows, ordered by recency. Cursor-based pagination using the post ID prevents the 'page drift' problem where rows shift between pages. Replace any simple LIMIT/OFFSET implementation Agent generated with the cursor approach below.
1// server/routes/feed.js2const express = require('express');3const { desc, lt, inArray, eq } = require('drizzle-orm');4const { db } = require('../db');5const { posts, profiles, follows } = require('../schema');67const router = express.Router();89router.get('/api/feed', async (req, res) => {10 const userId = req.user?.id; // from Replit Auth middleware11 const cursor = req.query.cursor ? parseInt(req.query.cursor) : null;12 const limit = 20;1314 // Get IDs of users this person follows15 const followedRows = await db.select({ followingId: follows.followingId })16 .from(follows)17 .where(eq(follows.followerId, userId));1819 const followedIds = followedRows.map(r => r.followingId);20 if (followedIds.length === 0) {21 return res.json({ posts: [], nextCursor: null });22 }2324 // Build query with optional cursor25 let query = db.select()26 .from(posts)27 .where(inArray(posts.authorId, followedIds));2829 if (cursor) {30 query = query.where(lt(posts.id, cursor));31 }3233 const results = await query34 .orderBy(desc(posts.createdAt))35 .limit(limit + 1);3637 const hasMore = results.length > limit;38 const paginated = hasMore ? results.slice(0, limit) : results;39 const nextCursor = hasMore ? paginated[paginated.length - 1].id : null;4041 res.json({ posts: paginated, nextCursor });42});4344module.exports = router;Pro tip: If a user follows nobody, show the explore feed instead as a fallback. This prevents new users from seeing a blank feed.
Add the like toggle route with trigger-maintained counts
The like system uses a toggle pattern — one endpoint handles both liking and unliking. Rather than counting likes on every read, a PostgreSQL trigger maintains the `like_count` field on the posts table. This keeps read queries fast even at scale. Ask Agent to add the trigger, or use the SQL below.
1// server/routes/likes.js2const express = require('express');3const { and, eq } = require('drizzle-orm');4const { db } = require('../db');5const { likes, posts } = require('../schema');67const router = express.Router();89router.post('/api/posts/:id/like', async (req, res) => {10 const postId = parseInt(req.params.id);11 const userId = req.user.profileId; // numeric profile ID from middleware1213 // Check if like already exists14 const existing = await db.select()15 .from(likes)16 .where(and(eq(likes.postId, postId), eq(likes.userId, userId)))17 .limit(1);1819 if (existing.length > 0) {20 // Unlike21 await db.delete(likes)22 .where(and(eq(likes.postId, postId), eq(likes.userId, userId)));23 await db.update(posts)24 .set({ likeCount: db.sql`like_count - 1` })25 .where(eq(posts.id, postId));26 return res.json({ liked: false });27 }2829 // Like30 await db.insert(likes).values({ postId, userId });31 await db.update(posts)32 .set({ likeCount: db.sql`like_count + 1` })33 .where(eq(posts.id, postId));3435 res.json({ liked: true });36});3738module.exports = router;Expected result: Clicking the heart on a post increments or decrements the count immediately and persists across page refreshes.
Deploy on Autoscale and test the feed
Social feeds have variable traffic — quiet overnight, spiky when someone shares a post publicly. Autoscale handles this perfectly, scaling to zero when no one's active and spinning up new instances during traffic bursts. Deploy from the Publish pane, then create a few test accounts to verify the follow/feed flow end to end.
1// .replit — verify deployment config2// entrypoint = "server/index.js"3// [deployment]4// deploymentTarget = "autoscale"5// run = "node server/index.js"6//7// Also confirm server/index.js binds correctly:8const PORT = process.env.PORT || 3000;9app.listen(PORT, '0.0.0.0', () => {10 console.log(`Social feed server running on port ${PORT}`);11});Pro tip: After deploying, log in with two different browser sessions (or incognito) to test the follow relationship. Follow User A from User B, create a post as User A, then verify it appears in User B's timeline.
Expected result: The app is live at your *.replit.app URL. The timeline loads posts from followed users, likes persist, and profiles show accurate follower counts.
Complete code
1const express = require('express');2const { desc, lt, inArray, eq, and } = require('drizzle-orm');3const { db } = require('../db');4const { posts, profiles, follows, likes, comments } = require('../schema');56const router = express.Router();78// GET /api/feed — timeline from followed users (cursor paginated)9router.get('/api/feed', async (req, res) => {10 const userId = req.user?.profileId;11 const cursor = req.query.cursor ? parseInt(req.query.cursor) : null;12 const limit = 20;1314 const followedRows = await db15 .select({ followingId: follows.followingId })16 .from(follows)17 .where(eq(follows.followerId, userId));1819 const followedIds = followedRows.map(r => r.followingId);2021 if (followedIds.length === 0) {22 return res.json({ posts: [], nextCursor: null, empty: true });23 }2425 const conditions = [inArray(posts.authorId, followedIds)];26 if (cursor) conditions.push(lt(posts.id, cursor));2728 const results = await db29 .select({30 id: posts.id,31 content: posts.content,32 imageUrl: posts.imageUrl,33 likeCount: posts.likeCount,34 commentCount: posts.commentCount,35 createdAt: posts.createdAt,36 authorUsername: profiles.username,37 authorDisplayName: profiles.displayName,38 authorAvatarUrl: profiles.avatarUrl,39 })40 .from(posts)41 .innerJoin(profiles, eq(posts.authorId, profiles.id))42 .where(and(...conditions))43 .orderBy(desc(posts.createdAt))44 .limit(limit + 1);4546 const hasMore = results.length > limit;47 const paginated = hasMore ? results.slice(0, limit) : results;4849 res.json({50 posts: paginated,51 nextCursor: hasMore ? paginated[paginated.length - 1].id : null,52 });53});5455// GET /api/feed/explore — all recent posts for discovery56router.get('/api/feed/explore', async (req, res) => {57 const cursor = req.query.cursor ? parseInt(req.query.cursor) : null;58 const conditions = cursor ? [lt(posts.id, cursor)] : [];5960 const results = await db61module.exports = router;Customization ideas
Hashtag support
Parse hashtags from post content on insert using a regex, store them in a `hashtags` table with a many-to-many join, and add a `GET /api/hashtags/:tag/posts` route for hashtag feeds.
Post reposts (retweets)
Add a `repost_of_id` column to the posts table. When a user reposts, insert a new post row referencing the original. The feed query joins back to the original post to show the original author and content.
Notifications on follow and like
Add a `notifications` table and insert a row whenever someone follows you or likes your post. Add a `GET /api/notifications` route and a bell icon in the React header with an unread count badge.
Verified badges for creators
Add a `is_verified boolean` field to profiles. Admin users set this flag via an admin panel route. Render a checkmark badge next to the username in post cards and profile headers.
Common pitfalls
Pitfall: Feed query is slow with OFFSET pagination
How to avoid: Use cursor pagination: pass the last seen post's ID as a cursor, and add `WHERE id < :cursor` to the query. This keeps page 10 as fast as page 1.
Pitfall: Like counts drift out of sync with the actual likes table
How to avoid: Either use a PostgreSQL trigger to maintain the count, or always compute `SELECT COUNT(*) FROM likes WHERE post_id = ?` and avoid the denormalized column entirely for lower-traffic apps.
Pitfall: Auto-created profile fails silently on first login
How to avoid: Wrap the auto-create logic in a try/catch and use an `ON CONFLICT DO NOTHING` insert, then always query for the existing profile after the insert attempt.
Best practices
- Store image upload credentials (Cloudflare R2, AWS S3) in Replit Secrets (lock icon), never hardcoded in source files
- Use the compound index on `(author_id, created_at DESC)` — without it the timeline query does a full table scan as posts grow
- Keep like and follow counts denormalized on the parent row for fast reads, but update them in the same transaction as the like/follow insert
- Validate post content length server-side (e.g., max 280 characters) — never trust client-side validation alone
- Add the retry wrapper from `server/lib/retryDb.js` to the feed route — after 5 minutes idle, the first query reconnects to PostgreSQL, causing a 1-2 second delay
- Use Drizzle Studio (open via the Database tool in Replit) to inspect query results and test indexes without writing extra code
- Rate-limit the post creation route to prevent spam — a simple in-memory counter per user_id with a 1-minute window is enough for early-stage apps
AI prompts to try
Copy these prompts to build this project faster.
I'm building a social media feed API using Express.js and PostgreSQL with Drizzle ORM on Replit. My schema has profiles, posts, follows, likes, and comments tables. Help me write a performant timeline query that fetches posts from followed users with cursor-based pagination, joining the author's profile data. Also explain how to maintain denormalized like counts safely when multiple users like a post simultaneously.
Extend my social media feed to support notifications. When user A follows user B, or likes B's post, insert a notification row for user B. Add: notifications table (id, user_id, actor_id, type enum follow/like/comment, reference_id, is_read bool default false, created_at). Routes: GET /api/notifications (paginated, unread first), PATCH /api/notifications/:id/read, PATCH /api/notifications/read-all, GET /api/notifications/count (returns unread count). In the React frontend, add a bell icon in the header with the unread count as a badge. Mark notifications as read when the dropdown opens.
Frequently asked questions
Can I build a social feed without any coding experience using Replit?
Yes. Replit Agent generates the entire Express + PostgreSQL backend and React frontend from the prompt in step 1. You'll need to follow along with the steps to verify the schema and test the routes, but you don't need to write code from scratch.
How many users can my social feed handle on the free Replit plan?
The free plan's Autoscale deployment handles moderate traffic well — easily hundreds of concurrent users. The main limits are PostgreSQL storage (10 GB) and compute. For a community app with tens of thousands of active users, upgrade to Replit Core for more resources.
Why is my timeline slow when a user follows many people?
The timeline query does an IN clause over all followed user IDs. At 500+ follows, this can be slow without proper indexing. Make sure the compound index on `(author_id, created_at DESC)` exists. For very high follow counts (10K+), consider a fan-out-on-write architecture instead.
Should I use Autoscale or Reserved VM for a social feed?
Autoscale is the right choice for most social feeds. Traffic is variable — quiet overnight and spiky when content goes viral. Autoscale scales to zero when idle (saving cost) and spins up fast enough that the cold start is masked by network latency. Reserved VM ($6-20/month) makes sense only if you add WebSocket-based real-time features.
How do I add image uploads to posts?
The recommended pattern is a pre-signed URL flow: the client asks your Express API for a pre-signed upload URL from Cloudflare R2 or AWS S3, uploads directly from the browser to the storage provider, then sends the resulting URL to your API to attach to the post. Store the R2 or S3 credentials in Replit Secrets (lock icon in sidebar).
How do I prevent one user from following themselves?
Add a server-side check in the follow route: `if (follower_id === following_id) return res.status(400).json({ error: 'Cannot follow yourself' })`. The unique constraint handles duplicate follows but not self-follows.
Can RapidDev help me build a custom social feed?
Yes. RapidDev has built 600+ apps including custom social and community platforms. If you need advanced features like real-time notifications, content moderation, or a recommendation algorithm, book a free consultation at rapidevelopers.com.
Why does my feed return an empty array for new users?
New users follow nobody, so the timeline query returns zero posts. Show the explore feed instead as a fallback: if the follows count is zero, redirect to `GET /api/feed/explore`. You can also implement suggested follows based on popular profiles to help new users get started.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation