* feat: add filtering and sorting across library, explore, and upcoming Add comprehensive filtering and sorting to all list views on both web and mobile. Library (new dedicated page): - New /library route (web) with URL search param persistence - New Library tab (5th tab, mobile) with FlashList grid - Filters: status (multi-select), type, genre (library-only), user rating range, release year (decade presets + custom), content rating, available to stream - Sort by: title, date added, release date, popularity, user rating, TMDB rating - Text search within collection - Filter popover (web) / bottom sheet with Apply (mobile) - Remove old dashboard.library endpoint in favor of library.list Explore/Discover: - New Discover section on Explore page with inline filter controls - TMDB filters: genre (now optional), year range, min rating, sort order, original language, streaming provider - Watch provider list endpoint (WATCH_REGION env var, default "US") - Default to popular content when no filters selected Upcoming: - Type filter (All/Movies/TV Shows) and status filter (All/Watching/Watchlist) - Toggle chips on both web (URL params) and mobile - Backend: mediaType and statusFilter params on upcoming endpoint Navigation: - Library added to web nav bar (desktop + mobile tab bar) - Library added as 5th tab on mobile (between Home and Explore) - Dashboard library section simplified to compact preview with "See all" link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add user streaming platform subscriptions and redesign filter UI Replace the denormalized availabilityOffers table with a normalized platforms + titleAvailability + userPlatforms schema. Users can now declare which streaming services they subscribe to, enabling personalized "On your services" availability display and filtered library/explore results. Backend: new platforms table seeded from TMDB provider data on startup, new API endpoints (platforms.list, account.platforms, account.updatePlatforms), title detail annotates availability with isUserSubscribed flag, library "on my services" filter checks user's subscribed platforms. Web UI: post-registration onboarding page for platform selection, streaming services settings section, title detail split into "On your services" / "Also available on", explore dropdown uses platforms table with active filter highlighting. Library filters redesigned from cluttered popover to clean collapsible inline strip with consolidated dropdowns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address race conditions, stale state, and edge cases in platforms feature - Fix debounced autosave race in streaming services settings: use ref for latest selection + counter guard so only the most recent save updates UI. Clean up timers on unmount. - Fix explore provider dropdown desyncing from query filter when switching Movie/TV type: reset selectedPlatformId alongside providerId. - Fix platformIdsExist rejecting duplicate valid IDs by deduplicating input before comparing against DB results. - Fix stale platform metadata: always upsert name/logo on TMDB refresh instead of skipping when platform already exists. - Fix invisible ratingMax filter: clear ratingMax when user changes ratingMin since the UI only exposes a single rating dropdown now. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clear stale saved-state timer and preserve falsy filter values - Clear previous savedTimerRef timeout before scheduling a new one in streaming-services-section, preventing an older timer from hiding the "Saved" indicator too early on rapid successive saves - Replace `value || undefined` with explicit empty-value check in library filter handler so numeric 0 (e.g. ratingMin=0) is not incorrectly dropped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add TanStack Form and migrate auth + change password forms Set up TanStack Form v1 composition layer with createFormHook, pre-bound field components (TextField, TextareaField, CheckboxField, SwitchField, SubmitButton), and migrate the two traditional submit-based forms: - AuthForm: replace 4 useState calls with useAppForm, use form.Field for each input while preserving motion animation layout - ChangePasswordDialog: replace 6 useState calls with useAppForm + Zod schema validation (cross-field password match, min length), per-field error display, form.reset() on dialog close Also adds @tanstack/react-form to the monorepo catalog and updates both web and native apps to reference it via catalog:. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sofa — Self-hosted movie & TV tracker
Sofa is a self-hosted movie and TV tracker for nerds. Track what you've watched, discover what's next, and plug your data into your existing home media stack. All without anything leaving your homelab. 🍿
What it does
- Track episode-level progress for TV series and pick shows back up from a dedicated "Continue Watching" view
- Mark movies as watched to discover more like them
- Rate titles, browse cast and crew, and get recommendations based on what you are already tracking
- Search TMDB and explore trending movies and shows without leaving your own instance
- Show streaming availability from TMDB's US provider data
- Automatically log completed watches from Plex, Jellyfin, or Emby webhooks
- Import existing watch history, ratings, and watchlists from Trakt, Simkl, or Letterboxd
- Expose your watchlist as import lists for Sonarr and Radarr
- Runs on SQLite with local image caching, built-in backups, and no external database requirement
- Supports local accounts or OIDC SSO for private instances
Note
Sofa is extremely US-centric right now, in terms of streaming providers, content rating systems, etc. Contributions to address this are more than welcome!
Quick start
Self-hosted
A minimal docker-compose.yml is provided in this repo. For most setups, the shortest path is:
- Copy the example environment file:
cp .env.example .env
- Fill in the required values:
TMDB_API_READ_ACCESS_TOKEN=your_tmdb_read_access_token
BETTER_AUTH_SECRET=generate_a_long_random_secret
BETTER_AUTH_URL=http://localhost:3000
- Start the container:
docker compose up -d
- Open
http://localhost:3000and create the first account. The first account becomes the admin automatically, and registration closes after that by default.
The included Compose file uses
ghcr.io/jakejarvis/sofa:edge. If you prefer to pin releases, switch to a published version tag likeghcr.io/jakejarvis/sofa:<version>or use the matchingjakejarvis/sofa:<version>image on Docker Hub. Multi-arch images are published forlinux/amd64andlinux/arm64.
Cloud
If you prefer to deploy Sofa to the cloud, there are several great options. All you'll need is the ability to run Docker containers and mount some form of persistent storage volume to /data within the container.
Required setup
TMDB
Sofa uses TMDB for metadata, posters, cast, recommendations, and streaming availability. You need a TMDB API Read Access Token before the app can do anything useful.
Create one here:
Tip
We want the API Read Access Token, not the shorter API key.
Auth secret
BETTER_AUTH_SECRET should be a long random string. To generate one:
npx @better-auth/cli@latest secret
# or
openssl rand -base64 32
Public URL
Set BETTER_AUTH_URL to the real external URL of your instance. This especially matters for login flows and OIDC callbacks behind a reverse proxy (like nginx or Traefik).
Configuration
| Variable | Required | Notes |
|---|---|---|
TMDB_API_READ_ACCESS_TOKEN |
Yes | TMDB metadata and discovery |
BETTER_AUTH_SECRET |
Yes | Session and auth secret |
BETTER_AUTH_URL |
Yes | Public base URL of the app |
DATA_DIR |
No | Root data directory. Defaults to /data in the container |
IMAGE_CACHE_ENABLED |
No | Defaults to enabled. Set to false to use TMDB images directly instead of caching them locally |
LOG_LEVEL |
No | error, warn, info, or debug |
OIDC_CLIENT_ID |
No | Enable OIDC when set with the matching secret and issuer |
OIDC_CLIENT_SECRET |
No | OIDC client secret |
OIDC_ISSUER_URL |
No | OIDC issuer URL |
OIDC_PROVIDER_NAME |
No | Login button label. Defaults to SSO |
OIDC_AUTO_REGISTER |
No | Defaults to true |
DISABLE_PASSWORD_LOGIN |
No | Set to true to hide email/password login when OIDC is configured |
See .env.example for the full list.
Integrations
Sofa ships with two kinds of integrations (for now): incoming watch activity and outgoing import lists.
Incoming watch activity
- Plex: logs completed watches through a webhook URL generated in Sofa. Requires an active Plex Pass license.
- Jellyfin: works through the Jellyfin Webhook plugin.
- Emby: logs completed watches through webhooks. Requires Emby Server 4.7.9+ and an Emby Premiere subscription.
These integrations are user-specific, so each user can connect their own media server account and watch history.
Outgoing import lists
- Sonarr: expose your Sofa TV watchlist as a custom import list
- Radarr: expose your Sofa movie watchlist as a custom import list
Development
For local development:
bun install
cp .env.example .env
bun run dev
Useful commands:
bun run testbun run lintbun run formatbun run check-typescd packages/db && bun run db:generatecd packages/db && bun run db:migrate
TMDB notice
This product uses the TMDB API but is not endorsed or certified by TMDB.
