The NestJS backend for an Instagram-style social network for dogs — feed, Reels, 24h Stories, a full social graph and push — designed with keyset pagination and 37 database indexes to scale toward 1M+ users.

01 — Overview
Frizbee is the backend for an Instagram-style social platform built for dogs: a photo / Reels feed, ephemeral 24-hour Stories, a full social graph (followers, mutual friends, blocks), “Wolf Pack” communities, reactions and threaded comments, and push notifications. It's a NestJS 11 + Prisma + PostgreSQL API with Redis, BullMQ, an S3 / CloudFront media layer and an FFmpeg HLS transcode pipeline.
Role
Timeline
Stack
02 — Context
A social feed has to stay fast as data grows into the millions of rows: offset pagination collapses, naïve queries trigger N+1 explosions, ephemeral content must expire reliably, media can't be pushed through the API, and blocks / privacy must be enforced on every read — all without slowing the hot path.
I designed the data model and queries for scale from day one. The feed uses keyset (cursor) pagination backed by composite (createdAt DESC, id DESC) indexes — 37 indexes and uniques across 24 models — so feed reads stay O(limit) regardless of table size. Viewer reactions and context are batch-loaded to kill N+1s; hot signals (Pick-of-the-Litter, viral cutoff) are precomputed off the request path and cached in Redis. Three independent BullMQ queues (media, notifications, push) isolate retry domains, an hourly cron expires Stories in bounded batches, and media uploads go direct to S3 via presigned URLs with an FFmpeg HLS transcode worker that streams from S3 to stay memory-light. Engineered to scale toward 1M+ users.
03 — Showcase



04 — Capabilities
05 — Contribution
As Backend Engineer, here is exactly what I owned and delivered on this project.
06 — Engineering
Challenge
Feed pagination has to stay fast at millions of rows.
Solution
Keyset (cursor) pagination with take limit+1 over composite (createdAt DESC, id DESC) indexes — O(limit) reads instead of offset scans that degrade with depth.
Challenge
Rendering a feed page risked an N+1 storm of per-post reaction lookups.
Solution
Batch-loaded the viewer's reactions for the whole page in one query (postId IN …) and loaded block / friend context once via Promise.all.
Challenge
Ephemeral Stories must reliably disappear without exhausting memory.
Solution
An hourly cron deletes expired stories in bounded pages (100×100), removing S3 objects before DB rows, backed by an expiresAt index.
Challenge
A Firebase outage shouldn't corrupt notification state or double-send pushes.
Solution
Split push into its own BullMQ queue with per-device jobs, idempotency via a pushSentAt marker, and dead-token pruning.
07 — Toolbox
08 — Impact