StreamLog logoStreamLog

Track movies, TV shows, and anime together with your partner.

Product

About StreamLog

Shared movie watchlist for couples

Documentation

Technical docs for developers

Blog

Movie night ideas & guides

Connect

Report & Feedback

Bug reports, feature ideas and questions

Support the project

Help keep StreamLog free with a donation

Legal

Terms of ServicePrivacy Policy

Β© 2026 StreamLog. All rights reserved.

Made with 🍿 & β™₯ by @Soumik and @Tani

StreamLog logoStreamLogSign in

Technical Documentation

How StreamLog is built β€” architecture, data model, API, and operations. StreamLog is a free shared movie, TV, and anime watchlist; this page documents the system for developers and contributors.

Contents

  1. 1. Overview & tech stack
  2. 2. Getting started
  3. 3. Project structure
  4. 4. Data model
  5. 5. Authentication & sessions
  6. 6. API reference
  7. 7. Upstream APIs & caching
  8. 8. Rate limiting
  9. 9. Theming
  10. 10. Testing
  11. 11. SEO
  12. 12. Deployment

1. Overview & tech stack

StreamLog is a server-rendered Next.js application. Pages render on the server (mostly static or short-revalidate), while the authenticated app talks to a REST API under /api backed by PostgreSQL through Prisma. Expensive third-party media lookups are cached in memory and in a database table, and every mutating endpoint sits behind a distributed rate limiter.

LayerTechnology
FrameworkNext.js 16 (App Router) Β· React 19 Β· TypeScript 5
StylingTailwind CSS v4 Β· shadcn/ui primitives Β· @base-ui/react Β· Phosphor icons
DatabasePostgreSQL via Prisma 7 (@prisma/adapter-pg)
AuthFirebase Authentication (email/password) + HMAC-signed session cookies
Rate limitingUpstash Redis sliding window (@upstash/ratelimit), fail-open
Media dataTMDB Β· OMDb Β· AniList Β· Jikan
TestingVitest (unit)
HostingVercel Β· Vercel Analytics

2. Getting started

Prerequisites: Node.js 20+, a PostgreSQL database, a Firebase project with email/password auth enabled, and TMDB/OMDb API keys.

# install dependencies
pnpm install

# apply database migrations
pnpm db:migrate

# start the dev server
pnpm dev

# other scripts
pnpm lint        # eslint
pnpm test        # vitest run
pnpm build       # prisma generate && next build
pnpm db:studio   # prisma studio

Environment variables

VariableRequiredPurpose
DATABASE_URLYesPostgreSQL connection string used by Prisma.
SESSION_SECRETProductionHMAC-SHA256 key for signing session cookies. AUTH_SECRET is accepted as a fallback; unset in production throws at runtime.
TMDB_API_KEYYesPrimary TMDB v3 API key for search, details, discover, and watch providers.
TMDB_API_KEY_2OptionalSecond TMDB key used as automatic failover when the first is missing or rate-limited.
TMDB_REGIONOptionalRegion code for TMDB watch-provider lookups.
OMDB_API_KEYOptionalOMDb key for IMDb ratings and the movie/TV search fallback path.
NEXT_PUBLIC_FIREBASE_API_KEYYesFirebase Web SDK config (all six NEXT_PUBLIC_FIREBASE_* values come from the Firebase console).
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYesFirebase auth domain.
NEXT_PUBLIC_FIREBASE_PROJECT_IDYesFirebase project id.
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKETYesFirebase storage bucket.
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_IDYesFirebase sender id.
NEXT_PUBLIC_FIREBASE_APP_IDYesFirebase app id.

3. Project structure

src/
β”œβ”€ app/                  # App Router pages & API routes
β”‚  β”œβ”€ (app)/             # Authenticated shell (dashboard, lists)
β”‚  β”œβ”€ api/               # REST endpoints (see Β§6)
β”‚  β”œβ”€ about|blog|docs|terms|privacy/   # Public static pages
β”‚  β”œβ”€ layout.tsx         # Root layout, fonts, theme init script
β”‚  β”œβ”€ sitemap.ts         # Generated sitemap
β”‚  └─ robots.ts
β”œβ”€ components/
β”‚  β”œβ”€ ui/                # Design-system primitives (button, dialog…)
β”‚  └─ *.tsx              # Feature components (board, dialogs, feed…)
β”œβ”€ lib/
β”‚  β”œβ”€ access.ts          # Membership/auth guards for routes
β”‚  β”œβ”€ api.ts             # Typed client-side fetch wrapper
β”‚  β”œβ”€ cache.ts           # memoWithDb: memory + MediaCache two-tier cache
β”‚  β”œβ”€ discovery.ts       # Discover layout & genre configuration
β”‚  β”œβ”€ http.ts            # JSON helpers, error envelope, zod parsing
β”‚  β”œβ”€ lists.ts           # List queries & cache invalidation
β”‚  β”œβ”€ media-details.ts   # Upstream detail merging (per-source)
β”‚  β”œβ”€ rate-limit.ts      # Upstash sliding-window limiter (fail-open)
β”‚  β”œβ”€ search-order.ts    # Cross-source result preference logic
β”‚  β”œβ”€ session.ts         # getCurrentUser/requireUser + profile caches
β”‚  β”œβ”€ theme.ts           # Client theme store (system/light/dark)
β”‚  β”œβ”€ title-dedupe.ts    # Title identity keys
β”‚  └─ firebase/          # Client SDK + server token verification
└─ generated/prisma/     # Generated Prisma client (do not edit)

