One Catalog for Everything

Back to Projects
Collaborative ProjectTeam of ThreeLive Product

Catalogie

Track everything you watch, play & read - movies, TV series, games and books in one catalog, on a self-hosted PERN stack we build together and keep shipping.

Live at catalogie.comOngoing · currently v1.2.0
Catalogie - one catalog for movies, TV, games and books
My Role
Design &
Engineering
Team
Collaborative
3 People
Type
Media Tracker
Web App + PWA
Stack
React · Express 5
PostgreSQL · TS
Status
Live & Ongoing
Since Jun 2026

Catalogie is a media tracker that puts movies, TV series, games and books in a single catalog - logged at the unit each medium actually deserves: the episode, the watch, the hour played, the page. It is a real product with real users, built and operated by three of us, running on our own hardware behind a TypeScript API, and it is still being built - six versions have shipped since launch and the roadmap is public. This case study covers both halves: the app at app.catalogie.com and the marketing site at catalogie.com.

01 --The Challenge

Four Apps, One Media Life

Anyone who consumes a reasonable amount of media ends up running four systems at once: a film diary in one app, a game backlog in a spreadsheet, a reading list in a third place, and TV they simply try to remember. None of them talk to each other, none of them can answer “what did I actually do this year”, and every one of them is a different company's idea of what your history is worth. The obvious fix - one tracker for everything - is deceptively hard, because the four media do not share a unit of progress. A series is tracked by episode across seasons. A film is a single event you may repeat. A game is an open-ended pile of hours. A book is a page number. Force them into one schema too early and every medium ends up tracked badly.

“Four media, four different units of progress. The hard part is not storing them - it's making one interface where all four feel native.”

🧩

Product Problem

Track four media whose progress models have nothing in common, in one library, without any of them feeling like an afterthought bolted onto a TV app.

🔌

Data Problem

No single catalog covers all four. TV and film, games, and books each come from different providers with their own auth, quotas and licence terms - and none of their keys may ever reach a browser.

🏗️

Platform Problem

A tracker only earns trust if the history survives. That meant real accounts, a real database, backups, and infrastructure we own - not a prototype with everything in localStorage.

👥

Team Problem

Three people on one codebase, plus a native mobile app being built in a separate repo - so the API had to be a documented contract, not an internal detail of the web client.

02 --The Team

A Collaborative Build

Catalogie began as my idea, and it is now designed, built and looked after by three of us - the credit sits inside the app itself under Settings → Made by. I own the product direction and work across the whole stack: the UI design, the React client, and the TypeScript backend from schema to deployment. I share that engineering surface with Nimna Niwarthana, while Thaanu Perera builds the native mobile app against the same API in a separate repository. That third consumer is the reason the backend was designed API-first from day one.

Ruchira Edirisinghe
Concept · Design · Engineering

That's me - Catalogie started as my idea. Product and UI design, the React client, the backend API and deployment.

You're on my portfolio
Nimna Niwarthana
Design & Engineering

Client, API and infrastructure, plus the admin panel.

Portfolio ↗
Thaanu Perera
Mobile Development

The native iOS and Android apps, on the same API.

LinkedIn ↗

How Three People Share One Codebase

With more than one person shipping daily, process is not bureaucracy - it is the only thing keeping a live product stable. Work flows from a personal branch to dev to main, where every merge is reviewed and main is the release branch. Two CI jobs gate every push - lint, typecheck, test and build for the server, the same for the client - plus a security job running npm audit and a gitleaks secret scan over full history. The architecture lives in a committed plan document and every decision that deviates from it is written into a decision log, so nobody has to reconstruct why something was built a certain way six weeks later.

🌿
Personal Branch
🔀
dev + Review
CI Gates
📦
main → GHCR
🚀
Deploy + Diary
📓

Every release also ships a dev diary - a plain-English account of what changed, including the bugs we got wrong. It is published on the marketing site and is a deliberate part of the process: if it shipped, it gets written up.

03 --Architecture

Stack & Architecture

Catalogie runs on the PERN stack - PostgreSQL 17, Express 5, React 18, Node 24 - with the entire server written in strict TypeScript. It is a modular monolith: one Express process, fourteen feature modules, and background jobs in-process, because at this scale microservices would only add operational surface. The whole thing self-hosts on an Oracle Ampere A1 VM (4 OCPU / 24 GB, Ubuntu arm64) behind Caddy, which terminates TLS and serves two vhosts: the built React SPA as static files at app.catalogie.com, and a reverse proxy to the API at api.catalogie.com. Sharing one registrable domain is what lets web auth cookies stay first-party.

A Prototype Backend vs. What Catalogie Runs

