Netflix

Full database schema for a Netflix-like video streaming platform with content management, multi-profile support, subscription billing, watch history, ratings, recommendations engine, and download features.

PostgreSQL15 tablesWeb App
streamingvideosubscriptioncontentrecommendation
Created with ERD Studio

About this schema

The distinction that shapes this schema is between an account and a viewer. users pays and signs in; profiles is who is watching. Everything personal — history, watchlist, ratings, downloads — hangs off a profile, which is why one household's recommendations do not bleed into another's.

The catalogue is one table with a type, not two. content holds both films and series, and seasons and episodes hang off it only when the type calls for them. A film is a content row with no seasons; a series is a content row with them. Searching, rating and adding to a watchlist all work on content and never need to know which kind it is.

watch_history points at both content and episode, and that is what makes resume work for a series. The content_id answers 'what are you watching', the episode_id answers 'where', and progress_seconds answers 'how far'.

Key relationships

users → profiles
The account pays, the profile watches. max_profiles on the plan is the limit, and is_kids and maturity_level are per profile so a child's account restricts content without a separate login.
content → seasons → episodes
Only series use these. A film is a content row with a duration and no seasons, which is why duration_minutes sits on both content and episodes.
profiles → watch_history
One row per profile per thing watched, holding progress_seconds. Updated in place, so resume is a lookup rather than a scan of a view log.
content ↔ people via content_cast
role and character_name sit on the junction because a person's relationship to a title is what varies — the same actor directs one film and appears in another.
profiles → downloads
expires_at is on the download, not the licence, because an offline copy is time-limited by the agreement with the studio and has to disappear on its own.

Design decisions

Films and series share one catalogue table

The alternative is a films table and a series table, which doubles every catalogue query — search, genres, watchlist, ratings all become unions. A type column plus seasons and episodes that only series use keeps the catalogue one thing. The cost is that duration_minutes on content means nothing for a series, which is a nullable column against a duplicated query surface.

Personal data belongs to the profile, not the account

If history and ratings lived on users, a shared household would get one blended taste profile and one resume point. Putting them on profiles is the entire point of profiles existing. The consequence is that deleting a profile deletes its history, which is what people expect when they delete a profile.

Cast credits carry the role

content_cast holds role, character_name and billing_order rather than separate actors and directors tables. The same person appears in different capacities on different titles, so the capacity is a property of the credit. billing_order is stored because the order on a title page is an editorial decision, not alphabetical.

Plan limits live on the plan

max_profiles, max_streams and video_quality sit on subscription_plans rather than being hardcoded. Changing what a tier includes is then a row update, and a subscription that points at the old plan row keeps the old terms until it is moved.

Tables in this template

usersUsers
9 cols
profilesProfiles
9 cols
subscription_plansSubscription Plans
8 cols
subscriptionsSubscriptions
10 cols
genresGenres
5 cols
contentContent
14 cols
content_genresContent Genres
5 cols
seasonsSeasons
8 cols
episodesEpisodes
10 cols
watch_historyWatch History
9 cols
watchlistWatchlist
6 cols
ratingsRatings
7 cols
peoplePeople
8 cols
content_castContent Cast
7 cols
downloadsDownloads
9 cols

Frequently asked

Why are profiles separate from users?
Because a household shares an account but not a taste. Profiles are what let four people have four resume points, four watchlists and four sets of recommendations under one payment, and what let a kids' profile have its own maturity limit without a second login.
How does resume work for a series?
watch_history has both content_id and episode_id. The content row says which series, the episode row says which episode, and progress_seconds says how far into it. Continuing a series means reading the most recent row for that content.
Should films and series really share a table?
Yes, unless almost nothing about them is shared. Here they share search, genres, cast, ratings, watchlist and artwork — six things — and differ in one, which is whether they have episodes. That ratio is what decides it.
Where does 'because you watched' come from?
Not from this schema directly. watch_history, ratings and content_genres are the inputs; the recommendation itself is computed elsewhere and cached, because a join across a profile's whole history is not a page-load query.
Why does downloads have an expires_at?
Offline rights are time-limited by licensing, so a downloaded file has to stop working on its own. Storing the expiry here lets the client enforce it without asking the server, which is the point of a download.