Browse Templates/Social Network

Social Network

A complete social network database schema designed for platforms like Facebook or Twitter. Includes user accounts with detailed profiles, post creation and sharing, threaded comments, bidirectional friendship management with status tracking, and post/comment likes. Suitable for building social media applications, community platforms, or any project requiring social interactions.

PostgreSQL10 tablesSocial
social-networksocial-mediapostscommentsfriendshipslikes
Created with ERD Studio

About this schema

Friendship is mutual here, and that single fact shapes three tables. friend_requests holds the directional ask with a status; friendships holds the accepted result as a pair of user ids; blocks holds the refusal. A follow model would need only one of these — mutual consent is what forces the request and the connection to be separate things.

likes points at both post_id and comment_id, with one of them null on every row. It is the compromise that keeps a single like table serving two kinds of target: the alternative is two nearly identical tables, or a polymorphic pair with no foreign keys at all. Here the keys stay real at the cost of a column that is empty half the time.

Counts are cached on the thing being counted. posts carries like_count and comment_count, comments carries like_count, and none of them is derived at read time. A feed renders hundreds of posts at once, so each one has to arrive with its numbers already on the row rather than as two aggregates per item.

Key relationships

users → profiles
Credentials and the public page are separate rows, so the heavily-read profile never carries password_hash alongside it.
posts → post_media / comments
Media is ordered by sort_order, so a multi-image post keeps the arrangement the author chose rather than insertion order.
comments → comments (parent_id)
Self-reference for replies under a comment. One level or many is an application choice; the schema allows either.
friend_requests → friendships
Two states of one relationship. The request is directional with a status; the accepted friendship is stored as the pair.
likes → posts / comments
One table, two possible targets, exactly one of the two keys filled. Uniqueness has to be enforced per target to stop double likes.
notifications → users (user_id, actor_id)
Recipient and actor on one row, with resource_type and resource_id naming what happened without a key per content type.

Design decisions

Requests and friendships are separate tables

One table with a status column would hold both, and every friend lookup would then have to filter on that status — the most common read in the product paying for the rarest state. Separating them keeps friendships to accepted rows only, and lets a declined request be deleted or retained without touching the graph. The cost is that accepting is a delete and an insert in one transaction.

likes carries two nullable target keys

A polymorphic target_type plus target_id would be tidier and would give up foreign keys entirely. Two nullable columns keep referential integrity for both kinds of like, at the price of a check constraint — exactly one must be set — and a column that is null on every row of the other kind. With two target types that trade is worth it; with five it would not be.

visibility is a column on the post

Each post names its own audience rather than inheriting one from the author. That lets a public post and a friends-only post sit in the same timeline, and makes the feed query a filter rather than a per-author lookup. What it does not support is custom lists — a named group of friends needs its own tables, and that is the first thing to add when visibility stops being a small enum.

Blocks are their own table, not a status on friendship

A block has to work between people who were never friends, which a friendship status cannot express. Keeping blocker_id and blocked_id separate also keeps it directional — being blocked is not symmetric, and the blocked person should not be able to read it from their own friend list.

Tables in this template

usersUsers
8 cols
profilesProfiles
10 cols
postsPosts
8 cols
post_mediaPost Media
6 cols
commentsComments
8 cols
likesLikes
5 cols
friendshipsFriendships
4 cols
friend_requestsFriend Requests
6 cols
notificationsNotifications
8 cols
blocksBlocks
5 cols

Frequently asked

Should friendships store one row or two?
One row with an ordered pair keeps the data smaller; two rows make every lookup a single indexed read on user_id. This schema stores the pair once, so queries have to check both columns — the right choice depends on whether reads or writes dominate your workload.
How do I stop someone liking the same post twice?
Unique constraints on user_id with post_id and on user_id with comment_id, each one partial so the null rows do not collide. Without them a double tap inflates like_count with nothing to object.
Why cache like_count instead of counting likes?
Because a feed renders many posts in one query. Counting per post turns a single read into an aggregate per item, which is the slowest part of any timeline — the column is the cache and the likes table remains the record.
How does visibility interact with blocks?
They are independent and both have to be checked. A public post is still hidden from someone the author blocked, so the feed query filters on visibility and excludes blocked pairs — the schema stores both facts but enforces neither.
Where does a shared or reposted item live?
Nowhere in this schema. A repost needs posts to reference another post, usually as a nullable shared_post_id, and the feed then has to render the original inline — a deliberate omission to keep the post table single-purpose.