Where It Started
  • All user state in localStorage - one cleared browser and years of history are gone
  • A plain-JS Express prototype with per-media tables and no auth
  • Serverless proxies on someone else's platform, no database, no backups
Where It Runs Now
  • PostgreSQL 17 with 36 reviewed SQL migrations and nightly encrypted off-box backups
  • Strict-TypeScript Express 5 API - Zod at every boundary, ownership checks from the session
  • Self-hosted on our own VM, TLS by Caddy, deployed from CI by container image

Stack at a Glance

⚛️
Client
React 18 · Vite · PWA
SPAService WorkerInstallable

“Static build served straight off our own Caddy - and installable to a phone home screen with no app store in the loop.”

🛡️
API
Express 5 · TypeScript · Zod
14 ModulesOpenAPI Docsbetter-auth

“Routers parse, services decide, repos query. No any, and no unvalidated request body ever reaches business logic.”

🐘
Data & Jobs
PostgreSQL 17 · Drizzle · pg-boss
36 MigrationsCron in PostgresNo Redis

“The queue lives in the database we already run - one less service to operate, and every job is idempotent and retried.”

The Progress Model - Four Media, One Library

The schema decision that makes the whole product work is refusing to flatten the four media into one progress column. A single library_entries row holds what is genuinely shared - the title, the status, favourite, rating, notes - with a uniqueness constraint of one entry per user per title. Everything that differs lives in its own append-only event table, which is also what makes rewatches, replays and re-reads possible instead of a single boolean.

📺

TV Series

Unit · Episode

An episode_watches row per viewing - append-only, so a rewatch is another row, not an overwrite. Season bulk-marking, per-episode ratings.

🎬

Movies

Unit · Watch Event

title_watches records each viewing with its date, so watch count is a COUNT(*) and your history has a timeline.

🎮

Games

Unit · Hours Played

play_sessions logs hours against a date with an optional note. Total playtime is a sum, so a backlog becomes measurable.

📖

Books

Unit · Pages

reading_logs plus a current page on the entry - progress is a percentage of a real page count, not a guess.

💡

All four event tables are unioned into one SQL view, activity_events. That single view is what powers the streak counter, the activity heatmap and the yearly wrap - so finishing a book and beating a game feed the same streak, which is the entire point of one catalog.

Catalog Integrations - Federated Metadata, Zero Keys in the Browser

No single metadata source covers movies, TV, games and books, so Catalogie federates several third-party catalogs behind one internal interface - and every one of them is called from the server. Keys live only in environment variables, outbound calls go to a fixed host allowlist (so a client-supplied URL can never trigger a request), each has a 10-second timeout and one retry, and responses are cached to respect quotas that are a shared, finite resource for a live product. Each provider sits behind its own typed client module, so swapping one out is a single file.

Covers
Auth Model
Quota Strategy
📺 Screen Catalog
Series, seasons, episodes and films - the spine of the catalog.
Server-side login exchanged for a token, cached and refreshed well before its expiry.
A 10-minute LRU on every response, and one silent retry when a token goes stale.
🎮 Games Catalog
Games, platforms, studios and artwork, with a second source filling the two gaps the first leaves - critic score and average playtime.
OAuth client-credentials token cached for its full lifetime; the supplementary source is matched by title and release year.
A limiter holds requests under the published rate ceiling, backs off once when throttled, and caches for 10 minutes. The enrichment source is optional - no key, no enrichment, and the page renders exactly as before.
📚 Books · ⭐ Ratings · 📅 Releases
Book records and cover art, external critic and audience scores, and upcoming release dates for the calendar.
Keys server-side only, each behind its own typed client module.
The tightest daily quota is cached in Postgres with a 24-hour TTL and coalesced requests; release data merges best-effort, so the calendar never breaks when a source is down.

Security & Operations

🔐

Auth Built for Two Clients

better-auth handles email + password and Google sign-in with DB-backed sessions. The web client uses first-party SameSite=Lax cookies; the native app uses the bearer plugin to get the same session as a token - because a native app has no browser origin and never sees a cookie.

🧪

Tests Ship With the Feature

Integration-first with Vitest and Supertest against a real Postgres, never mocks of our own SQL. 62 server suites and 20 client suites, plus an authz matrix that asserts user A cannot touch user B's entries, comments or uploads. Both are required CI jobs.

🖼️

Uploads Treated as Hostile

Avatars and comment attachments are validated by magic bytes rather than MIME or extension, re-encoded through sharp to strip EXIF and any embedded payload, stored under random UUIDs, and served with nosniff from a path that cannot execute anything.

💾

Backups & Restore Drills

Nightly pg_dump shipped off-box to object storage with 30-day retention, uploads rsynced weekly - and a monthly restore drill, because a backup nobody has restored is a hypothesis, not a backup.

