Doppelgänger
Five party games in one room, with no download, no sign-up and no game server; the host's browser is the referee and Postgres is the thing that stops you cheating
Screenshots
Five party games sharing one shell: a room code, a table of players, a timer. The host picks a mode in the lobby and everyone else just plays, on the phone already in their hand, with no download and no sign-up.
- Masquerade: everyone answers the same prompt in disguise, then matches each anonymous answer to its author.
- Punchline: everyone writes the funniest answer to a setup; the room votes.
- Mimic: you’re secretly assigned another player to answer as. Can the room tell who you were doing?
- Impostor: everyone but one gets the same secret word. One-word clues around the table, then vote out whoever seems off.
- Trivia: one question, one right answer. Correct beats wrong, fast beats slow.
Each mode plugs its own round shape into the same phase machine:
lobby → answering → (guessing | voting | clues→voting) → scoring → next round.
No game server
There isn’t one. The host’s browser is the referee: it polls every 1.5 seconds and advances the phase when everyone is done or the deadline passes. Every other client just watches the same room over Supabase Realtime.
That sounds reckless until you look at where the secrets live. The host’s browser advances phases; it does not get to know things early. Anti-cheat is enforced in the database, not by trusting any client:
- Players have no
SELECTon the answers table at all. They read a view that hides the author until scoring and hides other people’s text while the room is still answering. - Trivia questions and rounds are unreadable by every client, host included.
Players read a view where the correct answer stays
nulluntil the grading function flips it. - Trivia timing is computed in Postgres from the round’s start timestamp, because a client reporting its own elapsed time would answer everything in 0.01 seconds.
- The Impostor’s word and role are drawn inside Postgres, so even the host’s machine doesn’t learn who the impostor is before the reveal.
You can verify it from a terminal: query the raw answers table as a player mid-round and you get an empty array.
The rest of it
Three ways in, all landing in the same game: anonymous (created on first visit, zero friction), Google OAuth, or email and password. Friends added from the finished screen persist into future sessions, anonymous accounts included.
Prompts exist in seven languages, with UI language switchable independently. Languages with no prompts in the database show as disabled rather than as an empty room. Prompt history is tracked per signed-in player, so a fresh room with the same friends doesn’t repeat the prompts they’ve all already answered.
Categories come from one shared registry that defines each slice’s icon, colour and label exactly once; every mode’s content table foreign-keys into it under its own scope. The picker only appears once a language actually has two or more populated slices, because one option is a setting, not a decision.
Premium (Stripe subscriptions) raises the room cap from 8 to 12, rounds from 5 to 10, and removes the one-room-an-hour limit. Free players who hit that limit can watch a short ad for another room instead of hitting a paywall.
Crayon Cryptids
Everything is drawn, not rendered: flat riso fills, ink outlines, hand-cut edges. There is no light source anywhere in the design: no gradients on surfaces, no blurred shadows, no glows. “Alive” is expressed by moving, with bob, wobble and tilt, never by lighting.
Every player is a procedural blob monster generated from an avatar seed, drawn
as inline SVG, standing in a shallow arc on the table in every phase rather than
only in the lobby. The scoring reveal is your monster unmasking. A dedicated FX
layer drives particle bursts, screen shake and phase-intro cards, tuned by three
constants so intensity is a one-place decision rather than fifteen call sites,
and prefers-reduced-motion swaps all of it for simple fades.
Voice chat is built and wired with LiveKit, and currently feature-flagged off.
▸ stack