
- Sector
- SaaS · Product
- Year
- 2026
- Role
- Architect · full-stack build
- Outcome
- Symfony 7 modular monolith · React 19 + TipTap editor · my own product
What needed fixing.
Serious writers assemble a toolchain — one app for prose, another for the wiki, a third for sharing — and none of them know about the others. The silver amulet vanishes between chapter 3 and chapter 11 and nobody notices until a reader does. The three surfaces a novelist actually needs belong in one app; keeping them apart is the bug.
How I built it.
The backend is a Symfony 7 modular monolith — one application, bounded contexts (publishing, codex, search) that talk through domain events and narrow interfaces, with row-level tenancy enforced by voters on every resource. API Platform exposes it; Doctrine and Postgres store it, with prose held as JSONB. The front end is React 19 and TypeScript around a TipTap editor, where @mentions are live nodes — rename a character in month seven and every mention updates on read, never stale. Search is Meilisearch across prose and codex at once. It's the kind of system where the next feature is days of work, because the boundaries were drawn on day one.
A novelist types @Marta mid-sentence and sees her character card inline, without leaving the page. Three things most writing tools keep apart — manuscript, codex, chronology — in one app.
- 01Unify manuscript, codex and chronology in a single writing application
- 02Keep prose and the character codex linked at the schema, not bolted together
- 03Build a backend whose bounded contexts can grow without entangling
- 04Make @mentions live references that never go stale on rename
- 05Search prose and codex together, with chapter-level facets
- Symfony 7 modular monolith — bounded contexts via domain events and service interfaces
- API Platform + Doctrine + Postgres 16 (prose stored as JSONB)
- Row-level tenancy enforced by Symfony voters on every resource
- React 19 + TypeScript front end with a TipTap rich-text editor
- Live @mention nodes with inline card lookup — rename once, update everywhere
- Meilisearch global search across prose, codex and notes with facets
- Autosave with rolling revision history; codex CardSets; writing stats
The page where three tools become one
Open a chapter and the manuscript fills the page. Type @Marta and her character card surfaces inline — eye colour, the town she's from, the chapter she first appeared in — without a second window, a second app, or a broken sentence. That single interaction is the whole thesis: prose, codex and chronology belong on the same surface.

- Editor
- TipTap 2
- Mentions
- Live nodes, never stale
- Autosave
- Every 2s + rolling history
- Frontend
- React 19 · TypeScript
Rename a character in month seven and every mention updates on read.
What greets a new writer
Before any of the machinery, there's a page that has to make a novelist feel at home — warm paper, a big serif headline, the unmistakable sense that this was built by someone who reads. The public face of Nightshelf is its own small product: an editorial marketing site that explains the idea in the same quiet voice the app speaks in, rather than the usual feature-checklist shouting. First impressions are a design problem too, and most software treats them as an afterthought.

- Type
- Newsreader + IBM Plex Sans
- Build
- Server-rendered for SEO
- Tone
- Editorial, not feature-checklist
The author's room
The editor is the centre of a whole studio, not a text box with ambitions. Six kinds of card — characters, locations, objects, factions, events, lore — each with its own colour and its own short form, all reachable from a single create dialog that never makes you choose a screen first. Select a sentence and a sidenote opens in the margin, anchored to the exact words, not floating somewhere near them. Mention a character who doesn't exist yet and the autocomplete offers to make them on the spot. None of it asks you to leave the page you're writing on.
Cards live in card sets — a writer's world bible, the place a series keeps its facts consistent. One set can attach to several books at once, so the lore of a trilogy lives in a single source that every volume reads from; rename a faction once and three books agree. Sets can be shared with a named team or kept private, made public as a worldbuilding showcase or unlisted behind a link. A keyboard-first search reads across chapters, cards, sets, guides and people at once, with a live preview beside the results. And because people write for hours, the whole thing themes — warm paper by day, dusk and e-ink for the late shift.
A world bible attached to a whole series — one source the books agree on.
Search across everything you've written, previewed live.
Wordcount and rhythm, for the days motivation needs evidence.Writing in the open
Writers like to be read, so the studio has a public side. Every author gets a profile at their pen name — published books, the world bibles they've chosen to show, a quiet calendar of a year's writing, and only the stats they opt into. It's closer to a writer's public shelf than a social network: no follower counts to perform for, no feed to feed. A world bible or a finished chapter can be handed to a beta reader as a link that opens cleanly on a phone, which is where half of reading actually happens.
And because a tool for a craft attracts people who want to talk about the craft, there are guides — a knowledge base that's part official and part written by the writers themselves. How to keep a series bible that pays off, how to take a manuscript from blank page to export, contributed in Markdown and read like the rest of the product: serif prose, no clutter. The best documentation for a writing tool turns out to be writing.
A writer's public shelf: books, world bibles, a year of writing.
Part official, part community — the craft, documented by the people doing it.A backend drawn on day one
All of it runs on one Symfony 7 backend — a modular monolith, organised into bounded contexts (publishing, codex, search, sharing) that speak through domain events and narrow interfaces. The data model is where the foresight shows: a card belongs to a card set, and a card set attaches to many books, which is the single decision that turns "a codex per book" into "a shared universe across a series" without a rewrite. Prose is stored as JSONB in Postgres, Meilisearch indexes prose, cards, guides and people together, and tenancy is enforced on every resource. The boundaries were drawn before the first feature, which is why the next one is days of work, not weeks.