⏱️

Background Jobs

pg-boss runs the cron in Postgres: episode sync at 03:00, release dates at 04:00, ratings refresh at 04:30 within a daily quota budget, weekly upload cleanup, an email outbox drained every two minutes and push delivery every minute.

📊

Measured, Not Guessed

Load testing pinned the API to one core and found the ceiling was Node CPU, not Postgres. The fixes followed the measurement - trimming the heaviest field from the library list payload and compressing responses - rather than the reflex of adding a cache layer.

04 --The Product

Covering Almost Every Tracking Need

The goal was never “a TV tracker that also does books”. It was to answer every question a person actually asks about their own media life - and to answer all of them in one place. Working through those questions one at a time is how the feature set was decided, and it is why the app now covers close to the whole surface of what a tracker is asked to do.

01
What am I tracking?

One library across all four media, with per-medium statuses that use the right words - watching / watchlist for TV, playing / wish for games, reading / want for books - plus favourites and custom filters.

02
Where did I stop?

Progress at the natural unit: the episode inside its season, the page inside the book, the hours inside the playthrough. Bulk-mark a whole season, or log a session with the date it happened.

03
What did I think of it?

Rate on your own scale - five stars with halves or a ten-point scale, chosen independently per medium - and rate individual episodes. The number on disk is always canonical, so switching scale is presentation, never a migration.

04
What did everyone else think?

External critic and audience scores sit beside a Catalogie community average aggregated anonymously from members' own ratings - shown in whichever scale you prefer.

05
What's next?

A watchlist, backlog and TBR pile in one queue, a release calendar of what is coming, and opt-in push notifications so a new season or a shipped game finds you instead of the other way round.

06
What have I actually done?

Time counted per medium, a genre fingerprint, an activity heatmap with current and longest streaks, 35+ milestone badges in tiers, and a Yearly Wrap for any year you pick.

07
Who else is watching this?

Follow other people, keep a profile you control down to a single title, and leave threaded comments with reactions and image attachments - on a title or on a specific episode.

08
I already have years of history.

Importers bring an existing watch history across rather than asking anyone to start from zero - parsed, matched against the catalog and reviewed before anything lands in the library.

09
The catalog is missing my book.

Describe it once and, after a moderation pass, it becomes a real shared entry everyone can track - and you get told when yours goes in. The catalog grows from its readers.

Where It Deliberately Stops

“Almost all” is the honest claim, not “all”. Music and podcasts are not tracked yet, native iOS and Android apps are in build rather than shipped, and recommendations are drawn from what you finish and rate rather than anything more sophisticated. Those are on the roadmap in the open - the feature request path is a support email that goes straight onto the list, which is how several shipped features got there.

🔒

Two product promises shape everything above: Catalogie is free to use, forever - no tier, no card, no trial clock - and your catalog is private by default, shareable down to a single title, with export always free. The first 1,000 accounts become Founding Members with a permanent badge.

The App, Screen by Screen

The live app across its main surfaces - the dashboard and profile, Discover across all four media, the movie watchlist, the games playlist with hours played, the reading list, and sign-in. Click any screen to open it full-size.

05 --The Site

The Front Door - catalogie.com

The second half of the project is a separate repository: a one-page marketing site that has to sell a tracker to someone who has never heard of one. It is React 18 and TypeScript on Vite, animated with Framer Motion - scroll reveals, staggered entrances, magnetic buttons, count-up statistics, animated charts - over Lenis smooth scrolling, with a scroll progress bar and a navbar that highlights whichever section is in view. There is no UI kit and no icon package: every icon is inline SVG and the donut, genre bars and activity heatmap are hand-built in SVG and CSS. It deploys to GitHub Pages from a workflow on every merge.

✍️

Copy in One File

Every word on the site - headlines, features, FAQ, roadmap, legal, dev diaries - lives in a single typed content module. Rewriting the pitch never means touching a component, which is what makes the messaging fast to iterate.

🎨

An Accent Per Medium

The brand gold carries the CTAs, and each tracked medium owns a hue - amber for movies, violet for TV, green for games, rose for books. The same four accents run through the app, so the site and the product read as one system.

🔢

A Live Founding Counter

The Founding Member section is not decorative - it calls the production API for the real remaining count, so the scarcity on the page is the actual number of spots left.

📜

Hidden, Not Deleted

Catalogie is presented as free forever, but the earlier tiered-pricing copy is commented out in place rather than removed - so if paid plans ever arrive, the plan grid and its FAQ answers come back as they were written.

Talina
Talina · TitlesMontserrat · NumbersInter · BodyInstrument Serif · Accent
Color

A cinematic near-black base with a single brand gold for anything actionable, and one accent per tracked medium so a glance at a card tells you what kind of thing it is before you read a word.

