Adds a browser-accessible bookmark list on a new subdomain, served by the existing Go binary via go:embed'd templates and htmx. Cookie sessions (HMAC-keyed off API_TOKEN, 60-day) gate /ui/*; the bearer-authenticated /bookmarks* API and the userscript are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Manga Bookmark
Track manga read-progress on asurascans.com (a.k.a. asuracomic.net) and demonicscans.org from a phone (Bromite / mobile Chromium), synced to a self-hosted Go backend so bookmarks unify across both sites and all devices.
Two parts:
backend/— tiny Go (net/http+ pure-Go SQLite) sync service. 4 routes, static binary, distroless container.userscript/manga-bookmark.user.js— single Bromite-compatible userscript (noGM_*APIs) that injects an on-page bookmark UI and syncs viafetch().
Bromite userscript (isolated world, Shadow DOM UI, localStorage cache)
-- fetch() HTTPS --> reverse proxy (TLS + CORS) --> Go net/http --> SQLite (volume)
1. Backend
Config (env)
| Var | Default | Notes |
|---|---|---|
API_TOKEN |
(required) | Bearer token shared with the userscript. |
ALLOWED_ORIGINS |
Asura + Demonic origins | Comma-separated CORS allowlist. |
DB_PATH |
/data/bookmarks.db |
SQLite file location. |
PORT |
8080 |
Plain HTTP; TLS terminated by the proxy. |
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/bookmarks |
Bearer | All bookmarks (single-user). |
PUT |
/bookmarks/{key} |
Bearer | Upsert one series; returns the row as stored. |
DELETE |
/bookmarks/{key} |
Bearer | Remove one. |
GET |
/healthz |
none | 200 ok. |
key is <site>:<series_id> — e.g. asura:trash-of-the-counts-family-f886a8af
or demonic:Infinite-Level-Up-in-Murim. Sync is last-write-wins.
updated_at orders the bookmark list, so it moves only on real reading
progress: the server applies its timestamp when the row is new or
last_chapter_num changes, and otherwise keeps the stored one. Favouriting a
series or recording a newly published chapter therefore leaves the order alone.
Because the timestamp a client sends is only a candidate, PUT echoes the row
as stored and clients adopt that rather than their own payload.
Develop / test
cd backend
go test ./... # unit + handler tests
CGO_ENABLED=0 go build # static binary
Run the stack
cp .env.example .env
# edit .env: set API_TOKEN (openssl rand -hex 32)
docker compose up -d --build # binds 127.0.0.1:8080
Smoke test:
TOKEN=$(grep '^API_TOKEN=' .env | cut -d= -f2)
curl -s localhost:8080/healthz # ok
curl -s localhost:8080/bookmarks # 401
curl -s -H "Authorization: Bearer $TOKEN" localhost:8080/bookmarks # []
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Test","last_chapter":"Chapter 1","last_chapter_num":1}' \
localhost:8080/bookmarks/asura:test-1
curl -s -i -X OPTIONS -H 'Origin: https://asurascans.com' \
-H 'Access-Control-Request-Method: PUT' \
localhost:8080/bookmarks/asura:test-1 | grep -i access-control # 204 + CORS headers
Deploy behind your reverse proxy
Route https://manga-api.<domain> → the service on :8080 (TLS at the proxy).
-
Host proxy (nginx/Caddy on the host): the base compose already binds
127.0.0.1:8080; point the proxyproxy_pass http://127.0.0.1:8080;. -
Docker proxy (Traefik/nginx in a container on its own network): use the override, which drops the published port and joins the shared network:
docker network create proxy # once, if it doesn't exist docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --buildSet
PROXY_NETWORKin.envif your network isn't namedproxy.
Verify: https://manga-api.<domain>/healthz returns ok over valid TLS (no
mixed-content), and an OPTIONS preflight from a real site origin returns the
CORS headers.
2. Userscript
Configure
Edit the config block at the top of userscript/manga-bookmark.user.js:
const API_BASE = "https://manga-api.<domain>"; // no trailing slash
const API_TOKEN = "<same token as backend>";
The token lives in the userscript's isolated world — the manga sites' own JS cannot read it.
Install on Bromite (mobile)
Bromite runs Chromium's native userscript engine (no Tampermonkey needed):
- Bromite → Settings → User scripts → enable user scripts (allow the permission prompt).
- Save the configured
manga-bookmark.user.jsto the device (or open its raw URL). Bromite detects the.user.jsand offers to install it. - Confirm the install; the
@matchlist covers both sites. - Open a series on either site — a 📑 button appears bottom-right.
Exact menu wording varies by Bromite build; if "User scripts" is absent, update Bromite or use a build with userscript support.
Desktop iteration (optional)
The script is GM_*-free, so it also runs in Tampermonkey/Violentmonkey on
desktop for faster testing — install the same file unchanged.
Use
- Bookmark: on a series or chapter page, open the panel → + Bookmark this.
- Auto-progress: opening a chapter of a bookmarked series records it when the chapter number ≥ the stored one (re-reading older chapters never regresses progress; unparseable numbers set the current chapter).
- Manual override: panel → Edit on any row forces a specific chapter.
- Continue: jumps to the last-read chapter (or the series page).
- Latest chapter: rows read
Read: … · Latest: …once the newest published chapter is known and it is ahead of your progress. See below for how that is found. - Favourites: the ☆ on any row toggles it; the ★ Favourites tab narrows the list. Favourited series still appear under All. The flag syncs, so it follows you across devices; the chosen tab does not persist.
- Bookmarks made on Asura appear when the panel is opened on Demonic, and vice versa — the backend is the shared store.
Neither favouriting nor learning a new chapter reorders the list — only reading progress does.
Offline / backend down: changes are cached in localStorage and retried on the
next successful load (last-write-wins).
How "latest chapter" is found
Only a series page lists every chapter (a reader page links just its neighbours), and the backend cannot fetch either site — Cloudflare blocks server-side requests, and neither site offers an API or feed to poll. So the userscript does the looking, from your own browser session:
- Opening a bookmarked series page records its newest chapter directly.
- Otherwise it fetches series pages in the background — same-origin only, so
browsing Asura refreshes Asura bookmarks and Demonic refreshes Demonic. One
series per navigation, and at most one check per series every 4 hours
(
LATEST_CHECK_BATCH/LATEST_CHECK_THROTTLE_MS). Failures are silent and simply retried after the window.
Freshness is tracked per device in localStorage under mangabm:lastchecked
and is deliberately not synced, since each device checks on its own.
This means a bookmark is as current as its last check — not the moment a chapter drops. Nothing can be instant here: neither site offers push, feeds, or an API.
Adapter reference (verified live 2026-07-24)
The site adapters key everything off URL regex, with title/cover from
og:title / og:image. Confirmed against live pages via Playwright:
| Site | Series URL | Chapter URL | series_id |
|---|---|---|---|
Asura (asurascans.com) |
/comics/<slug-hash> |
/comics/<slug-hash>/chapter/<n> |
<slug-hash> |
Demonic (demonicscans.org) |
/manga/<slug> |
/title/<slug>/chapter/<n>/<page> (chaptered.php?manga=<id>&chapter=<n> 301s here) |
<slug> |
Notes:
asuracomic.netdeep links are dead (re-checked 2026-07-25). They 301 to theasurascans.comroot, discarding the path, at the edge — before the userscript gets a document — so nothing client-side can rescue them. Reach series throughasurascans.com. The host stays matched in case the redirect starts preserving paths again.- Asura
og:titlecarries aChapter N - Read Online \| Asura Scanssuffix that the adapter strips; Demonic chapterog:titleis<Title> Chapter N. - Demonic's
<slug>is identical on/manga/…and the canonical/title/…reader, so a bookmark set from the series page and the auto-update from the reader resolve to the same key. - Asura showed no Next.js markers on the live site, so navigation uses a framework-agnostic watcher (history patch + polling) rather than a Next-only hook — works for client-routed and full-reload sites alike.
If either site changes its URL shape, update the regex in the matching adapter
in userscript/manga-bookmark.user.js and re-verify.