Skip to content
Case study — 04 / 04 · index

Inkwell

A comics reading platform built for a client, rebuilt so its whole backend runs in the browser and anyone can open it with one link.
Project
Inkwell
Role
Design and build, solo
Status
Live demo
Live
Inkwell · the film
01Overview
ProductInkwell, a comics reader
SectorDigital comics and webcomics
Year2025 to 2026
ScopeReader, accounts, CMS, importer, in-browser backend
TeamSolo, both builds
Description

Inkwell is a comics reading platform I built for a client as a full-stack site, then rebuilt as a public demo that runs entirely in the browser: no server, no database, nothing to sleep.

The client build is three services behind one gateway. A React and Vite front end talks to an Express and Mongoose API on MongoDB for accounts and reading data, and to a Strapi 5.34 CMS for the catalogue, on SQLite by default and Postgres by configuration. A plain Node gateway on port 3000 starts both as child processes and routes between them, so the gateway, the API, the CMS and the built front end ship as one Docker image, and Docker Compose runs MongoDB beside it. Readers get filters, Cmd+K search, a reader with long strip and single page modes, email verification, progress, history, bookmarks, four shelves, streaks, 20 achievements, reviews and comments; admins get a chunked zip importer.

Inkwell keeps that front end, rebranded, and moves the backend into the page. A router answers the old /api, /cms/api and /admin paths with the same JSON and status codes, so the app cannot tell its servers are gone. The catalogue is 11 public domain, CC0 and Creative Commons series, 42 chapters and 343 pages, each one credited; one click signs in a demo reader with three weeks of history; and the importer reads a zip without it leaving the browser. It is static files on Vercel at inkwell.alexsmiths.com, and it makes no requests to any other host.

The client build needs a gateway, an API, a CMS and a database running before anyone sees a page. I wanted it to be one link anyone could open, so I moved the backend into the browser.

Flagship · the backend in the page

A full-stack site, with no server behind it.

installBackend() patches fetch before React mounts, and an Express-style router answers 66 routes under /api, /cms/api and /admin: the old services' paths, JSON shapes and status codes, plus a demo sign-in and a credits feed. The front end did not have to change how it talks to its backend. What a visitor changes is saved in their own browser, and every reply waits 60 to 180 ms so loading states stay honest. There is nothing to sleep, nothing to spin up and no secret to hold.

Live demo
02Key components
Product
01

A reader that stays out of the way

Long strip scrolls a chapter as one column and single page turns it a page at a time, with keyboard navigation and settings saved in the browser, so a reader sets the mode and image width once. The canvas drops from the site's near-black to pure black so the page is the only light, and the chrome stays thin: a top bar, and a pill with the reading timer and page count. Every chapter ends with its work's credit and licence.

Demo
02

One click to a reader with a history

Real sign-in is turned off, and the refusal points at the demo button. That button builds a demo reader, Mira Ellison, fresh every time, replaying 21 simulated days of reading session by session through the same stats and achievement engine the live routes use. Nothing is a hard-coded date, so the history moves with the calendar: she arrives with a 15-day streak, 29 chapters, 6 hours and 10 of the 20 achievements, and signing out and back in resets everything. The other readers, and every review, rating and view count, are simulated too, as the app's footer says.

System
03

The old API, answered in the page

Seven handler modules answer 39 routes under /api, 14 under /cms/api, 12 under the authenticated CMS proxy and 1 under /admin. Another user's id in the URL still gets 403, the ownership rule from the client build's security pass. The refusals the demo adds, like the login in this frame, are 403 with a code and never 401, because the front end signs out on any 401. An image tag never calls fetch, so every catalogue cover, page and avatar is a static file, and imported pages are Blob URLs from IndexedDB.

Tools
04

Zips read, never unpacked

