1
Backend API
Sulthan Zaki edited this page 2026-07-26 20:39:31 +07:00

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* requires Authorization: Bearer <API_TOKEN>, checked with a constant-time compare; 401 otherwise.
  • CORS reflects Origin only when it's in ALLOWED_ORIGINS; allows GET,PUT,DELETE,OPTIONS and headers Authorization,Content-Type; answers preflight OPTIONS with 204.
  • CORS is the outermost middleware layer, so preflight short-circuits before auth ever runs. /healthz is public, everything under /bookmarks is 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_num changed 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_at column (Store.DueForLatestCheck's WHERE clause).
  • 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