PRTOTYPE.COM · Production studioAudit · Build · Maintain
PRoTOTYPE.COM
Chapters case study
Case study · Desktop product

Point it at your folders. It keeps your place.

Chapters is a desktop audiobook player for Windows and Linux. It builds a library from the folders you choose, reads tags and covers in the background, and resumes each book where you stopped. Your library and listening stay on the PC, with no account and no cloud. PRTOTYPE.COM designed it and built it.

Type
Desktop app · Electron
Under it
Worker-thread scan · atomic store · signed licensing
Status
Release candidate · v0.5.1
1,131 unit tests in 114 files
Same bundle on Windows and Linux
30-day trial · no sign-up
The brief

A library on your own disk, and a player that copes.

Many people keep their audiobooks as plain files: ripped, DRM-free, or bought elsewhere. They need a player that finds the books, remembers the place in each one, and stays usable while it scans their folders.

Chapters keeps everything on the machine. Playback needs no internet and no account. Folders are the library, and the app builds the shelf from them.

That premise sets the engineering problem. The folders arrive in any shape, and nobody is there to fix them. A scan must not block the window, a crash must not lose a place in a book, and the licence check must fail safe when the network is bad. The Linux build must be the same code that passed the Windows checks.

The first commit was on 27 February 2026, and the repository now holds 274 commits. Version 0.5.1, dated 24 October 2026, changed no application code. It is a release candidate, built and tested locally and unpublished.

“No DRM, no accounts, no internet required.”The line in the app’s own About panel. Licence checks use the network; playback does not.
1,131
Unit tests · 114 files · all passed · run 13 Nov 2026
3,380
Identical application files in the Windows and Linux builds
17
Packaged acceptance checks on Windows · 14 on the Linux build
180 days
Offline grace between licence checks · 30-day trial
14k
Lines of application TypeScript · about 16k more of tests
The film

Watch it run.

Forty-five seconds of version 0.5.1 in use, with no sound. Press play, or pick a beat in the list to jump to it.

Demonstration data. The film uses the same data as the tour below: 13 public-domain books with covers drawn for this demonstration and invented narrator names, synthetic speech, and a seeded listening history. The pointer in the film is drawn by the recorder, because the automation does not move the real cursor. The screens are the shipped ones.

The product

A shelf built from folders, and a player behind it.

These are captures of version 0.5.1 in its default dark theme, with a throwaway profile and the licence server unreachable, so the app shows its local 30-day trial and a Pro trial badge. Click any plate to see it full size.

Every screen below shows demonstration data.

Player: one .m4b file, 27 chapters, the sixth now playing.
01 / Player

One file, 27 chapters.

The War of the Worlds is a single .m4b file. Chapters reads its 27 chapters from the file and lists them with their times. A pill above the list names the one now playing: Chapter 6 of 27, The Heat-Ray in the Chobham Road. The player and the chapter list are Free.

Chapter list read from the fileOne audio elementFree
Scan: every book is listed and playable before its tags and covers are read.
02 / Scan

A scan you can ignore.

The banner reads “Loading covers & details…” with a count: 87 files found, 13 added, details 0/13. All 13 books are already in the library and playable, each with a placeholder cover. Two worker threads read the tags and covers afterwards. Cancel keeps everything found so far.

Skeleton books first2 worker threadsFree
Bookmarks: four marks on one book, each with a timestamp and a note.
03 / Bookmarks

Mark the moment.

Four bookmarks on one book, at 11:43, 46:29, 1:05:05 and 2:01:41, each with a note. Add Bookmark takes the current position, and Export writes the list out. Bookmarks are Free. The separate notes and annotations panel is Pro.

Timestamps and notesExportBookmarks Free · Notes Pro
Audio: 1.25x with Voice EQ on, the waveform view and the sleep-timer menu.
04 / Audio

Listen at your pace.