#F5C451
Brand Gold
#F5A623
Movies
#A78BFA
TV Series
#34D399
Games
#FB7185
Books
#0A0A0B
Base

The Site, Section by Section

Captured live from catalogie.com - the hero, Founding Members, the feature grid, the dashboard story, free-forever, the roadmap, the FAQ, the dev diaries, and the mobile view.

06 --Shipping

An Ongoing Project

This is the part that makes Catalogie different from everything else in this portfolio: it is not finished, and it is not meant to be. It is a live product with real accounts on it, and new versions go out regularly - six of them since launch, each with a public dev diary explaining what changed. The section below is a snapshot, not a conclusion; it grows every time we ship.

6 releases · slide to browse
v1.2.0Release25 July 2026

Books from readers, sharper ratings, dozens of new badges

  • Reader-contributed books - describe a missing title once and, after moderation, it becomes a shared catalogue entry everyone can track
  • Rate your way: 5 stars with halves or a 10-point scale, set independently per medium, with the Catalogie community average shown beside the external score
  • Per-episode ratings, each with its own external score on the still
  • Milestone badges grew from 6 to 35+ in tiers, and Yearly Wraps was rebuilt around any year you pick
  • A still-airing series now sits under "More episodes coming" instead of being filed away as finished
v1.1.3Patch17 July 2026

A faster library, clearer messages and performance work

  • Fixed a future-dated game session that could take the whole dashboard down with it
  • Library and follower counts refresh as you move through the app - no reload to see a change made on another device
  • Movie watch dates surfaced at last: they had been recorded for years and simply never shown
  • Posters load as you scroll, the library ships about half the payload it used to, and book pages return faster
v1.1.2Patch16 June 2026

A clearer sign-in, steadier rendering and the dev diaries

  • Sign-in and create-account now state the Terms and Privacy Policy, both linked, with a way back to the homepage
  • Fixed a bug where slow or blocked web fonts could briefly scramble a page - fonts are now self-hosted
  • Shipped the standalone Dev Diaries page that documents every release
v1.1.1Patch15 June 2026

Sign-up safety, a friendlier date picker and polish

  • Closed a sign-up bug that could permanently lock someone out of their own account
  • Unverified sign-ins auto-resend a fresh verification email if one goes missing
  • A new date picker for logging watch dates, and heatmaps that open on your most recent week
v1.1.0Release14 June 2026

Following, notifications and a faster library

  • Follow other people and shape a public profile you control down to a single title
  • Comments and reactions on the titles you track
  • Opt-in push notifications, and Founding Member badges for the first 1,000 accounts
v1.0.0Launch13 June 2026

Catalogie is live - one home for everything you track

  • Movies, TV, games and books in a single library
  • Progress by episode, chapter, page or hours played
  • A dashboard with streaks, genres and time counted
  • Accounts with email or Google sign-in, private by default

What Ships Next

📱

Native Apps

iOS and Android against the same API and account, with offline logging and push reminders. Already the reason the API speaks bearer tokens.

🌍

A Second Region

A UK VM plus a CDN in front, designed before it is built - the scaling path is documented rather than improvised.

📥

More Importers

Every tracker people are leaving behind is another parser and matcher, so nobody has to retype a decade of history.

🗳️

Requested Features

A support email goes straight onto the roadmap, and Founding Members get a direct line into what gets built next.

07 --Results & Reflection

Outcome & Impact

Key Outcomes

0
Movies, TV, games and books tracked in a single library
0
Shipped since launch, each with a public dev diary
0
Feature modules behind one documented TypeScript API
0
Designing, engineering and operating it together

“A film diary, a game spreadsheet, a reading list and a season you half-remember - replaced by one catalog that is still being built.

📈 Outcome

A live, self-hosted media tracker with real accounts on it: four media in one library, progress at four different units, ratings on your own scale, a social layer, an analytics dashboard, importers, a release calendar and push notifications - behind a strict-TypeScript API with 36 migrations, an authz test matrix and nightly off-box backups.

🧗 Challenge

Keeping a deployed product working while rebuilding its entire backend underneath it. Every commit had to leave the live site functional, which meant the new API grew module by module beside the old one and only took over at cutover - no big-bang rewrite, no weekend of downtime.

💡 What I Learned

Shared external quotas are production infrastructure. Burning a catalog API key during testing took a real feature down for real users, and the rule that came out of it - test against internal endpoints, stub the catalog, keep dev keys separate from production - is now written into the project's working rules.

🚀 Next Steps

Ship the native iOS and Android apps against the existing API, widen the importers, add a second region behind a CDN once there are users to justify it, and keep the release cadence - every version with a diary that explains, in plain language, what changed and what we got wrong.