4. Data model

The Prisma schema lives at prisma/schema.prisma. All child rows cascade on delete, so removing a list or user cleans up everything underneath it.

User

Account record. tokenVersion is bumped on logout so every previously issued cookie invalidates immediately β€” server-side revocation without a session table.

List

A watchlist. type is PERSONAL or SHARED; ownerId names the creator. Max 8 lists per account (enforced in routes).

ListMember

Membership join with role OWNER | MEMBER and a per-member pinnedAt for pinning. Unique on (listId, userId); shared lists cap at 3 members.

Invite

Single-use invite tokens, optionally locked to one email. Expires after 7 days; revokedAt supports manual cancellation.

Title

A tracked title in a list. Identity is (listId, source, externalId) β€” enforced by a unique index and mirrored by titleIdentity() in lib. status uses the WatchStatus enum; position orders rows.

WatchLink

Streaming links per title. auto=true links are fetched from TMDB (max 2); users can add up to 3 manual links. Unique on (titleId, label, url).

TitleProgress

Per-user season/episode progress for series and anime, plus totals cached from details and a rewatch flag. Unique on (userId, titleId).

Rating

Per-user 1–10 rating per title. Shared lists display the average of member ratings. Unique on (userId, titleId).

Note

Per-user free-text note per title (max 2,000 characters). Unique on (userId, titleId).

MediaCache

Durable cache for slow upstream responses. Written best-effort, never blocking the request path; indexed on expiresAt.