The client build's batch importer kept its page, its endpoints and its chunk limits, up to 100,000 chunks of 50 MB, and now runs in the browser. It reads the zip's directory with zip.js through Blob.slice and inflates one page at a time, so a large file is never held in memory whole. A path that climbs out of the zip rejects the whole file, every page is checked by its magic bytes, and a Web Lock keeps one import running across every tab. Imported chapters appear in lists, search, the series page and the reader at once.

Content
05

Open comics, every one credited

11 series, 42 chapters and 343 pages: seven in the public domain, Hokusai Manga from the Met's CC0 photographs, and three under Creative Commons. A build script fetches each source once, then crops, scales down and re-encodes it to WebP with sharp, never enlarging, so each page stays its creator's own work. Every creator, licence and source is written into the catalogue with the pages and listed chapter by chapter on the credits page, and the reader closes every chapter on its work's credit and licence.

03Design philosophy
Near-black ground, a red accent, and the page as the only light.
Inkwell tokens · red on near-black☾ dark
Primary
Actions and the active state
--heroui-primary
#ef4444
Success
Saved or done
--heroui-success
#17c964
Classics
Runs to #a855f7: a classic or a colour work
--classics-from
#3b82f6
Surface
Cards and panels on the canvas
--heroui-content1
#1a1a1a
Background
The canvas under every page but the reader
--heroui-background
#0f0f0f
Principles
  • Text steps down in three greys.
  • Cover art leads, with its badges in the corner.
  • A genre chip keeps one tint on every card and page.
  • The reader keeps its chrome to a bar and a pill.
  • Each chapter closes on its work's credit and licence.
Aa InterINTER · UI
2 createdINTER · FIGURES
04How it was built

A client build first, then a rebuild that kept what was worth keeping: the front end, its API contract and its rules. The backend changed; the design did not.

May 2025 → Oct 2026
  1. May 2025

    The front end

    A React, Vite and Tailwind single-page app: the front end both builds still share.

  2. Jun 2025

    The chapter reader

    The chapter reader arrives: the screen every series page leads to.

  3. Aug 2025

    An Express API

    An Express API joins, wired into the remaining pages.

  4. Oct 2025

    Email verification

    Sign-up confirms the address with a code sent by email, through Resend.

  5. Feb 2026

    One gateway, one image

    A plain Node gateway on one port starts the API and the CMS as child processes and routes between them. The gateway, the API, the CMS and the built front end build into one Docker image, and Docker Compose adds MongoDB.

  6. Feb 2026

    Strict types, real chapters

    TypeScript strict mode, error boundaries and an auth context, then the reader serving real chapters from the CMS.

  7. Mar 2026

    The batch importer

    One zip of folders becomes series, volumes, chapters and pages: chunked uploads, a live log streamed as server-sent events, three CMS uploads at a time with retry, cancel, an import lock, and drafts until an admin publishes them.

  8. Mar 2026

    A security pass before handover

    Ownership checks on every user route, the CMS closed to public writes, tokens moved to an httpOnly cookie behind a proxy, DOMPurify on rich text, rate limits, single-use verification codes and a password reset flow.

  9. Oct 2026

    The backend moves into the page

    The same front end, rebranded as Inkwell, on an in-browser backend that answers its 66 routes, with a public domain and Creative Commons catalogue and a one-click demo account.

  10. Oct 2026

    The importer, in the browser

    zip.js, IndexedDB and a Web Lock in place of the upload server, behind the same endpoints and chunk limits.

  11. Oct 2026

    Tests, CI and a deploy

    410 unit tests, 22 end-to-end tests on desktop Chrome and a Pixel 7 profile, GitHub Actions on every push to main and every pull request, and Vercel serving main at inkwell.alexsmiths.com.

05Tech stack
Chosen twice: once so a client could run a catalogue, accounts and an admin from one Docker image and a database, and again so the same front end could run with no server at all.
Front end, both builds
React 18 + TypeScript 5.7UI · types

