Swap the hand-rolled Scheduler class (setInterval-based) for croner, a battle-tested cron library with overlap protection and proper cron expressions. Consolidate lib/jobs/ into a single lib/cron.ts and remove the unused admin jobs API route. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
bun dev # Start Next.js dev server
bun build # Production build
bun lint # Biome lint check
bun format # Biome format (auto-fix)
bun db:push # Push schema changes to SQLite database
bun db:generate # Generate Drizzle migration files
bun db:migrate # Run Drizzle migrations
bun db:studio # Open Drizzle Studio (visual DB browser)
Architecture
Sofa is a self-hosted movie & TV tracking app (like Trakt/TVTime) built as a single Next.js 16 application with SQLite.
Stack
- Framework: Next.js 16 (App Router), React 19, TypeScript
- Database: SQLite via bun:sqlite + Drizzle ORM (WAL mode, singleton via
globalThis, sync queries) - Auth: Better Auth with Drizzle adapter, email/password
- Styling: Tailwind CSS v4, shadcn components, dark cinema theme with warm primary accents
- Fonts: DM Serif Display (display), DM Sans (body), Geist Mono (mono)
- Linting: Biome (2-space indent, organized imports, React/Next.js recommended rules)
- External API: TMDB (The Movie Database) with Bearer token auth
Path alias
@/* maps to project root (./), e.g. @/lib/db/client → ./lib/db/client.
Key directories
lib/db/schema.ts— Single file with all Drizzle table definitions (auth tables + app tables)lib/services/— Business logic layer (metadata, tracking, discovery, availability)lib/tmdb/— TMDB API client and TypeScript typeslib/auth/— Better Auth server config (server.ts) and client hooks (client.ts)lib/cron.ts— Background job scheduler (croner,globalThissingleton)app/api/— Next.js route handlersapp/(pages)/— User-facing pages (dashboard, search, title detail, login, register)components/— App components +components/ui/for shadcn primitives
Auth pattern
Route handlers call Better Auth directly. Every authenticated route follows this pattern:
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
const session = await auth.api.getSession({ headers: await headers() });
if (!session)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// use session.user.id
Database schema
All app tables use UUID text primary keys generated via the uuid package. Better Auth tables use their own ID format. Key relationships:
titles→seasons→episodes(TV hierarchy)userTitleStatuslinks users to titles with status (watchlist/in_progress/completed)userMovieWatches/userEpisodeWatchestrack watch historyuserRatingsstores 1-5 star ratingsavailabilityOfferscaches streaming provider data per titletitleRecommendationsstores TMDB recommendations and similar titles
Service layer
- metadata.ts:
importTitle()fetches from TMDB, inserts into DB, fire-and-forgets availability + recommendations + image caching.refreshTvChildren()fetches all seasons/episodes with 250ms rate limiting. - image-cache.ts: Downloads and caches TMDB images to local disk.
cacheImagesForTitle()caches poster + backdrop,cacheEpisodeStills()caches episode stills,cacheProviderLogos()caches streaming provider logos. - tracking.ts: Auto-transitions — logging a movie watch sets status to
completed; logging an episode watch setsin_progress; all episodes watched auto-completes the series. - discovery.ts: Feed generators — continue watching (next unwatched episode per in-progress show), library titles with availability, personalized recommendations from completed/highly-rated titles.
- availability.ts: Caches US streaming providers from TMDB watch/providers endpoint.
TMDB images
Only paths are stored in DB. Image URLs are resolved server-side only — API routes and server components call tmdbImageUrl() from lib/tmdb/image.ts before sending data to clients. Client components never import tmdbImageUrl; they receive ready-to-use URLs.
When IMAGE_CACHE_ENABLED is set (default), images are downloaded to local disk (CACHE_DIR, derived from DATA_DIR/images) and served via app/api/images/[...path]/route.ts. Categories: posters (w500), backdrops (w1280), stills (w1280), logos (w92). When disabled, tmdbImageUrl() returns direct TMDB CDN URLs using TMDB_IMAGE_BASE_URL (defaults to https://image.tmdb.org/t/p).
Core files: lib/services/image-cache.ts (caching logic), lib/tmdb/image.ts (URL construction), app/api/images/[...path]/route.ts (proxy route).
Background jobs
Defined in lib/cron.ts using croner cron expressions, started via Next.js instrumentation hook (instrumentation.ts) in production. Jobs: nightly library refresh (0 3 * * *), availability refresh (0 */6 * * *), recommendations refresh (0 */12 * * *), TV episodes refresh (30 */12 * * *), image caching (0 1,13 * * *). All jobs use protect: true to prevent overlapping runs. All batch jobs use 300ms delay between TMDB calls.
Environment variables
See .env.example: DATA_DIR (root for DB + cache, default ./data), TMDB_API_READ_ACCESS_TOKEN, BETTER_AUTH_SECRET, BETTER_AUTH_URL. DATABASE_URL and CACHE_DIR are derived from DATA_DIR but can be overridden individually.
Browser Automation
Use agent-browser for web automation. Run agent-browser --help for all commands.
Core workflow:
agent-browser open <url>- Navigate to pageagent-browser snapshot -i- Get interactive elements with refs (@e1, @e2)agent-browser click @e1/fill @e2 "text"- Interact using refs- Re-snapshot after page changes