Context
I'm building a browser-based reader (React + Vite, runs in a WebView on iOS/Even Realities G2) that talks to KOSync-compatible sync servers. The KOSync API surface at /users/create, /syncs/progress, etc. is exactly what I need — but today the hosted sync.crosspointreader.com doesn't send CORS response headers, which means browsers (including iOS WKWebView) block JavaScript from reading any response.
What I observe
$ curl -si -X POST https://sync.crosspointreader.com/users/create \
-H 'Content-Type: application/json' \
-H 'Origin: http://localhost:5173'
-d '{"username":"corstest","password":"test"}'
HTTP/2 201
content-type: application/json
← no Access-Control-Allow-Origin, no Access-Control-Allow-Headers, no Vary: Origin
$ curl -si -X OPTIONS https://sync.crosspointreader.com/users/create
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,x-auth-user,x-auth-key' \
-H 'Origin: http://localhost:5173'
HTTP/2 404 ← no CORS preflight handler
The server accepts the requests fine (great!), but the browser refuses to expose the response body to JS because the preflight fails and the actual response lacks Access-Control-Allow-Origin. Web-based clients today therefore need a Cloudflare Worker or similar CORS proxy — same as we've been doing for sync.koreader.rocks historically.
Suggested fix
The server uses Hono, which ships with a cors() middleware. It's a ~5-line change in src/app.ts:
import { cors } from 'hono/cors';
export function createApp(db: DB, config: Config, opts: AppOptions = {}): Hono {
const app = new Hono();
app.use('*', cors({
origin: '*', // or a configurable allowlist via env
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Accept', 'x-auth-user', 'x-auth-key'],
exposeHeaders: [],
maxAge: 86400,
}));
// …existing routes…
}
Auth is header-based (x-auth-user / x-auth-key) so credentials: 'include' isn't required — the safest default is origin: '*' which avoids the whole cookie/credentials attack surface. If a stricter allowlist is preferred, exposing it via a CORS_ORIGINS env variable (comma-separated) works well.
Why it's worth it
- Removes the CF-Worker-proxy requirement for any web-based KOSync client (PWAs, browser extensions, WebView-based reader companion apps like the Even Realities G2 companion I'm building).
- Makes the "just change the sync-server URL" story that the README already advertises true for browsers too.
- Zero impact on existing device firmware clients (KOReader, CrossPoint) — they don't do preflight.
- Makes it possible for browser-based clients to use the extended /api/v1 API (bookmarks / clippings / stats) without proxying every endpoint.
Context
I'm building a browser-based reader (React + Vite, runs in a WebView on iOS/Even Realities G2) that talks to KOSync-compatible sync servers. The KOSync API surface at /users/create, /syncs/progress, etc. is exactly what I need — but today the hosted sync.crosspointreader.com doesn't send CORS response headers, which means browsers (including iOS WKWebView) block JavaScript from reading any response.
What I observe
$ curl -si -X POST https://sync.crosspointreader.com/users/create \
-H 'Content-Type: application/json' \
-H 'Origin: http://localhost:5173'
-d '{"username":"corstest","password":"test"}'
HTTP/2 201
content-type: application/json
← no Access-Control-Allow-Origin, no Access-Control-Allow-Headers, no Vary: Origin
$ curl -si -X OPTIONS https://sync.crosspointreader.com/users/create
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,x-auth-user,x-auth-key' \
-H 'Origin: http://localhost:5173'
HTTP/2 404 ← no CORS preflight handler
The server accepts the requests fine (great!), but the browser refuses to expose the response body to JS because the preflight fails and the actual response lacks Access-Control-Allow-Origin. Web-based clients today therefore need a Cloudflare Worker or similar CORS proxy — same as we've been doing for sync.koreader.rocks historically.
Suggested fix
The server uses Hono, which ships with a cors() middleware. It's a ~5-line change in src/app.ts:
import { cors } from 'hono/cors';
export function createApp(db: DB, config: Config, opts: AppOptions = {}): Hono {
const app = new Hono();
}
Auth is header-based (x-auth-user / x-auth-key) so credentials: 'include' isn't required — the safest default is origin: '*' which avoids the whole cookie/credentials attack surface. If a stricter allowlist is preferred, exposing it via a CORS_ORIGINS env variable (comma-separated) works well.
Why it's worth it