The book plays at 1.25x with Voice EQ on. The square panel is the waveform view, which replaces the cover. The sleep-timer menu is open: End of chapter, or 5, 10, 15, 30, 45 or 60 minutes. “4h 8m remaining” and the sidebar durations are recalculated for the chosen speed. Voice EQ is a peaking filter at 2,500 Hz, +10 dB.

Speed 0.75 to 1.25x · FreeSpeed 0.5 to 3x · ProVoice EQ · Pro
The mini player: cover, 1.0x, EQ, elapsed and total time, a progress bar and transport buttons
Mini player (Compact Mode). Pro. Always on top is a separate Pro toggle.
Queue: Up Next holds three books that play one after another.
Also: History: recent track changes, each with a Restore button.
05 / Queue and history

What plays next, and what happened.

Up Next holds three books, and they play one after another when the current book finishes. Playback History, here on Dracula, lists recent track changes, each with a Restore button. Click one to return to that exact moment. Both panels are Pro.

Up Next queue · ProPlayback History · ProRestore the exact moment
Stats: layout of the activity view. The history behind it was seeded.
Also: Stats: the most-listened list and Badges (2 / 6).
06 / Stats

Time counted only while audio plays.

Stats & Achievements shows total listening time, books finished, a 14-day activity chart and streak cells. Scrolled down, it lists the most-listened books and the badges: 2 of 6 earned here. Time counts in real minutes, and seeks and speed changes add nothing to it. The history behind these two screens was seeded, so read them for layout only.

Monotonic wall clockPersisted outbox6 badges · Free
Settings: smart resume, silence skipping, version 0.5.1 and the 30-day trial. The purchase wording in the panel is not live: there is no public release.
Also: Settings: Diagnostics, and Backup & restore without the licence.
07 / Settings and licence

A trial that survives a data wipe.

Settings shows Version 0.5.1 and a Pro panel reading “Free trial · all Pro features for 30 more days”, after which Chapters drops to the free tier of 15 new books a year. On an installed copy the licence server freezes the first-launch date for a salted hash of the machine, so deleting local data does not restart the trial; this capture had the server unreachable, so the badge comes from the local rule. Smart resume (a 10-second rewind after an hour away) and silence skipping are Pro, and backup never carries the licence or folder permissions.

30-day trialSmart resume · ProBackup without the licence
Under the surface

What a listener never sees.

Six decisions in the code, each shown with the constants that set it. They keep the window responsive, the library intact and the licence safe when the network fails.

A scan that never freezes the window

Tag parsing runs in worker threads, never on Electron’s main thread. Two workers take one task each from a queue capped at 64. A worker that times out, errors or exits is terminated and replaced, and the next scan creates fresh ones.

2 workers · queue 64 · 60,000 ms timeout

Grouping by evidence

Numbered file names such as “01 Intro” are only a candidate until the tags agree. A collection folder alone does not make several M4B files one book. A manual correction must be a complete partition of the existing files, so the window cannot add or drop a track.

Confirmed against album tags · manual group title max 300 characters

A store that cannot half-write

Library state is one JSON file. Every save is chained on one promise queue, written to a unique temp file, flushed, then renamed over the real file. An earlier audit marked concurrent saves as unsafe; the queue is the fix. Quit waits for the queue to drain.

Temp file · flush · rename retries up to 5 times

Covers as files, behind a locked scheme

Covers used to sit as base64 inside the JSON store. They are now image files served through safe-cover://, and the id must be 16 to 64 hex characters. Audio uses a matching safe-audio:// scheme with range requests, and it refuses paths outside the folders the user approved.

safe-cover:// · safe-audio:// · 413 over 20 MB

Licensing that fails safe offline

Every licence reply is Ed25519-signed and verified, and only a signed refusal ends a licence. A captive-portal page degrades to offline grace and never clears it. The stored record is sealed to the machine, so a copied database.json opens nowhere else. The clock guard deters casual date changes and claims no more.

30-day trial · 3-day re-check · 180-day grace · 48 h clock tolerance

Fuses and an allow-list

