Your personal Letterboxd. A fast watchlist for everything you want to watch and everything you've loved — private by default, shareable when you choose.
Letterboxd is great, but the feed can get in the way of the shelf. Slate keeps the personal-app feeling: sign in with Google on the hosted version, or run a single-user copy in Docker. Either way, your library starts private and stays focused.
- ⌘K command palette: search TMDB and add anything to your library in one keystroke
- AI search: flip the "Ask AI" pill (or ⌘⇧K) and type plain English like "cozy autumn mysteries", "A24 horror after 2020", or "Nolan thrillers". Live query suggestions surface as you type. Powered by any OpenAI-compatible endpoint (Groq's free tier running Llama 3.3 70B by default) or Claude. Optional; drop in one key to enable
- Three clean states: Watchlist, Watching, Watched, with Love / Like / Dislike ratings and private notes
- Google accounts: the hosted app keeps each person's titles, lists, ratings, imports, recommendations, and ordering isolated in Neon
- Shareable profiles: opt in to a read-only public URL for your Watchlist, Watching, and Watched shelves; profiles are private by default
- Episode tracking without the chore: for the shows you're watching, slate stores where you are (S2·E5), not every episode you've ticked off. Tap the chip on the card to advance one episode; on the title page, tap any episode in the season grid to set "I'm caught up to here." Two clicks to recover after a binge
- Critic scores you can trust: IMDb rating + Rotten Tomatoes Tomatometer (with Metacritic fallback) on every saved title, fetched once via OMDB and cached
- Custom lists: curate collections like "Cozy winter" or "A24 horror"
- Rich title pages: cast, streaming providers, TMDB reviews, trailers
- One-step import from Letterboxd or Trakt CSV exports —
/import - Passcode gate: optional shared-cookie lock for private deployments
- Themeable: System, Light, or Dark mode via the three-way toggle; six accent color palettes to pick from (violet, indigo, sky, emerald, rose, amber)
- Responsive: looks good on every screen size
Next.js 16 (App Router) · React 19 · Tailwind CSS v4 · shadcn/ui · Postgres — Neon (Vercel) or PostgREST (self-host) · TMDB API · Docker / Vercel
Prefer a managed stack? Slate deploys to Vercel + Neon — free serverless Postgres that never pauses — in a few minutes.
git clone https://github.com/gitshanks/slate.git
cd slate && npm install| Service | What you need | Where |
|---|---|---|
| TMDB | v3 API key (free, instant) — required | themoviedb.org/settings/api |
| OMDB | API key (free, 1k lookups/day) — recommended; powers IMDb / RT / Metacritic chips | omdbapi.com/apikey.aspx |
The database is Neon — free serverless Postgres that (unlike Supabase's hobby tier) never pauses and doesn't cap you at two projects. You provision it from inside Vercel in the next step, so there's no key to grab up front.
Push to GitHub and import at vercel.com/new. Then, inside the project:
- Add the database. Storage → Create Database → Neon (Vercel Marketplace). Pick the same region as your project — it provisions a free Neon Postgres and sets
DATABASE_URLfor you automatically. - Load the schema. In the Neon console → SQL Editor, paste
supabase/schema.sqland run it. That's the entire database. Re-running is safe — everycreate/alterusesif not exists, so it picks up new columns without touching your data. - Add the rest of the environment variables (Settings → Environment Variables):
| Variable | Purpose | |
|---|---|---|
TMDB_API_KEY |
required | TMDB v3 key |
DATABASE_URL |
required | Neon Postgres connection — set automatically by the Marketplace integration above. Add by hand only if you bring your own Neon/Postgres: use the pooled (-pooler) URL with ?sslmode=require. |
OMDB_API_KEY |
recommended | Powers IMDb / Rotten Tomatoes / Metacritic chips on saved titles. Without it, those stay blank but everything else still works. |
OPENAI_API_KEY |
optional | Unlocks AI search in the ⌘K palette for natural-language queries and live suggestions. Defaults to Groq's free tier running Llama 3.3 70B; works with any OpenAI-compatible endpoint (OpenRouter, Ollama, LM Studio, vLLM, llama.cpp). Override the endpoint with OPENAI_BASE_URL and the model with OPENAI_MODEL. |
ANTHROPIC_API_KEY |
optional | Alternative AI backend; uses Claude instead of an open model. Set AI_PROVIDER=anthropic to prefer it when both keys are present. |
APP_PASSCODE |
optional | Lock the app behind a shared passcode. Omit for public. |
NEXT_PUBLIC_DEMO_MODE |
optional | Set to 1 on a portfolio or public-demo deploy. Skips the APP_PASSCODE gate, shows a demo banner, mounts a marketing landing page at /, and moves the watchlist to /app. Self-host default (unset) keeps the app at / so existing bookmarks and PWA installs are unaffected. |
NEXT_PUBLIC_SLATE_HOSTED |
optional | Set to 1 for the Google-account hosted product. Mounts the marketing page at /, the private library at /app, and enables public profiles at /u/:username. |
AUTH_SECRET |
hosted | Auth.js session secret. Generate with npx auth secret. |
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET |
hosted | Google OAuth web-client credentials. Add https://your-domain/api/auth/callback/google as an authorized redirect URI. |
NEXT_PUBLIC_SITE_URL |
hosted | Canonical origin used for public profile links, such as https://slate.nishh.dev. |
SLATE_LEGACY_OWNER_EMAIL |
optional | Google email allowed to claim rows created before account support on its first sign-in. |
Prefer Supabase, or already have another Postgres? Set SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY instead of DATABASE_URL — the data layer uses whichever backend is configured.
- Set
NEXT_PUBLIC_SLATE_HOSTED=1,AUTH_SECRET, the two Google OAuth variables, andNEXT_PUBLIC_SITE_URL. - In Google Cloud, authorize
https://your-domain/api/auth/callback/google. - For an existing database, run
npm run db:migrate:accounts. A new database gets the same ownership/profile schema fromsupabase/schema.sql. - If the deployment already contains your pre-account library, set
SLATE_LEGACY_OWNER_EMAILto your Google email before the first sign-in. Only that account can claim the legacy rows.
Leave every hosted-account variable unset to retain the original self-hosted/single-user behavior, including the optional APP_PASSCODE gate.
Slate is designed to be self-hosted. Everything runs on your machine and your data never leaves it. The only outbound calls are to TMDB for metadata.
git clone https://github.com/gitshanks/slate.git
cd slate
cp .env.example .env # fill in TMDB_API_KEY + OMDB_API_KEY
docker compose up -dOpen http://localhost:3000. Done.
You'll want two free keys before you start:
- TMDB (themoviedb.org/settings/api) — required. Drives metadata, posters, search, cast, providers, recommendations.
- OMDB (omdbapi.com/apikey.aspx) — strongly recommended. Drives the IMDb / Rotten Tomatoes / Metacritic chips on saved titles. 1,000 lookups/day is plenty for a personal library; results are cached for 24h. Skip it and the app still runs, but every rating chip stays blank.
| Service | Image | What it does |
|---|---|---|
postgres |
postgres:16-alpine |
Your database. One named volume. |
postgrest |
postgrest/postgrest |
REST API over the tables. |
caddy |
caddy:2-alpine |
Maps /rest/v1/* to PostgREST so the Supabase client works unchanged. |
slate |
built from ./Dockerfile |
The Next.js app. The only service exposed to the host. |
All your titles, ratings, and notes live in the postgres_data volume.
# Back up
docker compose exec postgres pg_dump -U slate slate > slate.sql
# Restore
cat slate.sql | docker compose exec -T postgres psql -U slate slate
# Wipe everything
docker compose down -v- Put Caddy or Traefik in front of the
slatecontainer for TLS and a real domain. - Set
APP_PASSCODEin.envto gate access behind a shared code. - The stack is single-user by design. If you want to expose it publicly, keep the passcode on.
- PostgREST runs without JWT auth inside the compose network, so don't expose port 3001 to the internet.
Two one-shot scripts cover the cases where a feature shipped after some titles were already in your database. Both read .env.local / .env, throttle to stay polite on the free TMDB / OMDB tiers, and skip rows that already have data, so they're safe to re-run.
# IMDb / Rotten Tomatoes / Metacritic for every saved title
npx tsx scripts/backfill-ratings.ts
# Per-season episode counts for every TV title — needed for the
# +1 chip and the season picker on title pages
npx tsx scripts/backfill-seasons.tsComing from another tracker? Drop a CSV into /import:
- Letterboxd: Settings → Data → Export your data. Import
watched.csv,ratings.csv, orwatchlist.csv. - Trakt: any movie/show CSV export.
Rows are matched against TMDB, deduped against your library, and dropped into the right state with ratings preserved.
app/
(app)/ # main app — protected by passcode when set
page.tsx # / Watchlist
watching/ # /watching
watched/ # /watched
title/[id]/ # /title/:id
lists/ # /lists, /lists/:slug
discover/ # /discover
search/ # /search keyword + AI search results
import/ # /import
person/[id]/ # /person/:id
share/ # /share add-to-library deep link
landing/ # marketing landing page (demo mode)
unlock/ # passcode screen
api/
tmdb/search/ # server-side TMDB proxy
ai-suggest/ # live AI query suggestions
ai-chat/ # natural-language AI search
version/ # build version probe
layout.tsx # root shell + ThemeProvider + Sonner
globals.css # design tokens (HSL → @theme inline)
components/
ui/ # shadcn/ui primitives
command-palette.tsx
poster-card.tsx
backdrop-hero.tsx
review-sheet.tsx
...
lib/
supabase.ts # server-only client + generated types
tmdb.ts # TMDB fetch helpers
actions.ts # Server Actions — all mutations
ai-search.ts # natural-language query parsing
accent-theme.ts # accent color palette config
...
proxy.ts # hosted auth routing + self-hosted passcode gate
supabase/schema.sql # one-shot DB setup
docker-compose.yml # self-host stack
Dockerfile # Next.js standalone runner image
- The database credential (
DATABASE_URLfor Neon, orSUPABASE_SERVICE_ROLE_KEY) is read only inside server-only modules (lib/supabase.ts,lib/neon-client.ts), so it can never leak into a client bundle. TMDB_API_KEYnever touches the browser. The command palette routes through/api/tmdb/search.- Hosted sessions use Auth.js with Google OAuth. Every owned query and Server Action is scoped in the server-side data access layer; Proxy redirects are only the fast outer gate.
- Public profile pages first verify the profile's
is_publicflag and expose only read-only library fields. - Self-hosted mode retains the optional shared passcode gate and stable
self-hosteddatabase owner.
This product uses the TMDB API but is not endorsed or certified by TMDB. Poster and backdrop artwork is served from TMDB's CDN.
- Jellyfin library integration: show what you already own in slate, matched via TMDB IDs. Coming soon.
- Following or a social feed: public profiles are direct-link, read-only shelves rather than another engagement network.
- Plex library integration.
- Trakt / MyAnimeList sync: CSV one-shot import only.
- Native mobile apps: PWA install is supported on iOS Safari and Chromium.
- Library export: you can import a library but can't yet dump one out.
MIT. Fork it, host it, make it yours.