Walk into the story you love, and live it.
Liminal is the interactive layer for stories you already love. Streaming gave us an endless shelf of worlds and then left us watching from the outside. Liminal is the threshold you cross to get in. You pick a story, and Liminal rebuilds it as a world you can step inside: one you walk through (Realm) or one you hear and speak to with your eyes closed (Domain). Everything is generated for that story, personalised to you, and always true to the tale the author told.
- What Liminal is
- The two modes
- Architecture
- How a world is generated
- The episode and unlock loop
- Personalisation
- Tech stack
- Getting started
- Environment variables
- Deployment
- Project structure
Every story here can be entered, not just watched. You arrive already attached to a world, choose who you want to be inside it, and step through. From there you are not reading a plot, you are living a place: knocking on doors, pressing people who would rather stay quiet, and gathering the pieces of what really happened. The story remembers what you did, and it ends differently depending on the choice you commit to. The one thing it never does is rewrite the author's truth: your path is yours, but the story stays canon.
Two doors into the same generated world.
- Realm (visual). A top-down pixel world you actually move through. It is a real open world: each place is a full screen, and walking off an edge takes you to the next place, one screen over. Clue-holders are spread across different places, a fog-of-war minimap fills in as you explore, and locked places open as you listen to the story.
- Domain (audio). A purely audio experience for eyes closed. The story is spoken aloud in your language while a single wavy orb breathes with every line, and you answer by speaking out loud, hands free. Your choices bend the scene while the source truth stays intact.
Liminal is one Next.js app. Both modes sit on the same shared spine (catalog, canon, generation, providers), so it is a second face on one engine, not two projects.
flowchart TD
subgraph Client["Browser"]
R["Realm<br/>GameCanvas + HUD"]
D["Domain<br/>WavyOrb + voice loop"]
P["Reader persona<br/>(localStorage)"]
end
subgraph API["Next.js App Router (server)"]
W["/api/world/:id<br/>generate + stream + cache"]
DL["/api/dialogue"]
V["/api/voice · /api/domain/*<br/>tts · transcribe · scene · choice"]
LR["/api/listen · /api/resolve"]
end
subgraph Engine["Shared engine (lib/)"]
CAT["catalog"]
CAN["canon + canon-gen"]
BW["build-world<br/>(screen graph)"]
SB["domain/scene-brain"]
PROV["providers:<br/>text · image · voice"]
end
subgraph Ext["Providers + storage"]
GROQ["Groq / OpenAI<br/>(text)"]
OAI["OpenAI<br/>gpt-image-1 · TTS · Whisper"]
CACHE["public/generated (art)<br/>.cache/canon (secret, server-only)"]
SUPA["Supabase<br/>(optional world state)"]
end
R --> W & DL & LR
D --> V
P -.persona + memory.-> DL & V
W --> BW --> CAN --> CAT
V --> SB --> CAN
DL --> CAN
BW --> PROV
SB --> PROV
PROV --> GROQ & OAI
W --> CACHE
LR --> SUPA
CAN -. secret withheld .- CACHE
Guardrail: the story's secret (canon.arc.secret) lives only in the
server-side canon cache and is woven in only at the committed resolution. It is
never sent to the browser on any normal turn.
Pick a story and press Generate. The engine:
- Loads the story's canon, or writes a full canon for stories that have none (places, characters, the hidden truth, and the clues that point to it), from just the catalog entry.
- Paints each place in one consistent top-down pixel-art style (so a generated world matches the seeded ones and itself).
- Lays the places on a connected screen graph so they spread out and you travel between them by walking, with clue-holders in different places.
- Streams progress place by place, then drops you in. Generated worlds are cached so the next visit is instant. It never hangs: any failure offers a retry.
The three demo stories ship with art already made; every other story is built live the first time someone opens it.
Stories are broken into short episodes (in the spirit of a Pocket FM run). Each episode is a five to ten minute session that opens one new place. You "listen" to the next episode from the drawer and the gated place opens, with a chime and a jolt so an unlock always reads as an event. Binge several episodes to open a larger stretch at once. When you have gathered enough and reached the final place, you commit to a theory and choose how to end it, which produces materially different resolutions.
The problem Liminal solves is personalised interactive experiences: two people who enter the same story should never have the same one.
- Reader persona. You tell Liminal your name and what pulls you into a story (the truth, the people, or the mystery). It is remembered locally and follows you across stories.
- Characters who remember you. Each character keeps a private memory of what you have said to them, across sessions, so the next conversation builds on the last. They greet you by name and lean toward or away from you based on your persona, without ever changing the facts.
- A resolution that is yours. Your persona and the theory you commit to colour the ending, while the source truth stays intact.
- Next.js 16 (App Router) and React 19, TypeScript throughout.
- Tailwind CSS v4 (tokens in
app/globals.css). - A canvas overworld engine (
components/GameCanvas.tsx) for movement, the follow/edge camera, and rendering. - Swappable provider layer: text (
lib/providers/text.ts, Groq or OpenAI), images (lib/providers/image.ts, OpenAIgpt-image-1with Gemini / Hugging Face fallbacks), voice (lib/providers/voice.ts, OpenAI TTS + Whisper STT). - Supabase is optional (saved, resumable worlds); the game plays without it.
You need Node 20 or newer and pnpm.
pnpm install
cp .env.example .env.local # then fill in real values (see below)
pnpm devOpen http://localhost:3000, set your reader persona, pick a story, and press Generate. The first build of a world takes a minute or two while its places are painted, then it is cached and instant.
Text generation needs one provider; image generation needs one working image provider for any story without pre-made art.
| Variable | Purpose |
|---|---|
LLM_TEXT_PROVIDER |
openai (default) or groq. |
OPEN_AI_API_KEY |
OpenAI key (note this project's spelling). Used for text, images, TTS, and Whisper. |
OPENAI_TEXT_MODEL |
Defaults to gpt-4o-mini. |
OPENAI_IMAGE_MODEL |
Defaults to gpt-image-1. |
OPENAI_TTS_MODEL |
Defaults to gpt-4o-mini-tts. |
OPENAI_STT_MODEL |
Whisper model, defaults to whisper-1. |
GROQ_API_KEY, GROQ_TEXT_MODEL |
Optional, if using Groq for text. |
LLM_IMAGE_PROVIDER |
openai (recommended), gemini, or huggingface. |
SUPABASE_URL, SUPABASE_ANON_KEY |
Optional. Enables saved, resumable worlds. |
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY |
Optional public Supabase values. |
The provider layer tries your preferred image backend first and falls through to any other one that has a working key, so a single good key is enough.
The app builds cleanly for Vercel (next build). Two things to know:
- Set the environment variables above in the Vercel project settings.
- The three seeded stories play fully on Vercel with no generation. Runtime
generation of new worlds currently writes art to
public/generated/, and Vercel's serverless filesystem is read-only, so live generation on the hosted site needs image writes pointed at Supabase Storage (a follow-up). Locally, generation works end to end.
app/ routes: landing, /stories, /story/[id], /realm/[id], /domain/[id]
api/world/[storyId]/ dynamic world generation (stream + cache)
api/dialogue in-character, canon-true NPC dialogue (+ persona + memory)
api/voice OpenAI TTS for spoken lines
api/domain/ scene, choice, tts, transcribe, tutorial
components/
GameCanvas.tsx the open-world canvas engine
realm/ RealmWorld, RealmLoader, Minimap, DialogueBox, CharacterSelect
domain/ WavyOrb, DomainExperience, ChoicePrompt, useVoiceTurn
site/ landing + catalog: Hero, ModeSelect, StoryCard, PersonaBar
lib/
catalog.ts the story catalog
canon.ts, canon-types story canon (seeded + generated)
world/ canon-gen, build-world, world-cache, scene-map
domain/ scene-brain, session, databricks-log
providers/ text, image, voice
personalization.ts reader persona + per-character memory
content/canon/ the three seeded story canons
Built for the stories you already love.


