Web + Android · 2025

BookishamPrivate Reading Room

  • PWA + Kotlin app
  • Books never downloaded
  • One device at a time
  • AES-256-GCM at rest
Bocchi (Bocchi the Rock!)

From the jacket flap

Your books, wherever you are.

Bookisham is a members' library that never hands over a file. Readers sign in on any device and read page by page from a canvas; an admin runs the shelf, the readers and the purchases. One Next.js server, two front doors: an installable web app and a native Android app.

Frontispiece

Frontis. Bookisham landing page: a bookshelf photograph with the sign-in and create-account buttons

Particulars of this edition

Role
Design, full-stack, Android
Server
Next.js 15 · Postgres on Neon · S3
App
Kotlin · Jetpack Compose · Material 3
Status
Live, v1.0.5 on Android
Fig. a A reader's library with cover art and reading progress
Fig. b The night-mode reader with a page scrubber

The book in figures

2
Apps, one server
3
Formats: PDF, EPUB, Word
48 h
Session, one device
0
Files ever downloaded

Chapter I · Overview

A library that lends, never gives.

Most reading apps hand the reader a file and hope for the best. Bookisham keeps the file: every book is paginated on upload, stored encrypted, and served one rendered page at a time through a session that belongs to exactly one device.

Readers create their own account, browse the shelf with prices and discounts up front, pay by UPI and attach the screenshot; an admin approves the request by eye and the book appears on the reader's shelf with a notification. The page they stop on is saved as they go, so a reload, or another device, lands on the same line.

The web app installs as a PWA and the Android app is native Kotlin with Jetpack Compose. Both speak to the same JSON routes, share the same one-device rule, and paint pages the same way: unwrapped in memory, drawn, and dropped.

Bocchi (Bocchi the Rock!)

The house rules

  1. § 1 Read, never download

    Pages are rendered on the server and painted onto a canvas. There is no file, no image URL and no text in the DOM.

  2. § 2 One device at a time

    A login ends every other session for that account. Sessions last 48 hours, whether or not the reader is active.

  3. § 3 Text off the clipboard

    Selection, copy, print and dev-tools shortcuts are swallowed; the page is covered when the window is not in front.

  4. § 4 UPI purchases

    Prices and discounts per book, a QR to pay, a screenshot as proof, and an admin decision that notifies the reader.

  5. § 5 Encrypted shelf

    Books, covers and rendered pages are AES-256-GCM encrypted in the app before they reach S3. A public bucket would leak ciphertext.

  6. § 6 Updates from GitHub Releases

    A tagged push builds and signs the APK; the app checks the release feed, downloads inside the app and hands off to the installer.

Bocchi (Bocchi the Rock!)

Chapter II · Two front doors

Same shelf, every device.

In the browser · Next.js 15

An installable web app

Server-rendered pages on warm paper, with a service worker that keeps the shell offline-ready and stays away from anything that carries a page of a book.

Plate II.1 Browsing every book in the library, with prices and locked covers
  • App Router, React 18, Tailwind
  • Fraunces for display, Inter for body
  • Install button and manifest for the home screen
  • Cache-Control: no-store on every page of a book

The server renders its pages for the browser and exposes the same shapes as JSON for the phone. Whichever door a reader walks through, the shelf, the progress and the rules are identical.

On the phone · Kotlin

Native on Android

Shown here: the web app installed on a phone. The Android app mirrors every screen in Jetpack Compose with no third-party UI library, and adds what only a native app can.

Plate II.2 The library on a phone
  • FLAG_SECURE: the OS refuses screenshots and recording
  • Pinch and double-tap zoom about the fingers
  • Pages held in an LruCache bounded to a third of the heap
  • Notifications raised in the tray by a background worker

