A self-hostable sync server for KOReader. Keeps reading progress in sync across all your devices.
- Less than 1,000 lines of TypeScript in a single file (
src/index.tsx) - SQLite database — no external services needed
- Runs on Docker — nothing else to install
Requirements: Docker. That's it.
docker run -d -p 3000:3000 -v koreader-data:/app/data ghcr.io/nperez0111/koreader-sync:latestThe server is now running at http://localhost:3000. The SQLite database is persisted in the koreader-data volume.
For a more permanent setup, use Docker Compose:
# docker-compose.yml
services:
kosync:
image: ghcr.io/nperez0111/koreader-sync:latest
container_name: kosync
ports:
- 3000:3000
restart: unless-stopped
volumes:
- data:/app/data
volumes:
data:docker compose up -d- Open a document on your KOReader device
- Go to Settings > Progress Sync > Custom sync server
- Enter your server's URL (e.g.,
http://your-server:3000) - Select "Register / Login" to create an account
- Test with "Push progress from this device now"
- Enable automatic progress syncing if desired
All configuration is through environment variables. None are required — defaults work out of the box.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
HOST |
0.0.0.0 |
Bind address |
PASSWORD_SALT |
"default_salt_change_in_production" |
Salt for bcrypt password hashing. Change this in production. |
DISABLE_USER_REGISTRATION |
false |
Set to true to block new user registration |
LOG_LEVEL |
info |
Minimum log level: debug, info, warn, error |
NODE_ENV |
— | Set to development for pretty-printed logs, otherwise JSON |
Pass them to Docker with -e flags or in your docker-compose.yml:
services:
kosync:
image: ghcr.io/nperez0111/koreader-sync:latest
environment:
- PASSWORD_SALT=your_secure_random_string
- DISABLE_USER_REGISTRATION=true
# ...Built in:
- Passwords are hashed with bcrypt (with configurable salt)
- Rate limiting on auth endpoints (10 requests/min per IP)
- Input validation on all endpoints (required fields, length limits)
- Parameterized SQL queries (no injection risk)
For production, you should also:
- Put the server behind a reverse proxy with HTTPS (e.g., Caddy, Traefik, nginx)
- Use a strong, random
PASSWORD_SALT - Back up the SQLite database regularly (located at
/app/data/koreader-sync.db)
- POST
/users/create - Body:
{ "username": "string", "password": "string" } - Response:
201Created,409Username exists,403Registration disabled
- GET
/users/auth - Headers:
x-auth-user,x-auth-key - Response:
200OK,401Unauthorized
- PUT
/syncs/progress - Headers:
x-auth-user,x-auth-key - Body:
{
"document": "8b03a82761fae0ee6cd5a23700361e74",
"progress": "/body/DocFragment[15]/body/div[65]/text()[1].41",
"percentage": 0.2082,
"device": "boox",
"device_id": "197E7C6B3FD54A749C87DE9C1B05A3CE",
"metadata": {
"filename": "the_great_gatsby.epub",
"title": "The Great Gatsby",
"authors": "F. Scott Fitzgerald"
}
}The metadata field is optional. KOReader sends it when "Send document metadata" is enabled in KOSync settings (see koreader/koreader#15306). Previously stored metadata is preserved when omitted.
- Response:
200OK,401Unauthorized
- GET
/syncs/progress/:document - Headers:
x-auth-user,x-auth-key - Response:
200OK with progress data,404Not found
Returns all synced documents for the authenticated user, ordered by most recently updated. This endpoint is not used by the KOReader client — it's available for building dashboards or browsing your library.
- GET
/syncs/documents - Headers:
x-auth-user,x-auth-key - Response:
200OK
{
"documents": [
{
"document": "8b03a82761fae0ee6cd5a23700361e74",
"progress": "/body/DocFragment[15]/body/div[65]/text()[1].41",
"percentage": 0.2082,
"device": "boox",
"device_id": "197E7C6B3FD54A749C87DE9C1B05A3CE",
"filename": "the_great_gatsby.epub",
"title": "The Great Gatsby",
"authors": "F. Scott Fitzgerald",
"timestamp": 1703123456
}
]
}filename, title, and authors are null for documents synced before metadata support was added.
- GET
/health - Response:
200{"status": "ok"}
bun install
bun run devThe dev server runs with hot reload. Create a .env file to configure environment variables locally (see Configuration).
