Episode 42 builds a social media platform: user profiles and relationships, post creation with media upload, comments and reactions, following- followers, a paginated timeline feed, real-time notifications, chat, hashtags, and feed performance strategies at scale.

Second project in the real-world phase: a social media platform with complete, real-time features. This is one of the most challenging app types because of the combination of complex social data, media uploads, large-scale feeds, and instant notifications.
Episode 42 builds it layer by layer: core social features, real-time features, advanced functionality, and feed performance strategies.
Social relation models in Prisma:
model User {
id Int @id @default(autoincrement())
username String @unique
displayName String
avatarUrl String?
followers Follow[]
following Follow[]
}
model Follow {
followerId Int
followingId Int
createdAt DateTime @default(now())
@@id([followerId, followingId])
}Relationships follow the pattern: Follow as a join table with a composite primary key. The followers and following queries use pagination (episode 12) because they can be large.
Posts with media use file upload (episode 17):
type Mutation {
createPost(input: CreatePostInput!, media: [Upload!]): Post!
}
input CreatePostInput {
body: String!
hashtags: [String!]
}Media is uploaded to object storage (S3), and its URL is stored on the post. Type and file-size validation are applied before upload — following the security checklist from episode 17.
Comments are a tree (a comment can reply to another comment), and reactions use an enum:
type Comment {
id: ID!
body: String!
parent: Comment
replies: CommentConnection!
}
enum ReactionType {
LIKE
LOVE
LAUGH
SAD
ANGRY
}
type Reaction {
type: ReactionType!
count: Int!
}Nested comments are depth-limited for performance; reactions are aggregated with group-by queries.
Live notifications use a per-user subscription:
type Subscription {
notificationReceived: Notification!
}async function notifyUser(userId, notification) {
await pubsub.publish(`NOTIFICATION:${userId}`, { notificationReceived: notification });
}Online presence uses the presence pattern from episode 38: online status is published and listened to by other relevant users.
Real-time chat uses a per-room topic (episode 16). Live comment updates can use a per-post subscription — every new comment is published to that post's topic. For high traffic, limit frequency or use short aggregation (for example updating every few seconds).
isPrivate on users — posts only visible to approved followers.type Query {
hashtag(tag: String!, first: Int!, after: String): PostConnection!
}Privacy is enforced with data filtering (episode 14): the feed resolver filters posts by isPrivate and follow relationships — a user must not see private content from someone they don't follow.
The feed is a classic social media problem. Common strategies:
const followers = await ctx.db.follows.findMany({ where: { followingId: userId } });
for (const f of followers) {
await ctx.redis.lpush(`feed:${f.followerId}`, postId);
}Choose the strategy based on scale; start with the simple fan-out on read, then evolve.
LPUSH feed:123 postId in the feed pattern.authorId, createdAt, and the join table.A cursor-paginated feed (episode 12) combined with caching makes scrolling feel smooth.
Key takeaways:
In the next episode, episode 43, you'll learn about a multi-tenant SaaS project — multi-tenancy architecture with per-tenant vs shared databases, tenant context propagation, organization management with invitations and roles, billing with Stripe subscriptions, and enterprise features like SSO and audit logs. Your enterprise SaaS app will be ready!