Adds favorite, latest_chapter and latest_chapter_num to the bookmark record, with an idempotent ALTER TABLE migration so the already-deployed database picks them up. updated_at now moves only when a bookmark is new or last_chapter_num changes. Clients order their list by updated_at, so favoriting a series or recording a newly published chapter must not disturb that order. Upsert consequently returns the row as stored and the handler echoes that rather than the request payload, since the candidate timestamp it sends is often discarded. Scanning also tolerates NULL in the optional columns, which a database created before this code can legitimately contain. 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; server sets updated_at. |
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.
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).
- Bookmarks made on Asura appear when the panel is opened on Demonic, and vice versa — the backend is the shared store.
Offline / backend down: changes are cached in localStorage and retried on the
next successful load (last-write-wins).
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; asuracomic.net 301s here) |
/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:
- 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.