Write both sides of the conversation
I built SelfVerse to put different perspectives into a conversation someone can read, revise, and return to. Choose a character, write a message, switch speakers, and respond from another point of view. The user writes every side of the exchange.
A conversation about starting a project might bring together me, Sage, Devil, and Judge. One voice wants to begin, another asks what matters in the long run, a third follows the tempting idea, and a fourth considers a fair compromise. These are possible interpretations; users decide what each character means and how they speak.
Selecting a speaker also changes the visual viewpoint. That character’s messages move to the right, while the others appear on the left. The same conversation can be read from each participant’s position without changing its authorship.
The idea began as a Swift application concept. I moved the active implementation to the web so the writing experience could work across phones, tablets, and computers, with installation and offline use through a Progressive Web App.
Give each perspective a recognizable identity
The interface has three areas: Messages, Moments, and Contacts. Messages holds longer conversations. Moments offers a private feed for shorter reflections, with likes and comments written as the selected character. Contacts manages the cast, and each profile collects that character’s posts and provides a way to start a conversation.
The current character library contains 55 illustrated characters plus me. Familiar roles such as Teacher, Judge, Gardener, and Librarian sit alongside Sage, Dragon, Phoenix, and Magic Mirror. A concrete identity offers a starting point for imagining a voice, while leaving its meaning open to the writer.
I developed the portrait set with an image-generation tool, revising the illustrations individually and checking their silhouettes, expressions, and circular crops at small sizes. Restrained colors and softly curved shapes make the portraits recognizable without competing with the writing. All 55 runtime images fit into approximately 325 KiB of local WebP assets.
The default me avatar is a charcoal circle showing m in English or 我 in Chinese. It gives the writer a visible place in the conversation without assigning a face or appearance. Bailey and LaFrance’s research on default avatar icons found that some apparently neutral human icons were perceived as male. A later CSCW study recommended representations without gender cues in relevant contexts. Those findings informed this specific design choice.
Users can rename any character or upload an image, adjust its zoom and crop, and save a compact 256 × 256 WebP processed locally. Names remain display values: stable IDs connect characters to their messages and posts. A rename updates their presentation throughout the app, and two characters with the same name remain distinct authors. Removing a character from active selection preserves their historical identity.
Treat a saved message as a committed record
I built SelfVerse with React 19, TypeScript, and Vite, using Immer for immutable state updates and IndexedDB for persistent records. The model separates characters, conversations, messages, posts, comments, and likes, joined through stable IDs.
The storage layer publishes a change to shared application state only after its database transaction succeeds. Each mutation reads the latest snapshot, applies the update, validates the result, writes it, and waits for the transaction to complete. Failed or aborted writes report an error and retain the previous committed state. Supported browsers receive a strict durability hint, with a fallback where that option is unavailable.
Several tabs can be open at once. A BroadcastChannel announces committed changes, while focus and visibility events provide additional refresh points. Revision checks prevent an older read from replacing newer data.
Drafts need a different kind of care. They save after a short debounce and when the conversation is left or hidden. Conflict checks protect text when another window has saved a newer draft. Restoration guards also prevent an old editor from overwriting an imported backup. These transitions matter because a writing app needs to preserve the work between sending messages as well as the finished conversation.
Make a growing collection searchable and recoverable
Search covers conversation titles, messages, Moments posts, and comments across the complete saved collection, including entries outside the visible history page. Results show context and highlighted matches. Selecting one opens the original location and focuses the relevant entry; returning to search retains the query.
Matching is literal and case insensitive. Multiple terms must occur in the same title or text body, giving English and Chinese queries a predictable meaning. Search remains in the browser and does not alter the stored writing.
Long histories use bounded rendering. Chats initially show 100 messages, feeds show 50 posts, and posts show 30 comments, with earlier entries available on request. A distant search match opens a limited window around the result. The full collection remains searchable and travels with its backup.
JSON export includes conversations, drafts, posts, comments, custom names, historical identities, and uploaded avatars. Import validates the schema, IDs, duplicate records, references, timestamps, image formats, and size limits before replacing anything. An invalid backup leaves the current collection intact. Settings also exposes record counts, storage estimates, persistence status, and the last backup time.
Personal writing and selected photos stay in this browser’s IndexedDB. Moving to another browser or device uses export and import. Backups are unencrypted JSON files, so they belong with the user’s private documents; keeping an external copy also protects against cleared browser data.
Keep the writing comfortable across languages and screens
English and Simplified Chinese cover navigation, settings, actions, errors, and preset character names. Localization happens at the presentation layer. Switching languages translates unchanged default labels while preserving custom names and everything the user has written.
Text input required careful handling of composition events. Confirming a Chinese or Japanese input-method candidate should not send a message or save an edit. I handled that distinction explicitly, including Safari’s composition behavior.
Phones use bottom navigation and one conversation pane. Wider screens can keep the conversation list beside the active chat, with a centered reading width for messages and the composer. Safe-area insets accommodate screen cutouts and the home indicator. Visual Viewport measurements adapt the available space when a mobile keyboard opens, and short screens receive more compact controls.
Principal controls have 44-pixel interaction targets. Dialogs keep focus inside, isolate the background, support Escape, and return focus when closed. Participant controls support keyboard navigation, visible menus supplement long presses, and pointer movement cancels a pending long press when scrolling begins. Reduced-motion and increased-contrast preferences receive corresponding adaptations.
Bring the application offline without interrupting a draft
Workbox, through the Vite PWA plugin, caches the application shell, scripts, styles, icons, and bundled portraits. After a successful online visit prepares that cache, the application can reopen offline and use its local IndexedDB records.
Updates are offered through an explicit action instead of automatically reloading an open draft. Default portraits belong to the application cache; custom cropped images belong to user records and are included in backups. That distinction keeps the artwork available offline without copying it into every exported collection.
Immer preserves unchanged references so memoized message rows and avatars can avoid unnecessary rendering. Date formatters, normalized search text, validation results, and size calculations reuse unchanged work as well. Rendering a small part of a conversation and preserving the whole record are separate responsibilities, and I designed each around the way it is used.
Verify the transitions that could lose someone’s work
The October 10, 2026 implementation passed 72 unit tests, together with TypeScript compilation and the production build. The suite exercises storage failures, concurrent updates, malformed backups, character upgrades, localization, search, and avatar selection.
Browser coverage uses Playwright projects for desktop Chromium, iPhone WebKit, Android Chromium, and iPad WebKit. Scenarios cover search navigation, message grouping, focus handling, custom avatars, orientation changes, keyboard-sized viewports, and safe-area spacing. A 5,000-message conversation checks bounded initial rendering and complete backup export. The latest portrait update also included targeted browser checks for custom-image persistence, renaming, reset behavior, and bilingual defaults.
Research on self-talk helped frame the original idea. Kross and colleagues studied how forms of self-reference affected psychological distance during introspection. I used that work as context for exploring changes of perspective through writing, rather than as evidence of an outcome for this application.
SelfVerse brought together an interaction I wanted to use and the engineering needed to keep it dependable. Changing a speaker should feel immediate; saving a message should mean it was committed; restoring a backup should preserve the meaning of its authors and history. Those details let someone concentrate on the conversation they are writing.