5. Authentication & sessions

  1. 1. The client authenticates against Firebase with email/password and receives an ID token.
  2. 2. POST /api/auth/session verifies the token server-side (RSA-SHA256 against Google's public keys), upserts the user row, and sets an HTTP-only cookie containing the user id and their current tokenVersion, signed with HMAC-SHA256 using SESSION_SECRET.
  3. 3. Every request resolves the user via getCurrentUser(): verify the HMAC, then confirm the cookie's tokenVersion still matches the database.
  4. 4. Signing out bumps tokenVersion, instantly invalidating every cookie ever issued for that account β€” no session table required.

To keep request handling cheap, profiles are cached in-memory for 60 seconds and token versions for 5 seconds, so a logout propagates across serverless instances within seconds. Password resets go through Firebase; changing passwords re-authenticates before updating.

6. API reference

All endpoints are JSON-in/JSON-out under /api, require the session cookie unless noted, and return errors in a common envelope ({ error: string }). The Group column is the rate-limit bucket (Β§8).

RouteMethodsGroupPurpose
/api/auth/sessionPOST Β· DELETEsessionExchange a Firebase ID token for an HTTP-only session cookie; delete it to sign out (bumps tokenVersion).
/api/meGET Β· PATCHmeRead and update the caller's profile (name, image URL).
/api/searchGETsearchUnified title search across TMDB/OMDb (movies, TV) and AniList/Jikan (anime).
/api/media/detailsGETdetailsRich metadata for one title: rating, release date, seasons/episodes, runtime, genres, overview, cast, trailer key.
/api/discoverGETdiscoverGenre-driven discovery feed backed by TMDB trending/discover endpoints.
/api/listsGET Β· POSTlistsList the caller's lists (with posters and counts) or create a new personal/shared list.
/api/lists/[id]GET Β· PATCH Β· DELETElistsFetch one list board, rename it, or delete it (owner only).
/api/lists/[id]/titlesPOSTtitlesAdd a title (deduped by source+externalId) or a custom LINK card.
/api/lists/[id]/bulkPOSTtitlesAdd multiple titles in one call.
/api/lists/[id]/duplicatePOSTlistsCopy a list into a new personal list owned by the caller; titles/statuses/positions carry over, per-member state does not.
/api/lists/[id]/pinPATCHlistsToggle the caller's per-member pin.
/api/lists/[id]/invitesPOSTinvitesCreate a single-use invite link (owner only), optionally restricted to an email.
/api/lists/[id]/invites/[inviteId]DELETEinvitesRevoke a pending invite.
/api/lists/[id]/members/[userId]PATCH Β· DELETElistsTransfer ownership or remove a member (owner only).
/api/titles/[id]PATCH Β· DELETEtitlesChange status / move metadata, or remove the title from its list.
/api/titles/[id]/ratingPOSTtitlesSet or clear the caller's 1–10 rating.
/api/titles/[id]/notePOSTtitlesSave or clear the caller's note.
/api/titles/[id]/progressPATCHtitlesSave season/episode progress for series and anime.
/api/titles/[id]/watch-linksPOST Β· DELETEtitlesAdd or remove manual watch links (max 3 per title).

7. Upstream APIs & caching

Search

Movies and TV prefer TMDB results and fall back to OMDb when TMDB returns nothing (pickSearchResults in lib/search-order.ts). Anime queries AniList first, then Jikan. A title's identity is always the pair (source, externalId), so the same movie added twice resolves to one row.

Details

getMediaDetails merges per-source payloads into one shape: TMDB supplies genres/runtime/trailer and optionally an IMDb rating via OMDb cross-lookup; AniList supplies anime scores and episode counts. Upstream failures are retried with exponential backoff and then remembered for 5 minutes to avoid hammering a struggling provider.

Two-tier cache

memoWithDb checks an in-process Map first, then the durable MediaCache table (which survives cold starts and is shared across instances), then hits the provider. Writes are best-effort and never block the response. Detail payloads live for 24 hours; watch-provider links attach automatically (up to 2) when a title is added.

8. Rate limiting

Every API request passes through a distributed sliding-window limiter backed by Upstash Redis, keyed by user id (or IP pre-auth). Limits are global across all serverless instances. The limiter is fail-open: if Redis is unreachable the request proceeds, because availability outranks hardening.

GroupLimitCovers
session10 / minLogin attempts (brute-force guard)
search30 / min/api/search
details40 / min/api/media/details
discover60 / min/api/discover
lists30 / minList CRUD, duplicate, pin, members
invites10 / minInvite create/revoke
titles60 / minTitle add/update/remove, rating, note, progress, links
me30 / minProfile read/update

When a client exceeds a limit, the API responds 429 with a Retry-After header; the UI surfaces the friendly "You're moving too fast" dialog.

9. Theming

The UI ships light and dark palettes as CSS variables (:root and .dark) consumed by Tailwind v4 semantic tokens. Users pick System default, Light, or Dark from the avatar menu; the choice persists in localStorage["streamlog-theme"].

  • An inline script in the root layout applies the resolved theme class to <html> before first paint, preventing flashes.
  • lib/theme.ts exposes a useSyncExternalStore-based store; OS scheme changes are followed live in system mode and other tabs sync via the storage event.
  • Surfaces use semantic tokens (bg-surface, border-border, ring-border) rather than hardcoded colors; scrims over poster imagery intentionally stay dark in both themes.

10. Testing

Unit tests run with Vitest (pnpm test). Coverage focuses on pure decision logic that is easy to get subtly wrong:

  • search-order β€” cross-source result preference and dedupe identity
  • rate-limit β€” pathname-to-bucket mapping and rule coverage
  • discovery β€” genre layout invariants
  • blog β€” article integrity and canonical metadata
  • http, cache, watch-providers β€” envelopes, TTL behavior, provider filtering

11. SEO

  • app/sitemap.ts emits every public page; blog slugs are derived from BLOG_ARTICLES so new posts are included automatically.
  • app/robots.ts allows all crawlers and points at the sitemap.
  • Auth-gated routes (/login, /app, /lists, /join) emit noindex metadata and stay out of the sitemap.
  • Marketing pages ship canonical URLs, Open Graph and Twitter cards, and JSON-LD structured data.

12. Deployment

  1. 1. Provision PostgreSQL (Neon, Supabase, RDS…) and set DATABASE_URL in the hosting dashboard.
  2. 2. Configure every environment variable from Β§2 β€” secrets stay out of the repo.
  3. 3. Apply migrations with pnpm db:migrate (Prisma migrate deploy) before shipping a release that changes the schema.
  4. 4. Deploy to Vercel; pnpm build runs Prisma generate automatically. Add the Upstash Redis REST credentials to enable rate limiting (it fails open without them).
  5. 5. Verify /sitemap.xml and /robots.txt after the first deploy and submit the sitemap in Google Search Console.
Read the user guide instead