The easiest way to self-host this sync server is using Docker Compose:
- Create a new directory for your sync server:
mkdir koreader-sync && cd koreader-sync- Create a
docker-compose.ymlfile with the following content:
services:
kosync:
image: ghcr.io/nperez0111/koreader-sync:latest
container_name: kosync
ports:
- 3000:3000
healthcheck:
test: ["CMD", "wget" ,"--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 5m
timeout: 3s
restart: unless-stopped
volumes:
- data:/app/data
volumes:
data:- Start the server:
docker compose up -dYour sync server will now be running at http://localhost:3000. The SQLite database will be automatically persisted in a Docker volume.
- Open a document on your KOReader device and navigate to Settings → Progress Sync → Custom sync server
- Enter your server's URL (e.g.,
http://localhost:3000if running locally) - Select "Register / Login" to create an account or sign in
- Test the connection by selecting "Push progress from this device now"
- Enable automatic progress syncing in the settings if desired
- Install dependencies:
bun install- Set up environment variables:
# Create a .env file with the following variables (change values as needed):
PASSWORD_SALT="your_secure_random_string"
PORT=3000
HOST="0.0.0.0"- Run the development server:
bun run devThe application uses Pino for structured logging. Logs are output in JSON format in production and pretty-printed in development.
debug: Detailed information for debugging (health checks, auth attempts)info: General information about application flow (user actions, successful operations)warn: Warning messages (authentication failures, validation errors)error: Error messages (database errors, unhandled exceptions)
LOG_LEVEL: Set the minimum log level (default:info)NODE_ENV: Set todevelopmentfor pretty-printed logs,productionfor JSON logs
{
"level": 30,
"time": 1703123456789,
"msg": "User registered successfully",
"username": "john_doe"
}- Request/Response Logging: All HTTP requests and responses are automatically logged
- Structured Data: All logs include relevant context (user IDs, document names, etc.)
- Error Tracking: Comprehensive error logging with stack traces
- Performance Monitoring: Request duration tracking
- Security: Sensitive data like passwords are never logged
- POST
/users/create - Headers:
content-type: application/json
- Body:
{ "username": "string", "password": "string" } - Response: 201 (Created) or 402 (Username exists)
- GET
/users/auth - Headers:
x-auth-user: Usernamex-auth-key: Password/API Keyaccept: application/vnd.koreader.v1+json
- Response: 200 (OK) or 401 (Unauthorized)
- PUT
/syncs/progress - Headers:
x-auth-user: Usernamex-auth-key: Password/API Keyaccept: application/vnd.koreader.v1+jsoncontent-type: application/json
- 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 only when the user enables the "Send document metadata" toggle in the KOSync settings (see koreader/koreader#15306). When omitted, previously stored metadata for the document is preserved.
- Response: 200 (OK) or 401 (Unauthorized)
- GET
/syncs/progress/:document - Headers:
x-auth-user: Usernamex-auth-key: Password/API Keyaccept: application/vnd.koreader.v1+json
- Response: 200 (OK) with progress data or 404 (Not Found)
Returns every document the authenticated user has synced, including any captured metadata, ordered by most recently updated. This endpoint is specific to this server — the official KOReader client does not call it, but it is useful for building dashboards or browsing your synced library.
- GET
/syncs/documents - Headers:
x-auth-user: Usernamex-auth-key: Password/API Keyaccept: application/vnd.koreader.v1+json
- Response: 200 (OK)
{
"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 will be null for documents synced before metadata was sent (or when the KOReader toggle is off).
- Password Hashing: All passwords are hashed using bcrypt with additional salt
- Custom Headers: Uses KOReader's custom authentication headers
- SQLite Security: Uses parameterized queries to prevent SQL injection
- Environment Variables: Sensitive configuration like password salt is loaded from environment variables
For production use, you should also:
- Use HTTPS to encrypt all traffic
- Implement rate limiting to prevent brute force attacks
- Add input validation and sanitization
- Regularly backup the SQLite database (located in
/app/data/koreader-sync.db) - Consider adding request signing for additional security
- Use a strong, random PASSWORD_SALT in production
