Backend API
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. |
WEB_PASSWORD |
unset | Gates the browser UI. Unset = web routes not registered at all. |
LATEST_CHAPTER_POLL_ENABLED |
true |
Background latest-chapter poller on/off. |
LATEST_CHAPTER_POLL_COOLDOWN |
1h |
Minimum time between checks of the same series. |
LATEST_CHAPTER_POLL_INTERVAL |
10m |
Wake interval of the poller ticker. |
LATEST_CHAPTER_POLL_BATCH |
14 |
Series checked per wake. |
LATEST_CHAPTER_POLL_STAGGER |
20s |
Delay between fetches within a batch. |
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.
Web UI routes (only when WEB_PASSWORD set) are separate — see Web-UI.
Auth & CORS (security invariants)
/bookmarks*requiresAuthorization: Bearer <API_TOKEN>, checked with a constant-time compare;401otherwise.- CORS reflects
Originonly when it's inALLOWED_ORIGINS; allowsGET,PUT,DELETE,OPTIONSand headersAuthorization,Content-Type; answers preflightOPTIONSwith204. - CORS is the outermost middleware layer, so preflight short-circuits before
auth ever runs.
/healthzis public, everything under/bookmarksis not.
Sync model
Single-user, single bookmarks table, last-write-wins. Whichever device
PUTs last for a given key overrides the stored row.
The updated_at rule
List order is driven by updated_at, and it only advances on real reading
progress:
- Row is new, or
last_chapter_numchanged from what's stored
Otherwise the server keeps the existing updated_at — so toggling a
favourite or the poller discovering a new published chapter never reorders
the list. Because of this, PUT returns the row as the server actually
stored it, and every client must adopt that response rather than trust its
own request payload. Full rationale: plans/2026-07-25-bookmark-list-favorites-design.md §4.
Latest-chapter poller
A ticker goroutine in the same binary, independent of the userscript's own
maybeCaptureLatestOnSeriesPage/backgroundRefreshLatest logic — two
parallel clocks feeding the same latest_chapter field:
- Per-bookmark cooldown via the
latest_checked_atcolumn (Store.DueForLatestCheck'sWHEREclause). - A wake interval that picks a batch of due series each tick.
The row is stamped before the fetch, so a broken series waits out a full
cooldown instead of retrying every tick. Fetches use bogdanfinn/tls-client
with a Chrome TLS profile as defence in depth against fingerprint-based
blocking. Any failure logs and skips — this is an enhancement, not on the
request path, and never blocks bookmark sync. Design doc:
docs/superpowers/specs/2026-07-26-server-latest-chapter-polling-design.md.
Develop / test
cd backend
go test ./... # unit + handler tests
CGO_ENABLED=0 go build # static binary
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 -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