Six Electron fuses are set at build, including no run-as-Node, no --inspect and load-only-from-the-asar. An earlier build packaged the whole repository, a keys file among it, into a 149 MB installer. The builder now takes an explicit list, and a check rejects any other path in the archive.

6 fuses · asar allow-list · sandbox on
Under the hood

From a folder of files to a resumed chapter.

Seven stages, from the folder picker to the saved position. The scan job behind stages 02 to 04 can be cancelled at any point, and it keeps what it has found.

01
Pick a folder
Native picker · approved roots
The user chooses folders through the operating system’s own dialog, and only those folders may ever be read. A file or a window cannot grant itself access.Only approved folders are read
02
Find the audio
Directory walk · 9 extensions
The scan walks up to 32 levels deep and keeps .mp3, .m4a, .m4b, .m4p, .aac, .wma, .ogg, .wav and .flac. A banner reads “Finding audio files…” and shows a count.
03
Books appear first
Skeleton entries · batches of 25
Files are grouped into books and added as playable entries named after their folder, so the library fills before any tag is read. Unchanged folders are skipped by comparing track paths, sizes and modified times.
04
Read the tags off-thread
Worker pool · 2 workers · 60 s timeout
Two worker threads read tags, covers and durations, one task each, with a queue capped at 64. A stuck or corrupt file times out, its worker is replaced, and the scan carries on.
05
Decide what is one book
Identity check · manual correction
Numbered file names are only a candidate until the album tags agree, so unrelated files are not merged. Saved progress follows the right track if a book is split or merged by hand.
06
Store and serve
Atomic JSON write · safe-cover://
Results are saved in batches through one write queue: temp file, flush, rename. Covers become image files served over a scheme that can reach nothing else on disk.
07
Play and remember
One audio element · save every 5 s
A single audio element plays through safe-audio:// with range requests, so a large M4B starts at once. Position is saved every 5 seconds. On return, smart resume (Pro) rewinds 10 seconds after an hour away.
electron/lib/store.ts, lines 453 to 483 (one logging call removed)
    private save(): Promise<void> {
        // Snapshot is taken inside run() at write time (last-serialise-wins is
        // the correct semantics here), and every write is chained so that two
        // concurrent saves cannot race on a shared temp file. The second arg to
        // .then keeps the chain alive even after a prior write rejects.
        const run = () => this.persistSnapshot(this.data);
        this.writeQueue = this.writeQueue.then(run, run);
        return this.writeQueue;
    }

    private async persistSnapshot(snapshot: DbSchema): Promise<void> {
        const tmp = `${this.filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
        try {
            const content = JSON.stringify(snapshot, null, 2);
            const fh = await fs.open(tmp, 'w');
            try {
                await fh.writeFile(content, 'utf-8');
                await fh.sync();
            } finally { await fh.close(); }
            await this.renameWithRetry(tmp, this.filePath);
        } catch (err) {
            await fs.unlink(tmp).catch(() => {});
            // (logging removed)
            throw err;
        }
    }

    /** Await all writes currently queued. Safe to call from before-quit. */
    flushWrites(): Promise<void> {
        return this.writeQueue;
    }
A failed write does not stop the queue
The second argument to .then runs the next write even after a rejection. A failed write deletes its temp file and rethrows, and the later saves still run.
Windows can refuse a rename
Antivirus or an indexer can hold a file for a moment. The rename retries up to 5 times, waiting 15 ms times the attempt number, and quit awaits flushWrites() first.
A stuck parser cannot stall the scan
A tag worker that times out after 60 seconds, errors or exits is terminated and replaced. The task fails with timeout or worker-failed, and the scan carries on.
The release log

Four releases, with the open items listed.

Entries from the changelog, shortened, with dates as printed. None of these builds has been published. The gaps are the open items in the project’s own notes.

0.3.1
23 Jun 2026

A security pass, on the record

A pre-flight audit recorded 4 critical, 19 high, 43 medium and 36 low findings. The release actioned all the critical, high and medium ones. The counts are the changelog’s own.

Known gaps, from the project notes
The 36 low findings are recorded; the changelog does not say they were fixed.
0.4.0
18 Jul 2026

Audio goes through a gate

Audio moved behind a safe-audio:// protocol that serves only approved folders, replacing direct file access. The content security policy changed to match.

0.5.0
23 Oct 2026

Linux, from one build

AppImage and deb are built alongside the Windows installer from the same tested bundle, and the two are compared byte for byte. The release also fixed a Linux fingerprint bug that gave every Linux machine the same empty hash, so none could start a trial.

Known gaps, from the project notes
Windows-only checks stay Windows-only.
Final validation and candidate acceptance were still pending for the 0.4.4 candidate work.
0.5.1
24 Oct 2026

The Linux check grows from 2 to 14

No application code changed. The Linux release check now runs 14 of the 17 packaged checks, up from 2, and Linux receipts are written apart from the Windows ones. The build script also stops before packaging if WSL has no ffmpeg.

Known gaps, from the project notes
The installers are not code-signed. Windows would show an Unknown publisher warning.
The Linux build was checked under WSLg. No real Linux desktop has run it.
Backup-recovery, shell-acceptance and onboarding-acceptance run on Windows only, as does the minimise and close-to-tray block of playback-reliability.
The release workflow has never run, and the update feed is empty.
No acceptance yet on physical audio, media keys, USB reconnect or a screen reader.
store.ts still mixes migrations with the write queue.
The stack

One bundle, two systems.

One Vite build feeds both packages. The Linux packaging runs inside WSL on the same tested files, then a script compares the two archives.

Electron 42
Shell
The main process owns files, licence and scanning. The window gets a narrow, validated bridge.
React 18 + TypeScript · Vite · Tailwind
Interface and build
About 14,000 lines of application TypeScript in strict mode, and tsc --noEmit passes.
music-metadata (patched)
Tags and chapters
Runs only inside worker threads. A patch restores MP4 chapter parsing, applied by a script that verifies file hashes.
HTML5 Audio + Web Audio
Playback
One audio element. An analyser drives the waveform and silence detection, and a peaking filter provides Voice EQ.
JSON store, atomic write queue
Persistence
Temp file, flush, rename, fault-tested. No database engine to corrupt.
Self-hosted licence server
Licensing
Ed25519-signed replies and 2 seats per key. The same server backs Productive Voice.
electron-builder + electron-updater
Release
NSIS wizard installer of about 106 MB, plus AppImage and deb. The Linux build is checked under WSLg. The update feed is wired to a releases page that is empty today.
Vitest 4 + Playwright 1.58
Tests
1,131 unit tests in 114 files, 14 Playwright spec files and 27 verification scripts.
Why it holds up

Checks that were run.

1,131 tests
The unit suite: 1,131 tests in 114 files, all passed, run 13 November 2026.
Strict types
tsc --noEmit exits 0 with strict mode on. Lint reports 0 errors and 190 warnings.
17 + 14 checks
17 packaged checks run against the built Windows installer and 14 against the Linux build. One script compares the two archives and finds 3,380 identical application files.
An allow-list
app.asar holds dist/, dist-electron/ and production modules only. A pattern check rejects any other path in the archive.
Release candidate · v0.5.1

Built and tested, not yet released.

Version 0.5.1 has no public release and no price. The installers are not code-signed, so Windows would show an Unknown publisher warning. The Linux build has been checked under WSLg only.

Chapters
Case study · Desktop audiobook player · Designed and built by PRTOTYPE.COM · Written November 2026

Like what you see

Need a desktop app with its checks attached?

Chapters went from a first commit on 27 February 2026 to a Windows and Linux release candidate built from one tested bundle: 1,131 unit tests in 114 files, 17 packaged acceptance checks and a byte-for-byte comparison of the two builds. If you have a desktop app that needs a scan engine, a safe store or a release pipeline, let's talk it through.

Fixed prices · Set against a written spec
Book a Production Consultation→