The ranking studio for people who publish their tier lists.
Rank by 1v1 duels, let your Twitch chat vote, drive an OBS overlay, and export the whole session as a video — in the browser, in English or French, with no account and no upload.
Tier list makers are built for making a tier list. RankRoom is built for the twenty minutes either side of that: deciding the order on camera, showing it to an audience while it happens, and walking away with assets you can actually publish.
A blank board is a blank page, so RankRoom opens with a gallery of ready-made lists — Nintendo series, shooters, consoles, Ghibli, directors, fast food, car brands, French streamers, programming languages, days of the week. Pick one, and everything is on the bench waiting to be ranked.
Templates carry more than a list of names: where a custom ladder says something S–F cannot, the template brings its own. Consoles are ranked Generational → Forgotten, fast food Tonight → Never, programming languages By choice → Never again.
On images: templates are text tiles on purpose. Shipping logos, box art and photos of people would mean shipping other people's copyrighted work. Drop your own images on top and they replace the text.
Dragging 40 tiles into the right order is a chore, and the result is usually wrong anyway. Duel mode asks one question at a time — A or B? — and binary-inserts each item into the ranking. 20 items take about 60 duels instead of the 190 a full round-robin needs, and the ordering that comes out is one you actually defended.
Ties, skips and a full undo history are all one keystroke away. When the run ends you choose the shape of the ladder — even, top-heavy or bell — and the ranking drops onto the board.
One tile on deck at a time, placed with a number key, mirrored to a transparent overlay as it happens. A tally lamp tells you when you are on air, because a tier list app built for streamers should behave like the rest of the gear on the desk.
Two ways into the overlay, because they solve different problems:
| Link | Where it works | Live? |
|---|---|---|
#/overlay |
A second browser window you capture in OBS | ✅ follows the studio in real time |
#/overlay/<board> |
Pasted straight into an OBS Browser Source | ❌ shows the board as it was when you copied the link |
The Browser Source runs inside OBS's own embedded browser, a separate process from your Chrome — nothing can push state across that line without a server, and RankRoom does not have one. The baked link is the honest answer; the window capture is the live one.
RankRoom reads a Twitch channel straight from the page: anonymous, read-only, no bot, no OAuth, no server. Viewers vote, and where they land shows up as a translucent ghost layer under your own placements — you stay solid, they stay see-through, and the disagreement is visible at a glance on stream.
Two ways to run it, because a live bit and a long stream want different things:
| Mode | What chat types | Good for |
|---|---|---|
| Poll the deck | just a tier letter — S, a, !b |
A vote on camera, one tile at a time |
| Free placement | !s dune |
Ambient participation across a whole stream |
One vote per viewer per item, latest wins, with an optional expiry window so a poll can move on. Ghosts only ever show a disagreement — when chat agrees with you, the layer stays quiet. And nothing chat says moves a tile on its own: you take their pick for one item, or apply their whole ranking, on purpose.
The ghost layer renders in the OBS overlay too, so your audience watches its own votes land next to yours.
Above: chat put Nope in S with five votes, so it shows as a dashed, translucent ghost — while the streamer's own placements stay solid.
Every placement is logged as you make it. Replay mode plays the session back on a canvas and records that canvas to a real video file — 16:9 for YouTube, 9:16 for Shorts, Reels and TikTok. The ranking assembling itself in fast-forward is the clip; you didn't have to screen-record anything.
When chat has voted, the replay can end on their verdict: the ghost layer fades in over your finished board and the single biggest disagreement is ringed in red. Below, the streamer put Zelda in S — chat put it in F.
Load someone else's board — a share link or a .json — and RankRoom lines it up against
yours, matching by item name when the two were built separately. It scores how much you
agree, normalising across boards with different tier counts, and surfaces the hot
takes: the items you are furthest apart on. That list is the video.
- Drag and drop that works with a mouse, a finger, and a keyboard — pick a tile up with Space, move it with the arrows, drop it with Space. Screen readers get told which tier the tile is over.
- Drop or paste images anywhere on the page. They are re-encoded to small WebP, which strips EXIF — including the GPS coordinates you did not mean to publish.
- Export a PNG at three sizes, a ready-made 1280×720 thumbnail built from your top
tier, the raw
.json, or a.rankroomarchive that keeps images as real files. - Share links carry the whole board in the URL fragment. Fragments are never sent to a server, so not even the host it is served from sees what you ranked. The link keeps working even if that host disappears.
- Works offline. A service worker precaches the app, the fonts and the icons.
- An account, if you host the server. Sign in with Twitch, keep boards across
devices, publish a page at
#/u/<your-channel>, and drive an OBS overlay that follows your board live — paste one URL once and it keeps up for the whole show. Without a server none of this appears and nothing else changes. - 26 built-in templates across seven categories, searchable by name or by an item inside them — typing "bloodborne" finds the soulslike board.
- English and French, detected from your browser and switchable from the header.
- Built for a phone as much as a desktop — tiles size themselves to the viewport, five per row on a 390px screen, with no hover-only controls anywhere.
- Command palette on ⌘K, undo/redo, autosave, a board shelf, five tier palettes, and a light theme for when the studio look is too much.
The default tier colours are SMPTE colour bars — the broadcast test card. That ramp is
ordered by descending luminance, which is exactly what a tier ladder needs: it reads
correctly at a glance, survives being screenshotted in greyscale, and every swatch clears
WCAG AA against its label. The empty board is a test pattern reading NO SIGNAL, and the
red and green accents are a vision mixer's program and preview buses, used with that
meaning everywhere in the app.
The image above is a real export, produced by the same canvas renderer that draws the replay video and the thumbnail.
Tile size follows the viewport by default — five per row at 390px, ten on a laptop — and the slider in the inspector overrides it when you want something else. Tier controls live in a single menu rather than a hover-revealed column, which is what let a row drop from 88px to 56px.
Everything above works with no server at all, and that is the default. For the
three things a browser cannot do alone — an identity that survives a new device,
public boards other people can open, and an OBS Browser Source that follows
your board live — there is a small self-hostable server in
server/.
It is Rust, it ships as one container, and it is meant to be run by you: there is no hosted instance to trust. Sign-in is Twitch OAuth with no scopes requested, session tokens are stored hashed, and every board is re-validated server-side because the client is not a security boundary. Fourteen authorisation tests run against a real Postgres on every commit.
Build the front end with VITE_API_URL pointing at your server to enable it.
Leave it unset and the app has no idea the server exists.
git clone https://github.com/micferna/rankroom.git
cd rankroom
npm install
npm run dev| Script | What it does |
|---|---|
npm run dev |
Vite dev server on :5173 |
npm run build |
Type-check, then build to dist/ |
npm run preview |
Serve the production build |
npm test |
Run the test suite |
npm run test:coverage |
…with coverage for lib/ and store/ |
npm run lint |
ESLint, type-aware and strict |
npm run typecheck |
tsc --build |
npm run format |
Prettier |
npm run check |
Everything above, in the order CI runs it |
npm run assets |
Regenerate the icons and social card |
Node 20.19+ is required. There is no backend, no database, and no environment file.
cp .env.example .env # set PUBLIC_URL and DB_PASSWORD
docker compose up -d --buildTwo containers: the app and Postgres. The app serves the front end and the API from one
origin, which is a security decision before it is a packaging one — same-origin means the
session cookie keeps SameSite=Lax, there is no CORS to misconfigure, and cross-site
request forgery is not in the threat model. The image is distroless, runs as a non-root
user, and carries no known vulnerabilities.
Only the app is published, and only on loopback: point your reverse proxy at
127.0.0.1:8080 and let it terminate TLS. It must overwrite X-Forwarded-For rather
than append to it — the rate limiter keys on that header. server/README.md
has the details.
Backups run in the stack, verified by reading each dump back before it is kept,
with a restore script that refuses anything it cannot read. See
server/README.md — including the two things it
cannot do for you.
Or host nothing at all. The app is still a static bundle: npm run build and upload
dist/ anywhere. You lose accounts, public profiles and the live overlay; everything else —
duels, chat voting, exports, share links — runs entirely in the browser and always has.
React 19 · TypeScript 5.9 (strict, noUncheckedIndexedAccess) · Vite 8 · Tailwind CSS 4 ·
zustand · dnd-kit. Three runtime dependencies beyond React itself.
Everything that could be a dependency but did not need to be is not one: the icon set, the
canvas renderer, the service worker, the share-link codec, the i18n layer, the Twitch chat
client and the icon generator are all first-party and dependency-free. npm audit reports
zero vulnerabilities.
The domain logic — placement, tier maths, the duel engine, the vote tally, the templates,
the share codec, the untrusted-input parser — is pure and lives in src/lib/,
covered by 137 tests and importable without a DOM. React components read it; they do not
reimplement it.
📐 docs/ARCHITECTURE.md walks through the data model, the undo system, the replay pipeline and the security boundaries.
Issues and pull requests are welcome — see CONTRIBUTING.md. Found a security issue? SECURITY.md explains where to send it.
MIT. The bundled fonts are SIL OFL 1.1.







