slate
A private Letterboxd for one. Built in two weekends, with AI in the loop.

I wanted to track movies and TV shows somewhere that didn't feel like a social network. Letterboxd is great if you're into the social part, but it doesn't really do TV, and honestly I wasn't there for the followers anyway. I wanted a shelf. Something I could open at 11 pm to decide what to watch tonight, then close.
The other thing I wanted to test: could a senior product designer ship a real, opinionated consumer product end to end with AI in the loop, in spare time, without it turning into the kind of vibe coded mess that doesn't survive contact with actual use?
slate is what came out. Two weekends, one person, and I've been using it daily since.
The constraints I set early, and held
A lot of how slate feels comes from the things I decided not to build. The single user direction was the first call, but a few others mattered just as much.
Single user by design.
No accounts, no follower graph, no public timeline. This is the opposite of Letterboxd's bet. My friction with Letterboxd was always the social layer wrapped around the tracking, the part I'd been trying to ignore for years. So I cut it.
Three states, no more.
Want, Watching, Watched. Plus a Loved / Liked / Disliked sentiment underneath. I went back and forth on adding Paused, Dropped, Maybe, and ended up not. More shelves means more decisions per title, which is the thing you're trying to avoid at 11pm when you're picking what to watch.
Self-hostable from day one.
Hit the button, connect a free Supabase project, you're done in two minutes. Your account, your data, your control. (Self-hosters get a docker-compose path too, but most people won't need it.)
Bring your own AI model.
The vibe search feature defaults to Groq's free Llama 3.3 tier so the live demo works for everyone, but you can point it at Claude, GPT, or Ollama with one env var. The AI shouldn't be a vendor lock-in for a tool whose whole point is portability.
Each of these was a moment of saying no to the obvious extension. Without those nos, I'd have ended up shipping a watered down Letterboxd clone, which is the thing I was trying to escape in the first place.
Three decisions worth walking through
Ask AI lives inside the command palette, not beside it
Same keystroke, same flow. You keep the muscle memory you already had.
The vibe-search feature ("cozy autumn mysteries", "A24 horror after 2020") could have lived on its own page. A Discover tab, separate flow, separate query box. I considered it.
The problem with that path is that the user is already in Cmd+K muscle memory. They press it, they search, they add. Forking that flow into a "are you searching by title or by vibe?" decision adds a tax to every search, and adds a second place to remember when you want to find something.
So there's a small pill at the top of the command palette that flips between Search and Ask AI. Same keystroke, same flow, the result list looks identical. You hit enter, the title is in your library, you keep the muscle memory you already had.
The general rule I took from this: when you're adding an AI feature to an existing product, the cheapest, fastest place for it to live is usually inside the surface the user already uses, not next to it.
Sentiment without stars
"I loved that one" is retrievable a year later. "I gave it a 4 out of 5" isn't.
Most apps give you a way to grade what you watched. Stars, points out of ten, something numerical and personal. Slate doesn't. Instead you get Loved / Liked / Disliked, and separately, critic scores from IMDb, Rotten Tomatoes, and Metacritic are pulled automatically.
The reasoning is simple: personal star ratings feel like grading something, an evaluation you'd put on a homework assignment. I noticed I never went back to read my own ratings. What I went back to was the sentiment, because that's how memory actually works. "I loved that one" is retrievable a year later. "I gave it a 4 out of 5" isn't.
The critic scores do the grading job instead, and they're better at it than my personal rating would have been. They're aggregated, they're already there when you log a title, and they give you a reference point without asking you to evaluate anything in the moment. The personal rating field was friction with no payoff. So I cut it.
CSV import on day one, not month three
Most personal projects skip migration. You ship the thing, you tell people "just start fresh." I knew that wouldn't fly for me. I had five years of Letterboxd history and I wasn't going to retype any of it.
So Slate has CSV import for Letterboxd and Trakt out of the gate. Drop in your export, Slate matches every row against TMDB, dedupes against your library, drops it into the right state with ratings preserved.
It took longer than I expected (TMDB matching is fuzzy for older and international films) but it was the difference between "cute side project" and "app I actually use."
The rule I'd pull out of this is that any tool that asks the user to leave another tool needs to give them a door. I think it's one of the bigger reasons most personal projects don't actually get used by anyone, including the person who built them.



