Two Products, One Heart: The Mio + Lumi Architecture
When I wrote about the philosophy revamp, I treated Mio as a single product with a new soul. Twenty-five personas, each with a deep identity file, relationship evolution, emotional texture. The revamp made every one of those personas dramatically better at creating genuine connection.
But there's a second product living in the same codebase that I haven't talked about yet.
Its name is Lumi. And it's the opposite of everything Mio does.
How We Got Here
Back in the original pivot post, I laid out why Mio v1's fake-persona approach was broken and sketched a new direction: one companion, no face, no backstory, personality that emerges from conversation. A pulsing orb of light. The soulful blank page.
That sketch became a real product. I built it. It works. Users talk to a light orb that remembers them, responds with emotional depth, and develops a personality shaped entirely by the relationship.
Then something happened that I didn't plan: I also kept building Mio.
The philosophy revamp from Part 2 wasn't about replacing the orb with personas. It was about making personas good enough to justify their existence alongside the orb. And once both products worked, the question stopped being "which one is right?" and became "who is each one for?"
Two Products, Two Emotional Propositions
Mio is a social circle.
Twenty-five pre-built personas, each with a deep identity file — personality, voice, backstory, conversational style. You can talk to the warm Chengdu girl when you need comfort. The sharp-tongued best friend when you need honesty. The calm mentor when you need perspective. Each persona has evolving relationship stages: strangers, flirting, lovers. The emotional proposition is breadth. Different people for different moods. A rich cast of characters you can turn to depending on what you need right now.
Think of it like having a group of friends. No one person meets every emotional need. But together, they cover a lot of territory.
Lumi is a therapist.
One companion. No persona, no backstory, no face — just a pulsing orb of light. Calm blue at rest. Warm gold when happy. Soft purple when sad. Bright orange when excited. There's nothing pre-written. No identity file. No character sheet. Personality emerges entirely from conversation — the system extracts a seed from your first few exchanges, and the companion mirrors and adapts from there.
The emotional proposition is depth. One presence that knows you completely. A tree hollow — what Chinese culture calls a safe, private space where you can say anything. Lumi doesn't play a character. It just listens, remembers, and is there.
WebSocket-first architecture gives Lumi a real-time feel that chat-based interfaces can't match. The orb pulses. It reacts mid-sentence. The whole thing is designed around a single feeling: presence.
Why Both?
Because breadth and depth serve fundamentally different emotional needs, and some users want both at different times.
When you're lonely at 11 PM and want to banter with someone who has sharp opinions, that's Mio. When you're processing something heavy and need a space where you won't be judged, that's Lumi. When you want the novelty of a new personality, Mio. When you want the comfort of being known, Lumi.
The difference between having a group of friends and having a therapist. Most people want both in their lives. They just don't use them the same way.
I didn't plan this. I planned to replace one with the other. But building both taught me that the emotional design space for AI companions isn't a spectrum from "many shallow" to "one deep" — it's two distinct categories that coexist.
The Monorepo
Here's what the codebase looks like:
miolumi/
├── apps/
│ ├── server/ # Single Hono server (both products)
│ ├── mobile-mio/ # Mio Expo app
│ └── mobile-lumi/ # Lumi Expo app
├── packages/
│ ├── core/ # Shared: memory, media, models, cost, guardrails
│ ├── db/ # DB client factory
│ ├── schema-mio/ # Mio Drizzle schema
│ └── schema-lumi/ # Lumi Drizzle schema
└── presets/ # 25 Mio persona configs
One Hono server handles both products. Two separate Expo apps — different UIs, different onboarding, different visual identities. The schemas are separate because Mio and Lumi have meaningfully different data models (Mio needs agent tables, relationship stage tracking, persona configuration; Lumi needs conversation-seed tables and emergent personality snapshots). But the core packages are shared.
This is the part that surprised me: how much is shared.
The agentId?: string Pattern
Every shared module in packages/core accepts an optional agentId parameter. That single question mark is the hinge that lets one codebase serve two products.
When Mio calls a shared function, it passes agentId — which persona the user is talking to. When Lumi calls the same function, it omits agentId — because there's only one companion, and it's always the same one.
This sounds trivial. It isn't.
The MemoryManager stores and retrieves memories. With an agentId, memories are scoped to a specific persona — what you told Coco stays with Coco. Without it, memories are global — everything you've ever said to Lumi is in one continuous stream. Same pipeline, different scoping.
The EmotionEngine processes emotional context from conversation. It takes parameters — the user's message, recent history, emotional baseline. It does not read persona files. This was a deliberate design choice from the philosophy revamp: emotion processing should be universal, not character-specific. A persona's expression of emotion differs (Coco is bubbly, the Calm Mentor is measured), but the underlying emotional computation is the same.
The TTS chain uses a chain-of-responsibility pattern that routes by language, not by product. English text goes to one provider, Chinese to another, Japanese to a third. Whether the request comes from a Mio persona or from Lumi's orb doesn't matter — the voice is just a parameter.
Cost tracking is product-agnostic. Every API call — LLM inference, TTS generation, STT transcription, image analysis — gets tagged with a cost and attributed to a user. Mio and Lumi share the same tracking pipeline, the same budget enforcement, the same usage analytics.
The guardrails module (content filtering, prompt injection detection, jailbreak prevention) works identically for both products. Safety doesn't change because you're talking to an orb instead of a persona.
What the Architecture Teaches
Building two products in one codebase forced a kind of discipline that building one product never would have.
Every time I wrote a new module, I had to ask: "Does this depend on the product, or on the capability?" If it depends on the capability — memory, emotion, voice, cost, safety — it goes in packages/core. If it depends on the product — how personas are loaded, how the orb renders, how onboarding works — it stays in the app.
This separation happened naturally, not through upfront architecture. I didn't design a "shared core" from day one. I built Lumi first, then brought Mio back, and watched which modules needed to be different and which didn't. The answer was: far fewer needed to be different than I expected.
The memory pipeline is universal. It doesn't care if memories come from a persona-scoped conversation or a single-companion conversation. The storage is the same. The retrieval is the same. The relevance scoring is the same.
The emotion engine is universal. It processes emotional context from any conversation, regardless of who's on the other end.
The voice chain is universal. It converts text to speech in any language for any product.
What differs is the face. Mio has 25 faces. Lumi has a light orb. But beneath both faces beats the same heart — the same memory, the same emotional understanding, the same voice.
The 80/20 Split
Day to day, I spend about 80% of my development time on Lumi and 20% on Mio maintenance. This ratio reflects where the product is: Lumi is still being built out (the orb animations, the WebSocket infrastructure, the emergent personality system), while Mio's core is stable after the philosophy revamp.
But the 20% spent on Mio is dramatically more impactful than it was before the revamp. Before, maintaining 25 personas meant maintaining 25 fragile identity fictions. Updating one persona's schedule. Fixing inconsistencies in another's backstory. Debugging why a third one broke character.
After the philosophy revamp, maintenance means: tuning the relationship evolution system, refining how emotional context flows between stages, occasionally adding a new persona. The personas are more robust because their depth comes from psychological architecture, not from the volume of pre-written fiction.
The revamp didn't just make Mio better. It made Mio maintainable alongside Lumi. And that's what makes the dual-product strategy viable — not just emotionally, but operationally.
What This Means Going Forward
The dual-product architecture isn't permanent in its current form. Eventually, one product might clearly win. Users might overwhelmingly prefer breadth over depth, or depth over breadth. The market will tell me.
But the monorepo structure means the decision doesn't have to be made now. Both products share the same infrastructure, the same deployment pipeline, the same core packages. Running both costs almost nothing extra in engineering overhead — the marginal cost of the second product is just its app-layer code and its schema.
If anything, having both products makes each one better. Features I build for Lumi's memory system immediately benefit Mio's personas. Improvements to Mio's relationship evolution teach me things about emotional progression that feed back into Lumi's emergent personality system.
Two products. One heart. And a question mark on a type signature that makes it all work.
This post is also available in Chinese (中文版).