· case-study · 4 min read
Building a Web Novel Platform from Scratch — One Codebase, Three Audiences
Authors write, readers read, admins watch the dashboards. Three different user types, three different screens — and one full-stack codebase that has to keep all of them happy.
In one sentence
Authors write, readers read, admins watch the dashboards. We built a web novel platform with three distinct user roles end-to-end — planning, design, backend, infrastructure, all from one team.
No 3D, no AI on this one. Just "a normal web platform." Which, as you'll see, is sometimes the hardest kind to make.
Three users, three screens
The first decision in any platform build is who uses it. This project had three from day one:
- Authors — write novels, publish episodes, manage their works
- Readers — read public works without an account; bookmark with one
- Admins — manage all users and novels, watch dashboards for platform health
These three each get different pages, different permissions, different UX — so we were essentially building three apps inside one codebase.
"Novel > Episode" as a data structure
Web novels aren't just blog posts. A novel can hold dozens or hundreds of episodes, each at a different publish state, some free, some paid. We modeled it like this:
User ──────────────┐
│
Novel │
├─ id │
├─ authorId ──────┘
├─ title
├─ genre
├─ status (draft/public/private)
└─ Episodes []
├─ id
├─ order
├─ title
├─ content (rich text)
├─ status (draft/published)
└─ publishedAt
The order field is where the trouble lives. Authors reshuffle episodes — "I want to insert a new episode 5 in the middle." Handle that and you can't make order a plain integer.
Renumbering every episode every time someone inserts one will kill the database. The trick: insert the new episode at order
4.5, and only normalize back to integers in a periodic batch when fractions get messy. This is the standard approach.
"Read without an account" rewrote our caching strategy
A core decision early on was that public episodes should be readable without login. That single line completely changed our backend caching.
- Public episodes are cached at the CDN. Logged-in or not, same cache hit
- Episode content body never embeds dynamic data like "like count" → fetched separately by a small API
- Cache invalidates only when an author edits or publishes — invalidate the single episode URL
The result: >90% of traffic served from the CDN. The server only handles author edits and reader interactions like likes/comments.
Admin dashboard split into its own app
Initially we planned to keep the admin screens as routes inside the main React app. We changed our minds quickly:
- The heavier the admin gets, the heavier the bundle every reader downloads
- Permission separation is weaker than code separation. One slip and you've leaked access
- Admin UX is dashboard-heavy — uses a different set of libraries (charts, tables)
So admin lives as a separate React app on a separate domain, sharing the backend. That single decision dropped the public bundle by 30%+.
"Edit" and "publish" must be different operations
The most-requested feature from authors was unexpected:
"My in-progress edits should never accidentally go live."
We started with content = a single text blob per episode. That meant readers were watching whatever the author was typing in real time. So we split each episode into two content slots:
Episode
├─ draftContent
├─ publishedContent
├─ status
└─ publishAt (scheduled publish)
Authors always edit draftContent. Hitting "Publish" copies it into publishedContent. A small change, but author confidence jumped immediately.
Responsive design when the user is 90% mobile
Web novels are read >90% on mobile. That assumption changes everything.
- Body text 16–18 px by default — looks small in mockups, perfect on phones
- Line height 1.7–1.8 — comfortable for an hour of reading
- Dark mode as the default — overwhelming majority of reading happens at night
- Swipe gestures for episode navigation — buttons are secondary
Compound those small details and session time differs by 30%+. We initially classified these as "polish details." After beta testing, we reclassified them as "core features."
What we walked away with
"Normal web platforms" are the hardest
When you don't have a flashy 3D layer or AI to hide behind, every evaluation lands on speed, stability, and small UX details. Nowhere to hide. These are the projects that actually expose engineering quality.
Caching strategy matters as much as data modeling
"Public content on CDN, dynamic data via separate API" — that one rule cuts infra costs roughly 5×. Bake it into the model from day one and you'll thank yourself later.
Separate the admin app
Don't keep it in the same React app. Permissions, bundle size, library divergence — all three argue for separation.
Things people ask
How long does a similar content platform take? Three roles, standard scope: 12–16 weeks. Add subscriptions/payments and it's +4 weeks.
How did you implement the author editor? ProseMirror-based rich text. Auto-save every 2 seconds, inline image upload, automatic cross-linking between episodes. The priority was never breaking the writer's flow.
How far does this scale? A million users and a million episodes is fine on PostgreSQL with proper indexes. Beyond that we typically split episode bodies into a separate object store.
What does the admin dashboard look like? Around 15 KPI cards (daily signups, active authors, popular genres, per-episode traffic) plus time-series charts. Every metric is precomputed nightly so the dashboard responds instantly.
If you're starting a content platform — web novels, courses, newsletters, blogs — get in touch with the user roles and target scale. The first call usually maps out the data model and caching strategy with you.
Hammergrid Lab is a creative engineering studio building React/Node.js full-stack platforms alongside 3D and AI tools.
Related services
3D web, 3dweb, 3D configurator, 3D product configurator, 3D product viewer, WebGPU, Three.js