How it was built
The stack
Next.js 16, React 19, Tailwind v4, shadcn/ui, Supabase Postgres, TMDB and OMDB for catalog and ratings, all TypeScript. Server Actions handle every mutation. Self-host runs Postgres + PostgREST + the Next.js app + Caddy through docker-compose. Vercel deploy is one click.
The workflow
I treated this build the way I'd treat any product at work, just compressed. I started by writing a PRD with Claude as a thinking partner, working through what Slate was, what it wasn't, the three states, the AI search behavior, the import flow, the self-host story. That document then became the brief I fed into Claude Code.
In parallel, I designed the core screens in Figma (library, detail view, command palette, import flow) and pulled reference patterns from Mobbin for things like how the command palette should feel, how empty states should read, how the detail view should layer content.
The Figma frames and the Mobbin references went into Claude Code alongside the PRD. By the time I started writing code, the model had three sources of truth: what we were building, what it should look like, and what good looked like in adjacent products.
That setup is the actual reason this build took two weekends instead of two months. Not because Claude Code is magic, but because most AI-assisted work fails when the model has to guess what you mean. A PRD plus designs plus references means it doesn't have to guess. It just executes.
What AI did well
Scaffolding shadcn components against my Figma frames, writing Supabase queries from the PRD's data model, setting up the Next.js routing, the TMDB client, the Caddy config for self-hosting. A lot of code I could have written but didn't need to. Maybe 5x faster on the boilerplate, sometimes more.
What AI didn't do well
The IA decisions. Whether Ask AI lives inside the palette or beside it. Whether to kill personal star ratings in favor of critic scores. What the empty state of the Watching shelf says. Whether the passcode gate ships in v1 or whether it's overengineering. Those were all me. The model can execute on a clearly framed decision. It can't tell you what the right decision is, because it doesn't know what the product is supposed to feel like. That's the designer's job, and in my experience it gets more important, not less, as the rest of the work moves faster.
A few build decisions worth pointing out, because they're the kind of thing that gets skipped in vibe coded apps and then breaks at the worst time:
The Supabase service role key only lives in lib/supabase.ts, which has import "server-only" at the top. It can never reach a client bundle. The TMDB key doesn't reach the client either: the command palette routes through a server proxy at /api/tmdb/search. Small things, but if a self-hoster forks the repo and pushes to public GitHub, this is the difference between a leaked key and not.
Caddy proxies the Postgres + PostgREST stack on a self-hosted box, so the standard Supabase client just works. The same code path runs whether you're on Vercel + Supabase or on your laptop with docker-compose. No second branch of the codebase to maintain when you switch environments, which was a thing I really didn't want to deal with.
A small toast that prompts you to refresh when a new deploy is live. Slate is the kind of tool people leave open for weeks. So the app polls for new deploys every 45 seconds and fires a Sonner toast when it detects one. No service worker, no PWA. Just a version check.

What I'd do differently
I'd ship the Ask AI search in week one, not week three. I built it last and it changed how I use the app more than anything else. Most of my searches are vibe queries now, not titles, which means the entire IA of the command palette should have been vibe-first from the start. Instead it got retrofitted, and you can still feel that in the empty states and the way results render.
I'd have spent more time on the empty states. The Watching and Want shelves both look great when populated and visually awkward when empty, which is the moment most people are going to see them for the first time. I treated empty states as polish work and left them for late in the build. They're closer to the front door than that.
What's next
Probably not much. Slate does what I needed it to and I use it every day. It's MIT licensed, the docker compose is on GitHub, the Vercel path is documented in the README. If other people want to fork it and run their own copy, that's the whole point.
If you want to try it: there's a live demo at slate.nishh.dev/app with seeded data, or you can pull the repo at github.com/gitshanks/slate and have your own copy running before the kettle boils.