One app across both builds: the demo kept its layout, components and motion. TypeScript is strict in both, and in the demo it checks the app, the tests and the tool configs as three projects.

HeroUI 2.7 + Tailwind 3.4Components · tokens

Accessible components themed from one config, and that file is byte for byte the client build's, so the demo shows the product's real palette rather than a lookalike.

Vite 6 + Framer Motion 11Build · motion

Pages load as their own chunks. In the demo, the importer's code loads only when an import starts, and motion follows the system's reduced-motion setting.

Client build
Express 4 + Mongoose 8API · accounts

Auth, progress, history, bookmarks, notifications and the importer, on MongoDB. Every user-data route checks that the id in the URL is the signed-in user and answers 403 if not.

Strapi 5.34Catalogue CMS

An admin for series, chapters and pages, on SQLite by default and Postgres by configuration. Public create, update and delete are closed on all 11 content types.

Node gateway + DockerOne port, one image

A plain http-proxy gateway on port 3000 starts the API and the CMS as child processes and routes between them, so the app ships as one image, with MongoDB as a second container beside it.

The demo, in the browser
A fetch routerIn-page backend

installBackend() patches fetch before React mounts, because the first requests fire from mount effects. Each old route answers on its old path with its old shape, and state lives in localStorage under one versioned prefix.

zip.js 2.24 + IndexedDBBrowser importer

The zip is read through Blob.slice and inflated a page at a time, never unpacked. Pages are stored as Blobs, and a Web Lock allows one import across every tab.

sharp 0.35Catalogue build

Crops, scales down and re-encodes each page to WebP with enlargement turned off, so nothing is upscaled, recoloured or otherwise altered. A check validates the catalogue offline.

Quality and release
Vitest 4410 unit tests

343 in Node on the routes, the reading and achievement engines, streaks across midnight and daylight-saving changes, and the importer; 67 in jsdom on the reader, the account pages and sign-in.

Playwright22 end-to-end tests

On desktop Chrome and a Pixel 7 profile. Every test fails if a page requests another host or throws, and one runs a full import of the sample zip.

GitHub Actions + VercelCI · static hosting

The icon subset, types, lint, unit tests, build and e2e on every push to main and every pull request. Vercel serves the static build from main, with nothing to keep running.

06Gallery
17 frames · click any to view full
Live site
inkwell.alexsmiths.com · live demo, one-click demo account
0Servers or databases to run
343Comic pages, every one credited
410Unit tests, plus 22 end to end
Credits
Product and designAlexander Smith
EngineeringAlexander Smith
Catalogue and licensingAlexander Smith
StatusLive demo
Year2026
Deliverables
  • 01Client build: React SPA, Express API, Strapi CMS
  • 02One-image deploy behind a Node gateway, with MongoDB in Compose
  • 03Security pass before handover
  • 04In-browser backend answering 66 routes
  • 05Batch zip importer, on the server and in the browser
  • 06Live demo with an open-licence catalogue and credits
Tech stack
ReactTypeScriptViteHeroUITailwind CSSExpressMongoDBStrapiDockerVitestPlaywrightVercel
Comics and licences
Public domainLittle Nemo in Slumberland, Dream of the Rarebit Fiend and Little Sammy Sneeze by Winsor McCay; Krazy Kat by George Herriman; Buster Brown and The Yellow Kid by R. F. Outcault; Japanese Fairy Tale Series, published by Hasegawa Takejirō and illustrated by Kobayashi Eitaku and Suzuki Kason
CC0Hokusai Manga by Katsushika Hokusai, from The Met's Open Access photographs
CC BY 4.0Pepper&Carrot by David Revoy, and the demo avatars cut from its art; Pepper and Carrot MINI by Nicolas Artance
CC BY-SA 4.0Mini Fantasy Theater by David Revoy
Every source and licence
Creative Web Designer & Developer25.2048°N · 55.2708°E

AlexanderSmith

Locating the studio25.2048°N · 55.2708°E