* Shared by both: POST /api/auth/login · GET /api/me · GET /api/library · GET /api/browse · GET /api/books/:id · GET /api/read/:book/:page · POST /api/progress · POST /api/purchase-requests · GET /api/notifications · /api/admin/* · bk_session cookie · 48 h · one device per account.

Chapter III · Under the hood

Three pipelines, one promise.

Upload, read, protect. Each is a small chain of steps, and each ends with the same outcome: the reader sees the page and nobody holds the file.

Bocchi (Bocchi the Rock!)

In 3 parts

  1. I.Upload — From a file to numbered pages
  2. II.Reader — From a request to a painted page
  3. III.Shield — Keeping the text off the clipboard

Pull a ribbon on the facing page to open its part.

Part the First

UploadFrom a file to numbered pages

  1. Accept PDF, EPUB or Word. The admin drops a file with an optional cover, title, author, price and discount.
  2. Lay out reflowable formats. Word goes through mammoth to HTML; EPUB and HTML are laid out at A4 so every book has fixed, numbered pages.
  3. Paginate with MuPDF. The page count is stored with the book; it is what progress and the scrubber count against.
  4. Encrypt, then upload. Each object is sealed with AES-256-GCM using a key only the server holds, then put under one prefix in the bucket.
  5. Render covers and pages on demand. The first request for a page renders it at the asked scale and caches the result, encrypted, next to the book.

* Pieces: mupdf · mammoth · sharp · AES-256-GCM · @aws-sdk/client-s3 · Neon Postgres · schema applied on first boot · stateless with S3.

Bocchi (Bocchi the Rock!)

Chapter IV · Key screens

From the front door to the last page.

§ 1. The front door

Plate IV.1 The full landing page

One photograph, edge to edge, and the two things a visitor came for: how to reach us and where to sign in. The shelf is listed for everyone, with prices, so a visitor knows what is inside before creating an account.

  • WhatsApp or email contact menu
  • Install prompt for the PWA
  • Three steps, terms and privacy

§ 2. The shelf

Plate IV.2 The library page

A reader sees only the books they have been given, each with an ember progress bar and the page they are on. An admin sees every book, which is how they check what a reader will see.

  • Progress saved per reader per book
  • Opens on the page you left
  • Session expiry shown plainly

§ 3. The reader, at night

Plate IV.3 The reader

A near-black surface so a white page does not glare in a dark room, one floating bar with zoom and a page scrubber, and a column of pages painted from bitmaps that never touch the disk.

  • Canvas pages, no image URLs
  • Scrubber, page jump, keyboard paging
  • Email watermark tiled across every page

§ 4. When you look away

Plate IV.4 The reader with its pages covered

The moment the window loses focus, the tab is hidden, the developer tools open or PrintScreen is pressed, the pages disappear behind a cover. Come back and they return where they were.

  • Blur, visibility and size polling
  • Clipboard overwritten on PrintScreen
  • Same rule as FLAG_SECURE on Android

§ 5. The admin's desk

Plate IV.5 The admin readers table

Everyone with an account, when they were last seen and how many books they hold. A new reader's password is shown exactly once; from a reader's page the admin ticks books, resets the password, signs a device out or disables the account.

  • Password shown once, sent on by hand
  • Sign out device ends the session at once
  • Per-reader book access

§ 6. Books and purchases

Plate IV.6 The admin books page

Upload a PDF, EPUB or Word file with a cover, a price and a discount; edit or replace it later. Purchase requests arrive with the reader's UPI screenshot and are approved or rejected by eye, which grants the book and notifies the reader.

  • Prices and discounts per book
  • Manual UPI review, no gateway
  • Release notes broadcast to every reader

Chapter V · In the pocket

Installed, not bookmarked.

The web app on a phone: the same paper, the same shelf, the same reader. The native Android app follows these screens one for one.

Fig. 1 Landing on a phone
Fig. 2 Browse on a phone
Fig. 3 The reader on a phone
Fig. 4 The library on a phone
Fig. 5 Account on a phone
Bocchi (Bocchi the Rock!)

Chapter VI · Threat model and stack

What it stops, and what it can't.

It stops

  • Downloading or saving the file
  • Selecting, copying or printing text
  • Image URLs that can be shared
  • Reading on two devices at once
  • Reading past a disabled account or reset password
  • Reading a bucket object without the server's key

It cannot stop, so the watermark exists

  • A camera pointed at the screen
  • A hardware screenshot on the web (Android refuses via FLAG_SECURE)
  • A screen recorder outside the browser
  • A rooted phone reading its own memory
Bocchi (Bocchi the Rock!)

Stack

Next.js 15React 18TypeScriptTailwind CSSPostgres on NeonpgS3 · @aws-sdkMuPDFmammothsharpWeb CryptoService workerKotlinJetpack ComposeMaterial 3OkHttpkotlinx.serializationNavigation ComposeWorkManagerGitHub Actions

Operations

  • Schema applied on first start; first admin created from env
  • Stateless server once S3 is configured
  • Tagged push builds, signs and publishes the APK with SHA256SUMS
  • App checks GitHub Releases and installs in place
  • New release broadcast to every reader once

Chapter VII · More screens

The rest of the room.

Plate VII.1 Sign up with terms agreement
Plate VII.2 Sign in
Plate VII.3 Browse every book
Plate VII.4 Account, with copyable IDs
Plate VII.5 Notifications
Plate VII.6 A reader's books and actions
Plate VII.7 Purchase requests
Plate VII.8 Terms and conditions

Colophon

Bookisham: Private Reading Room, a private reading room, on the web and on Android.

Designed and built by one hand: Design · Full-stack · Android.

Shelved under Full-stack, September 2025. Status: Live.

Set in Next.js 15, Kotlin · Compose, Postgres, S3 · AES-GCM and PWA.

This page is typeset in Cormorant Garamond and EB Garamond, on paper the colour of an old library card.

Your guide through these pages: Bocchi, of Bocchi the Rock!.