Notes de développement
Un visual novel est un ensemble d’images. Pourquoi le Builder est-il devenu si complexe ?
Comment un petit prototype appelé Fibber est devenu une plateforme distribuée avec collaboration, publication versionnée et runtime narratif partagé.
Vu de l’extérieur, un visual novel paraît presque trivial : un décor, un personnage détouré, une zone de texte, quelques choix menant à la scène suivante et un peu de musique. C’est exactement ainsi que je le voyais en commençant la première version.
Je pense toujours que le cœur reste simple. Aujourd’hui, surtout avec l’aide de l’IA, presque tout développeur peut créer un lecteur de visual novels. La difficulté commence quand l’objectif n’est plus de produire une histoire, mais de construire un outil dans lequel une autre personne, peut-être même un enfant, pourra créer, tester, publier et continuer à faire évoluer sa propre histoire.
La première version n’avait qu’une mission
Avant de s’appeler Romergo, le projet s’appelait Fibber. Il assemblait une quête à partir de scènes reliées, me permettait de les remplir de textes et d’images et gérait les choix à embranchements. La stack reposait sur TypeScript, React, Strapi et Ant Design.
Cette version remplissait son rôle. Le graphe rendait l’histoire visible, les scènes pouvaient être modifiées et le résultat prévisualisé. Pour un prototype, cela suffisait. Cela a aussi suffi pour révéler le vrai problème.
Le premier véritable goulot d’étranglement n’était pas le code
Quand nous avons commencé à remplir une vraie quête de textes et d’images, assembler une scène de dix minutes pouvait prendre presque une journée entière. Créer une scène. Importer un décor. Importer un personnage. Les placer. Ajouter les dialogues. Relier la scène suivante. Recommencer.
Une partie du problème venait de l’interface. J’ai amélioré l’UX, accéléré les imports et fiabilisé la publication. Mais le coût principal restait intact : une personne devait encore effectuer chaque petite action à la main.
Et si deux personnes pouvaient créer en même temps ?
La première réponse était simple : si une personne constitue le goulot d’étranglement, permettons à plusieurs personnes de travailler sur la même histoire. J’ai commencé avec Liveblocks, appris de ce modèle, puis évolué vers ma propre couche collaborative fondée sur Yjs.
L’expérience ci-dessous ressemble à deux fenêtres d’éditeur qui bougent ensemble. En dessous se cache un changement bien plus profond dans la logique du produit : les modifications deviennent des mises à jour d’un document partagé, et non des envois de formulaires isolés. De là viennent la présence, les reconnexions, la gestion des conflits, les permissions et l’idée qu’un projet reste toujours vivant.
Chaque raccourci utile a fini par devenir une limite
Strapi était un excellent moyen d’obtenir rapidement un backend, mais chaque nouvelle fonction propre aux histoires demandait au CMS de se comporter un peu moins comme un CMS. Ant Design a donné vitesse et cohérence à la première interface, puis le produit s’est progressivement retrouvé contraint par le système visuel de quelqu’un d’autre.
Une itération serverless ultérieure fondée sur Remix a marqué une avancée importante. Pourtant, la manière dont j’avais réuni application, médias et déploiement ne m’offrait pas la liberté horizontale dont le produit commençait à avoir besoin. Aucun de ces choix n’était une erreur : chacun m’a donné assez de temps pour découvrir la contrainte suivante.
Les images ne sont pas des pièces jointes. Elles sont une infrastructure.
Les médias sont devenus une leçon à part entière. Une image d’histoire doit être importée une fois, transformée si nécessaire, stockée en sécurité et livrée rapidement à n’importe quel joueur dans le monde. La garder auprès d’un serveur applicatif déjà en croissance ne faisait qu’alourdir le backend. La première solution séparée fut donc Cloudinary.
Un soir, j’ai ouvert son tableau de bord et découvert qu’un seul utilisateur avait consommé environ 500 Mo en important la même image des centaines de fois. Mon correctif d’urgence comparait les empreintes des images et refusait les doublons. Cela fonctionnait, mais des limites de débit et des quotas auraient traité ce comportement plus directement. La plus grande surprise concernait la bande passante : une soirée de tests pouvait consommer plus de dix pour cent du quota gratuit.
Cet épisode a changé le modèle. Les médias ne pouvaient plus être un simple champ d’une scène. Il leur fallait leur propre pipeline de stockage, déduplication, livraison, cache et contrôle d’accès.
À quoi ressemble aujourd’hui cette idée simple
Le Romergo actuel sépare le Builder du lecteur, tout en leur faisant utiliser le même runtime narratif. L’aperçu dans l’éditeur et une partie publiée suivent les mêmes règles, ce qui rend les divergences de contrat beaucoup plus difficiles à introduire.
Yjs synchronise le projet modifiable entre les collaborateurs. La publication crée un instantané vérifié des données et des médias de l’histoire. Les auteurs peuvent donc continuer à modifier le brouillon sans changer discrètement la version déjà utilisée par les joueurs. L’API repose sur Hono, l’interface utilise les primitives shadcn et Radix, et la couche cloud assure coordination distribuée, stockage objet et diffusion par CDN.
Ce runtime partagé transporte désormais l’histoire vers le navigateur et la PWA, Telegram et une expérience Discord synchronisée. La compatibilité entre versions, le fonctionnement hors ligne et l’état multijoueur ne sont plus des effets secondaires autour d’une page. Ce sont des systèmes produit à part entière.
Architecture actuelle
Une histoire modifiable. Un contrat publié.
Une vue d’ensemble de l’architecture actuelle de Romergo.
1. Projet modifiable
- Builder UI: Scènes · flux · médias
- AI / MCP: Opérations d’édition authentifiées
- Realtime / Yjs: Durable Objects · document de travail partagé
- Editor API: Écritures CAS · matérialisation du brouillon
- Instantané runtime du brouillon: PublishedQuestLocalizedContentV2 · D1 / R2
- Médias du brouillon: Objets R2 · métadonnées D1
2. Publication versionnée
- Vérifications avant publication: Schéma runtime · attestation de l’auteur · présence des médias
- Publication immuable vN: Instantané runtime · médias copiés et remappés
- Contrat de visibilité: Public · non répertorié · privé
3. Un contrat d’exécution
- Instantané du brouillon: Dernier état modifiable
- Publication vN: État stable du lecteur
- Player-runtime partagé: Scènes · transitions · conditions · variables · sauvegardes
- Aperçu Builder: Exécute le brouillon avec le même moteur
4. Canaux de lecture
- Web / PWA: Lecteur dans le navigateur
- Telegram Mini App: Lecteur intégré
- Discord Activity: Jeu de groupe synchronisé
5. Infrastructure de plateforme
- Clerk + OAuth: Identité et accès MCP
- Hono Workers: Services API et MCP
- Cloudflare D1: Projets · versions · métadonnées
- Cloudflare R2: Médias · grands instantanés runtime
- Durable Objects: Salles Yjs · sessions Discord
- Progression du joueur: Synchronisation D1 · file locale hors ligne
Le visual novel reste un ensemble d’images
Ce qui est amusant, c’est que l’hypothèse de départ a survécu. Un visual novel reste composé de décors, de personnages, de textes, de choix et de sons. Romergo est devenu complexe parce que tout ce qui entoure ce cœur simple devait devenir fiable : collaboration, médias, publication, versions, jeu hors ligne et plusieurs façons de vivre la même histoire.
Si cette évolution m’a appris une chose, c’est qu’il faut mesurer tout le parcours humain, pas seulement le code qui affiche l’écran final. Le lecteur n’a peut-être besoin que de cinq primitives. Un produit qui aide quelqu’un à transformer une idée en histoire terminée et jouable a besoin d’un système.