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. One docker-compose, your data in a named volume on your machine, Postgres and PostgREST and the app behind a Caddy proxy. The Vercel path is there for people who want one click. The product promise (your shelf, your data) doesn't really hold up if you're locked to my Supabase instance.

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

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: 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

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 pulled automatically.


The reasoning: 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: 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 built Slate using Claude Code. The repo has a CLAUDE.md and AGENTS.md at the root, which is the thing that makes this kind of build viable for one person. Those files give the model the context it needs (file structure, conventions, what "done" looks like for a feature) so the back-and-forth stays productive instead of degrading into copy-paste hell on the second day.


What AI did well: scaffolding shadcn components, writing Supabase queries, setting up the Next.js routing, the TMDB client, the Caddy config. 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, and they're the parts that make Slate feel like Slate. The model can't reach for those calls because it doesn't know what the product is supposed to feel like. That's the designer's job, and it gets harder, not easier, when the rest of the work is going faster.


A few build decisions worth pulling 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 would have shipped the Ask AI search earlier. I built it last and it changed how I use the app more than anything else. If I'd had it from week one, the rest of the design would have flexed around it instead of getting retrofitted.


I'd also have written the CLAUDE.md before the first feature, not after it. The week I spent figuring out the right level of context to give the model would have been a day if I'd thought about it as a deliverable upfront.

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.

Denver, Colorado MDT (UTC +6)

© 2026 All rights reserved

Denver, Colorado MDT (UTC +6)

© 2026 All rights reserved

Denver, Colorado MDT (UTC +6)

© 2026 All rights reserved

Denver, Colorado MDT (UTC +6)

© 2026 All rights reserved
a man smiling for the camera

Nishank Sharma

Senior Product Designer

Loading weather data...

5:43 PM

Available