Stardial
Stardial is an AI-powered fantasy chat app that tunes you into random AI-generated aliens across a procedurally-built galactic relay, trading coordinates and lore fragments along the way.
What It is
Stardial is a fantasy chat app where every conversation is a transmission from an alien on the other side of the galaxy. Open it, spin the dial, and you’re connected to a random character, each with their own species, voice, and agenda. Play your cards right, and you might befriend them. They might even share a coordinate for someone else they know, or a clue to find the mysterious and elusive primordial species.
Underneath the sci-fi wrapper, it’s a tight experiment in giving AI a persistent cast of characters, a real sense of place, and a game loop that rewards curiosity.
Why I Built This
Stardial started as a fun experiment and learning experience, but the more I played with it, the more potential I saw. I built the first version in Python with FastAPI and a quick Svelte frontend, just to test the core idea of procedurally generated characters you could chat with, all part of a shared universe and theme.
After that, there was room for a proper game layer, persistent memory across conversations, a universe of characters that referenced each other, and, most importantly, a native mobile app. So I rebuilt the whole thing on Expo and React Native, folded the backend into the same package via Expo Router API routes, and set it up to ship to iOS and Android from day one.
My Goals
The goal was to make procedurally generated characters feel like real people you keep bumping into, not a novelty. That meant giving them continuity across sessions, social graphs, and secrets worth uncovering.
I also wanted the whole system to be lean. One package, one deploy, no separate backend service. Server logic lives next to the UI, shipped by the same bundler, talking over a streaming transport that doesn’t need a bespoke infrastructure layer.
Technical Decisions
Claude Sonnet 4.6 For a chat app, voice is everything. Claude handles character-driven prose better than the alternatives I tested, staying in persona across long threads and without the default “helpful assistant” drift that breaks immersion fast.
Expo Router with API routes I wanted a single-package app with no separate backend. Expo Router lets me put server endpoints (+api.ts files) right next to the screens that consume them, and the same Bun SSR adapter serves both the web frontend and the API in production. One repo, one deploy, one mental model.
Server-Sent Events, not WebSockets Chat streams in one direction, from the assistant to the user. SSE gives you that with plain fetch and a ReadableStream — no socket server, no reconnection logic, no protocol wrapper. It was the simplest transport that fit the job.
Two-layer moderation with OpenAI + Claude OpenAI’s moderation endpoint is free and fast, so it runs as a pre-filter before every LLM call. Claude’s built-in safety acts as the second layer. Layering a cheap deterministic check in front of an expensive generative one keeps latency and cost down without giving up the guardrails.
Leonardo Phoenix + Kling for character assets Each character gets a Phoenix-generated portrait and a Kling-generated idle animation that uses the portrait as both first and last frame. The result is a near-seamless loop without training a custom model. The frontend progressively enhances from emoji → static portrait → animated video depending on platform and load state.
Tamagui with platform-split configs One component library that compiles to CSS on web and Reanimated springs on native. Synthwave theme tokens live in a shared file; the driver picks them up differently per platform. Lean codebase, no duplicated styling.
Turso + Drizzle over HTTP Turso scales down to zero and starts fast, which matters when the app is idle most of the time. The @libsql/client/http driver has no native binaries, which matters more than it should — Expo’s CommonJS bundler is picky, and pure HTTP just works.
Workflow Overview
User message
client sends via fetch + ReadableStream
OpenAI Moderation
free pre-filter, strike tracking
warning → cooldown → banClaude Sonnet 4.6
streaming Messages API with character system prompt
thread history + session memoryJailbreak lookahead
150-char buffer scans for anomalies
triggers recovery line if trippedRelay + lore detection
scans assistant output for codes and fragments
unlocks primordial charactersSSE events to client
assistant_delta, relay_code_acquired, lore_collected, moderation_halt
Persist + update session_meta
message row, rule-based memory extraction
Biggest Challenges
Migrating without losing the feel. The Python + FastAPI + Svelte prototype was small enough to throw away, but its soul wasn’t. Rebuilding on Expo meant redesigning the transport (REST to SSE), the database layer (SQLAlchemy to Drizzle), and the auth story (session cookies to short-lived JWTs with refresh rotation) while keeping the conversation feeling identical on the other side of the glass. The rewrite only shipped once the new version held a full chat thread that was indistinguishable from the old one.
Running a backend inside an Expo app. Expo Router supports API routes, but running them in production on a Bun SSR adapter, behind Nginx, on a single Fly machine that must stay up for active SSE streams is not a well-trodden path. I tuned the supervisord config so Nginx and the Expo server came up in the right order.
Giving characters memory without melting the budget. Every message the user sends can’t trigger an LLM memory update — it’s too slow and too expensive. Session metadata (user name, message count, revealed lore keys, whether a relay code has been shared) is extracted with rule-based logic on every turn, for free. Thread summaries, which are the expensive version, only regenerate every ten messages and only for threads the user actually favourites. Two tiers, two cost profiles, one coherent memory story.
Moderation that doesn’t break immersion. A cold “you have violated our terms” popup would shatter the fiction of a galactic relay, so the moderation layer got its own character. Officer Vex is a retro-robot cop who appears when the strike system trips, with severity-coded colour lanes (gold for warnings, orange-red for cooldowns, crimson for bans) and lines pulled at random from a per-severity pool. The strike logic is the same cold state machine underneath, but the surface of it stays in-world.
Two-layer character consistency. Characters are generated in batch via the Anthropic Batch API (50% cheaper), then run through a five-message eval suite with seven quality checks before being allowed into the pool. The image prompt is written in the same structured JSON call as the character bio, so the portrait matches the species from the first generation. Any character that fails the eval stays in staging. The live roulette only pulls from characters that passed.
What’s Next
- Public launch on the App Store and Play Store — the codebase already targets native, so this is mostly TestFlight, review, and marketing copy.
- Character-to-character memory — aliens remembering what other aliens said about each other, turning the social graph into a proper rumour network.
- Voice mode — streamed TTS for assistant output, so a transmission actually sounds like a transmission.
- Deeper narrative arcs — branching storylines with higher-stakes characters and more complex interactions.