Product evolution

A visual novel is a set of pictures. Why did the builder become so complex?

How a small prototype called Fibber grew into a distributed platform with collaboration, versioned publishing, and one shared story runtime.

A visual novel looks almost trivial from the outside: a background, a character with transparency, a text box, a few choices pointing to the next scene, and some music. That was exactly how I saw it when I started building the first version.

I still think the core is simple. Today, especially with AI beside you, almost any developer can make a visual novel player. The difficulty begins when the goal is no longer to make one story, but to build a tool in which another person, perhaps even a child, can create, test, publish, and keep evolving their own story.

The first version had one job

Before Romergo was called Romergo, it was Fibber. It assembled one quest from connected scenes, let me fill those scenes with text and images, and supported branching choices. The stack was TypeScript, React, Strapi, and Ant Design.

That version did its job. The flow graph made the story visible, scenes could be edited, and the result could be previewed. For a prototype, that was enough. It was also enough to reveal the real problem.

The first real bottleneck was not code

When we began filling a real quest with text and images, a ten-minute scene could take almost a full day to assemble. Create a scene. Upload a background. Upload a character. Place them. Add dialogue. Connect the next scene. Repeat.

Some of that was an interface problem. I improved the UX, sped up uploads, and made publishing more reliable. But the largest cost remained untouched: a person still had to perform every small action by hand.

What if two people could build at the same time?

The first answer was simple: if one person is the bottleneck, let several people work on the same story. I started with Liveblocks, learned from that model, and later moved toward my own collaboration layer with Yjs at its core.

The experiment below looks like two editor windows moving together. Underneath it is a much bigger change in how the product thinks: edits become updates to a shared document instead of isolated form submissions. From there come presence, reconnects, conflict handling, permissions, and the expectation that a project is always alive.

Every useful shortcut eventually became a boundary

Strapi was an excellent way to get a backend quickly, but every new story-specific feature asked the CMS to behave a little less like a CMS. Ant Design gave the first interface speed and consistency, then gradually made the product feel constrained by somebody else’s visual system.

A later Remix-based serverless iteration was an important step forward. Still, the way I had combined the application, media, and deployment did not give me the horizontal freedom the product was beginning to require. None of these choices was a mistake: each one bought enough time to discover the next constraint.

Images are not attachments. They are infrastructure.

Media turned into its own lesson. A story image has to be uploaded once, transformed when needed, stored safely, and delivered quickly to any player in the world. Keeping it beside an already growing application server only made the backend heavier, so the first separate solution was Cloudinary.

One evening I opened its dashboard and saw that a single user had consumed about 500 MB by uploading the same image hundreds of times. My emergency fix compared image hashes and rejected duplicates. It worked, but rate limits and quotas would have addressed the behaviour more directly. The larger surprise was bandwidth: one evening of testing could consume more than ten percent of the free allowance.

That episode changed the model. Media could no longer be a field on a scene. It needed its own pipeline for storage, deduplication, delivery, caching, and access.

What the simple idea looks like today

The current Romergo separates the Builder from the player, while both use the same story runtime. A preview inside the editor and a published playthrough follow the same rules, which makes contract drift much harder to introduce.

Yjs synchronizes the editable project between collaborators. Publishing creates a checked snapshot of the story data and its media, so creators can continue changing the draft without quietly changing the version that players are already running. The API is built with Hono, the interface uses shadcn and Radix primitives, and the cloud layer provides distributed coordination, object storage, and CDN delivery.

That shared runtime now carries the story into the browser and PWA, Telegram, and a synchronized Discord experience. Compatibility between releases, offline behaviour, and multiplayer state are no longer side effects around a page. They are product systems of their own.

Current architecture

One editable story. One published contract.

A generalized view of Romergo’s current architecture.

1. Editable project

  • Builder UI: Scenes · flow · media
  • AI / MCP: Authenticated editing operations
  • Realtime / Yjs: Durable Objects · shared working document
  • Editor API: CAS writes · draft materialization
  • Draft runtime snapshot: LocalizedContentV2 · D1 / R2
  • Draft media: R2 objects · D1 metadata

2. Versioned publishing

  • Publish checks: Runtime schema · author attestation · media existence
  • Immutable publication vN: Runtime snapshot · copied and remapped media
  • Visibility contract: Public · unlisted · private

3. One execution contract

  • Draft snapshot: Latest editable state
  • Publication vN: Stable player state
  • Shared player-runtime: Scenes · transitions · conditions · variables · saves
  • Builder preview: Runs the draft through the same engine

4. Playback channels

  • Web / PWA: Browser player
  • Telegram Mini App: Embedded player
  • Discord Activity: Synchronized group play

5. Platform infrastructure

  • Clerk + OAuth: Identity and MCP access
  • Hono Workers: API and MCP services
  • Cloudflare D1: Projects · versions · metadata
  • Cloudflare R2: Media · large runtime snapshots
  • Durable Objects: Yjs rooms · Discord sessions
  • Player progress: D1 sync · local offline queue

The novel is still just pictures

The funny part is that the original assumption survived. A visual novel is still backgrounds, characters, text, choices, and sound. Romergo became complex because everything around that simple core had to become dependable: collaboration, media, publishing, versioning, offline play, and several ways to experience the same story.

If this evolution taught me one thing, it is to measure the entire human path, not only the code that renders the final screen. The player may need five primitives. A product that helps someone turn an idea into a finished, playable story needs a system.

Back to dev blog