* Replace server actions with API routes and migrate SWR to TanStack Query - Add 33 new REST API routes covering titles, episodes, seasons, people, dashboard, explore, integrations, admin, backups, and account operations - Replace SWR with @tanstack/react-query: add QueryProvider, api-client helper, query-client singleton, and TanStack Query hooks for titles - Migrate all 26 server actions to fetch calls against the new API routes in 10 client components (use-title-actions, title-card, command-palette, hero-banner, integration-card, account/backup/registration/update-check settings sections) - Delete lib/actions/ directory (titles.ts, watchlist.ts, people.ts, settings.ts) and lib/swr/fetcher.ts https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb * Convert pages to TanStack Query client-side data fetching Migrate dashboard, explore, settings, and people pages from server component data fetching to client-side TanStack Query hooks. Create query hook files for each domain (dashboard, explore, people, admin, integrations). Add GET routes for admin/registration and admin/update-check. Update CLAUDE.md architecture docs to reflect TanStack Query patterns. https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb * Migrate API layer from REST routes to oRPC with TanStack Query Replace ~35 hand-written REST API routes and manual TanStack Query hooks with a contract-first oRPC setup that provides end-to-end type safety from contract → server procedures → client → TanStack Query. - Add oRPC foundation: contract, context, auth/admin middleware, RPCHandler, client (browser + SSR via globalThis), TanStack Query utils - Implement ~30 procedures across 14 files (titles, episodes, seasons, people, dashboard, explore, search, discover, stats, status, integrations, admin, account, watchlist) - Migrate all client components from api()/lib/queries/ to orpc.*.queryOptions() and client.*.method() calls - Delete all replaced REST API routes, lib/queries/, lib/api-client.ts, and old hooks (use-discover, use-search, use-stats, use-system-health) - Wire up SSR optimization via instrumentation.ts - Keep non-RPC routes: auth, images, avatars, backup restore/download, webhooks, lists, health https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb * Add strict Zod output schemas to all oRPC contract procedures Replaces the permissive z.any() output schemas with precise Zod schemas for every procedure that returns data. This makes the contract fully self-documenting and enables runtime output validation. - Define ~30 output schemas in schemas.ts covering titles, people, dashboard, explore, search, discover, stats, integrations, admin, etc. - Wire all output schemas into contract.ts - Fix procedure return type mismatches (nullable fields, enum narrowing) - Align consumer types (SearchResult, IntegrationConnection) with contract https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb * Delete lib/types.ts — infer all types from Zod schemas The oRPC Zod schemas are now the single source of truth for domain types. All 17 consumer files (services, components, atoms, utils) now import inferred types (Episode, Season, ResolvedTitle, CastMember, etc.) from lib/orpc/schemas instead of hand-written interfaces in lib/types.ts. https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb * Migrate all mutations to TanStack Query useMutation Replace raw oRPC client calls with useMutation/mutateAsync via orpc.*.mutationOptions() for consistent mutation tracking across all components: title-card, command-palette, account, system-health, integrations, backups, backup-schedule, and title-actions. https://claude.ai/code/session_01L6gF2sqqe6gw5bemZUnRBb * Fix bugs in settings/explore and bump dependencies - Deduplicate backup list on create (filter by filename before prepend) - Add isError state to backup-schedule section with fallback UI - Add error check for avatar DELETE response before clearing state - Strengthen backup restore file guard with `instanceof File` - Change explore trending default type from "movie" to "all" - Pin all oRPC and TanStack Query packages to exact versions - Bump jotai 2.18.0→2.18.1, motion 12.35.1→12.35.2, shadcn 4.0.0→4.0.2, @types/node 25.3.5→25.4.0, semver 6.3.1→7.7.4 * Migrate title recommendations to client-side TanStack Query - Convert TitleRecommendations from server component with `"use cache"` to a client component using `orpc.titles.recommendations.queryOptions()` with an inline skeleton loading state - Delete recommendations-grid.tsx — merged into title-recommendations.tsx - Remove RecommendationsSkeleton from skeletons.tsx (now inline) - Drop Suspense wrapper in title detail page (client component handles its own loading state) - Include userStatuses in recommendations procedure output so the component no longer needs a separate session/tracking lookup - Remove `revalidate` option and all `updateTag`/`cacheTag`/`cacheLife` calls from refreshCredits and refreshRecommendations — no longer needed without "use cache" server components - Remove `cacheComponents: true` from next.config.ts * Replace Jotai title atoms with React Context + TanStack Query cache - Add title-context.tsx with TitleContext and useTitleContext / useTitleUserInfo hooks; useTitleUserInfo reads from orpc.titles.userInfo cache instead of atoms - Rewrite TitleProvider to seed the userInfo cache via queryClient.setQueryData on mount (unconditional overwrite prevents stale data on revisit / account switch) and provide titleId, titleType, titleName, seasons, and watchingEp via context - Rewrite use-title-actions to read/write userInfo via queryClient.getQueryData / setQueryData helpers instead of useStore + individual atoms; optimistic updates and rollbacks now operate on a single UserInfo object in the query cache - Migrate title-actions, title-keyboard-shortcuts, and title-seasons off useAtomValue / useSetAtom to the new context hooks - Delete per-title atoms from lib/atoms/title.ts * Migrate avatar and backup restore routes to oRPC procedures - Add `account.uploadAvatar` and `account.removeAvatar` procedures (FormData file input, auth middleware) replacing `PUT`/`DELETE` `/api/account/avatar` route - Add `admin.backups.restore` procedure (File input, admin middleware) replacing `POST /api/admin/backups/restore` route - Delete both REST route files - Migrate account-section and backup-restore-section off `useTransition` + raw `fetch` to `useMutation` via `orpc.*.mutationOptions()` - Rename `lib/utils/title-theme.ts` → `lib/theme.ts` and `getTitleThemeStyle` → `getThemeCssProperties`; update title detail page import accordingly * Add OpenAPI v1 REST API endpoint via @orpc/openapi - Add `@orpc/openapi` and `@orpc/json-schema` dependencies - Add `lib/orpc/openapi-handler.ts` with OpenAPIHandler instance - Add `app/api/v1/[[...rest]]/route.ts` catch-all serving GET/POST/PUT/DELETE at `/api/v1` with request-headers context - Annotate contract procedures with OpenAPI metadata (tags, summaries, descriptions, and successStatus codes) across all ~30 procedures * Fix 8 bugs: memory safety, data races, and correctness issues - Move queryClient.setQueryData from useState initializer to useEffect to avoid mutating the query cache during React render phase - Add SQL LIMIT to per-integration event queries instead of loading all events and trimming in memory - Lower backup restore upload limit from 500 MB to 100 MB and stream upload to disk instead of buffering entirely in memory - Reorder avatar upload to write new file before deleting old one so a failed upload doesn't remove the current avatar - Make optimistic rollback field-specific so a failed rating update doesn't erase concurrent status or episode-watch changes - Gate titles.userInfo query behind session state to prevent failing RPC calls for anonymous users - Derive OpenAPI session cookie name from BETTER_AUTH_URL so HTTPS deployments use the correct __Secure- prefixed cookie Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add explicit z.void() output schemas to all 17 void procedures Ensures the OpenAPI spec correctly generates 204 No Content responses for mutation endpoints that don't return data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
🛋️ Sofa
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.
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
- 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
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.
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 check-typesbun run db:generatebun run db:migratebun run db:seed
TMDB notice
This product uses the TMDB API but is not endorsed or certified by TMDB.