ahmad@abboud.dev · bash
tty1
Last login: just now on ttys001
ahmad@abboud.dev · portfolio shell v1.0
▸ mounting /projects … ok
▸ loading identity … ok
▸ 2 phone lines, 1 timezone, 0 recruiters harmed
 
Ahmad Abboud
Senior Full-Stack Engineer · Tech Lead
I drag factories into the 21st century.
 
You have a shell. Most of it is locked.
Type -h to see what you can run.
// no javascript. the shell is off, but everything it prints is written out beside it.
type -h and hit enter
ahmad@abboud:~$ open doppel

Doppelgänger

Creator & Solo Developer

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

5 game modes7 languagesno custom game server

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 SELECT on 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 null until 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

Next.js 14TypeScriptSupabasePostgres + RLSTailwindFramer MotionStripeLiveKit