OhMyGame
OhMyGame is the open-source studio for making games with AI. Design, build, playtest, and publish games in one app, with your own model keys.
A new murder mystery every day. Question AI suspects, examine the evidence, name the killer.
Translation pending
GamesTypeScript · PLpgSQLGitHub @eneseceOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A daily AI murder mystery. Everyone gets the same case each day. Read the file, study the evidence, interrogate four AI-played suspects (20 questions in total), then make one accusation.
▶ Play it: dailyalibi.vercel.app · English + Turkish · no sign-up
Built with Next.js 16 (App Router), TypeScript, Supabase (Postgres + Row Level Security + anonymous auth), Gemini's free tier, Zod, next-intl and Tailwind. It runs on Vercel Hobby and Supabase Free and costs $0 a month.
A chatbot that plays a murder suspect is easy to build. A game built on one has three harder problems:
The sections below explain how each one is handled.
flowchart LR
subgraph Browser
UI["Case file · Evidence<br/>Interrogation · Notebook"]
end
subgraph Vercel["Next.js on Vercel (server)"]
RSC["Server Components<br/>(public briefing only)"]
API["Route Handlers<br/>/api/interrogate · /api/hint · /api/accuse"]
PROMPT["Suspect prompt builder<br/>(one suspect's notes only)"]
GUARD["Output guard<br/>(streamed, 40-char holdback)"]
CRON["Vercel Cron<br/>daily publish"]
ADMIN["Admin panel<br/>(Server Actions)"]
end
subgraph Supabase
DB[("Postgres + RLS<br/>cases.secret: no browser grant")]
AUTH["Anonymous auth<br/>+ Turnstile"]
end
subgraph Offline["Offline (GitHub Actions / CLI)"]
GEN["Generator → Zod → 14 logic checks<br/>→ blind solver → repair"]
end
LLM["Gemini<br/>(free tier, behind a provider-agnostic module)"]
UI -- "public data" --> RSC
UI -- "question (≤ length limit)" --> API
API -- "counter, rate limit (SQL functions)" --> DB
API --> PROMPT --> LLM --> GUARD -- "streamed answer" --> UI
UI <-- "own rows only (RLS)" --> DB
UI --> AUTH
CRON --> DB
ADMIN -- "approve · schedule" --> DB
ADMIN -. "dispatch" .-> GEN
GEN --> LLM
GEN -- "draft + review" --> DB
One game, step by step: the case page is a Server Component that reads only the public briefing (cached). Each question is a POST to a Route Handler. The server checks the session and rate limits, reserves a question in Postgres (the counter lives in a SQL function, not in the browser), loads the case itself, builds the prompt for that one suspect, and streams the answer back through the output guard. The accusation is checked and scored on the server. Only after that is the solution sent to this player.
The rule: the killer, the motive, the true timeline and every suspect's secrets never reach the browser, in any form.
src/lib/case/schema.ts) has a public part and a secret part. Every module that touches secret imports server-only, so importing it from a Client Component breaks the build.cases.secret has no column grant for the anon/authenticated roles. A player who copies the publishable key and queries Supabase directly gets permission denied. Players can read their own games and messages, but can only write them through SQL functions (start_game, reserve_question, finish_game, …) that only the server's key may execute. Every table has RLS enabled.start_game function and every server read require it. Without this, a case scheduled for next week could be played early.src/server/interrogation/prompt.ts) takes the public briefing, one suspect's private notes and an isKiller boolean. The solution and the other suspects' notes are not parameters, so they cannot end up in the prompt. An innocent suspect does not know who the killer is, so no jailbreak can get it out of them.npm run check:leaks builds the app and searches every client JS file and pre-rendered HTML/RSC payload for every secret string of every case (115 at the last live check). Result: 0 leaks.npm run check:prompts builds every suspect's real prompt and fails if it contains another suspect's secret or (for innocents) the solution.npm run smoke plays a full game as a real user, then tries to cheat with the same credentials: reading secret, resetting the counter, calling the RPCs directly, reading another player's notes.revealWhen, such as "when shown the telephone log". The rule is that a condition beats a lie, and a condition that lists several things needs all of them. With the Show evidence button, a player can put a piece of evidence in front of a suspect. The evidence text comes from the case, never from the player.answer-guard.ts, guarded-stream.ts) holds back the last 40 characters. When an answer breaks character, echoes the prompt or (for the killer) confesses, it stops the model and replaces the unsent part with an in-character line. What is saved is exactly what the player saw.| Eval | What it does | Result |
|---|---|---|
Prompt injection (npm run eval) | 50 attacks (40 single-turn + 10 multi-turn, 10 categories, EN + TR) against every suspect role in 4 cases, using the game's real prompts | 199/200 defended (model alone 198/200). The one failure was a false alarm on manual review. Right language: 200/200 |
Consistency (npm run eval:consistency) | 6-question interviews per suspect, EN + TR, including evidence confrontations, graded by a judge model against the private notes | 192 answers: consistent 99.5%, no invented facts 100%, no early reveals 98.4%, reveals on cue 99.5%, in character 99.5% |
The evals found real bugs that were then fixed. A killer gave up their motive after seeing one piece of evidence when two were required. An internal [Reply in Turkish.] instruction leaked into an answer. A Turkish pattern in the guard was too strict.
LLM-written mysteries fail in two ways: they contradict themselves, or they can be solved from the case file alone, without any interrogation. The pipeline (src/server/generator/) checks both:
npm run check:solver checks that the solver's input really is blind.Generation with a thinking model takes 3-12 minutes, longer than Vercel's 300-second function limit. The admin panel therefore starts a GitHub Actions workflow and shows its recent runs.
ChatModel interface (src/server/ai/), so the model is set in one place. If the main model hits its quota, errors, or sends nothing for 10 seconds, the same request goes to a fallback model.fetch is enough. Gemini (SSE streaming) and Turnstile are called directly. The app has 8 runtime dependencies.cp .env.example .env.local # fill in Supabase + Gemini keys (see comments in the file)
npm install
# apply supabase/migrations/*.sql to your Supabase project, then:
npm run db:seed # loads the hand-written sample case
npm run dev
Without GEMINI_API_KEY, development uses a mock model.
| Command | Purpose |
|---|---|
npm run check:leaks | Search the production build for secret case text |
npm run check:prompts / check:solver | Verify prompt and solver isolation |
npm run check:guard | Output-guard unit checks |
npm run smoke | Full game + cheating attempts against a running app |
npm run eval / eval:consistency | AI evals (use GEMINI_OFFLINE_API_KEY) |
npm run generate -- --difficulty=hard | Generate and check a new case |
src/app/[locale]/ pages (EN/TR), incl. case, interrogate, accuse, result, archive, lab, admin
src/app/api/ route handlers: interrogate, hint, accuse, game, player, cron
src/lib/case/ Zod case schema (public / secret split)
src/server/ai/ provider-agnostic model interface, Gemini, fallback, retry
src/server/interrogation/ suspect prompt, output guard, game store (Supabase RPCs)
src/server/generator/ case generator, logic checks, blind solver
supabase/migrations/ schema, RLS, game functions, rate limits
scripts/ leak/isolation checks, smoke test, evals, generator CLI