A personal e-book server becomes more useful when it can do two jobs: maintain the library and remember where you stopped reading. Calibre-Web Automated handles browsing, imports and conversion. KOReader’s sync server stores reading positions for the reader’s built-in Progress Sync plugin. A small bridge brings those positions into the library’s web interface.

This guide combines the three in one Docker Compose file, behind an existing Traefik reverse proxy. Once the initial accounts and configuration are ready, the whole application stack starts with docker compose up -d. It also covers migrating an existing Calibre-Web installation while retaining its users and library.

Changelog

DateChange
2026-10-25Clarified synchronization latency for larger libraries.
2026-10-25Initial version: Automated library, private KOReader sync, progress bridge and migration workflow.

1. What the stack does

Browser -----------------------> Calibre-Web Automated
                                      ^
                                      | progress updates
KOReader <--> KOReader Sync <--- progress bridge
                server                |
                                      +-- reads library checksums
ComponentResponsibilityPersistent data
Calibre-Web Automated, or CWALibrary UI, user accounts, imports, conversion and CWA reading stateApplication configuration and Calibre library
KOReader SyncDevice-to-device reading positions using the normal Progress Sync protocolIts bundled Redis data
kosync-cwa-bridgePolls the sync server and forwards matching positions to CWAA small cache of already forwarded updates

The bridge is one-way: KOReader Sync → CWA. Changes made only in CWA do not travel back through it. Its current implementation maps one sync account to one CWA account. The default polling interval is a five-minute pause after each complete pass. Querying every unique checksum takes additional time, so larger libraries can take longer to reflect an update. A book also needs a matching checksum in the CWA library. These are explicit upstream limitations.

Why use a separate sync server?

CWA already includes a /kosync service, intended for its own KOReader plugin. That is a smaller alternative if you are happy to install that plugin on your readers. The separate server in this guide lets you keep KOReader’s ordinary Progress Sync plugin instead. The bridge translates between the two authentication schemes. More components are not a guarantee of greater reliability; choose this arrangement for the compatibility you actually need. CWA syncing documentation, bridge architecture.

This guide uses CWA v4.0.8, KOReader Sync v2.1.1 and bridge commit 286ef6f4b20955fa4a0b3f0a5f96550f70b15880. The bridge is a young project; its source is pinned here so a restart cannot silently replace it. Check current releases and advisories before your own deployment. Version pins make changes deliberate; they do not establish that every bundled dependency is free of vulnerabilities.

2. Prerequisites

You need:

  • Docker Engine and Docker Compose 2.30 or newer. The example uses the raw format for a private environment file, which Docker documents from that version onward.
  • A working Traefik v3 and CrowdSec stack, with the external Docker network proxy, entrypoint websecure, certificate resolver tls_resolver, and the file middlewares crowdsec-bouncer and default. Adapt those names if your proxy uses different ones.
  • A DNS record for a name such as books.example.com, pointing to that proxy.
  • Administrative access to the Docker host, plus Python 3 and Git.
  • Enough disk space for the images, library, conversions and backups. The example limits the three containers to about 1 GiB of RAM in total; conversion workloads may need a larger CWA limit and sufficient host reserve.
  • A working KOReader installation. For an older Kindle, see the installation and plugin guide.

The commands below run in an administrative shell. They do not install a second proxy, change your firewall or configure public DNS. First check the existing network and your Compose version:

docker compose version
docker network inspect proxy >/dev/null

Docker’s environment-file documentation explains format: raw. The Compose services reference describes resource limits, bind mounts and health dependencies.

3. Prepare the directories and source

Choose a new directory for this application:

mkdir -p /opt/ebook-stack
cd /opt/ebook-stack
mkdir -p config library ingest bridge/config bridge/data sync/redis sync/logs/app sync/logs/redis
chmod 700 config bridge/config

Keep imports and the managed library separate. CWA removes files from the ingest directory after processing. Never point that directory at your existing collection. Official Compose template.

Fetch the bridge source once:

git clone https://github.com/ericchansen/kosync-cwa-bridge.git bridge/upstream
git -C bridge/upstream checkout --detach 286ef6f4b20955fa4a0b3f0a5f96550f70b15880
git -C bridge/upstream rev-parse HEAD

Check that the last command prints that exact commit. Keep this source directory and its license: the bridge container will run the code directly from it. It is a runtime dependency, not a disposable build directory.

The example reuses CWA’s Python image for the bridge. The selected image contains requests and PyYAML, which the bridge imports. This avoids a separate local build or reliance on a prebuilt bridge image being available. Only the bridge’s Python entrypoint runs in that container; the CWA web application runs separately. Check this dependency pairing again when changing either project. Bridge package requirements.

4. Environment and migration

Create .env:

LIBRARY_DOMAIN=books.example.com
LIBRARY_PATH=./library
PUID=1000
PGID=1000
TZ=Etc/UTC
NETWORK_SHARE_MODE=false
DISABLE_LIBRARY_AUTOMOUNT=false
PUBLISH_ROUTES=false
KOSYNC_ALLOW_REGISTRATION=true

PUBLISH_ROUTES=false keeps both applications out of Traefik during initial setup. CWA will be reachable through a loopback-only port. Sync registration is enabled only during this private bootstrap; disable it before publishing the routes.

Use the UID/GID that should own your CWA configuration and ingest files. The public example uses 1000:1000; that is not a universal value. Prepare ownership for the writable CWA and bridge directories, substituting your chosen IDs:

chown -R 1000:1000 config ingest bridge/config bridge/data
chmod 700 config bridge/config
chmod 600 .env

Do not recursively change ownership of an existing shared library just to match this example. Check its established access policy instead.

Migrating from stock Calibre-Web

Skip this subsection for a new installation.

  1. Back up the old Compose, complete configuration directory and library, including metadata.db. Confirm you can read or restore the backup.
  2. Stop the old Calibre-Web instance and any other application writing to that library.
  3. Copy the old complete configuration directory into this stack’s config/. Preserve hidden files, particularly .key, as well as app.db and any additional databases.
  4. Set LIBRARY_PATH to the existing library’s real host path. CWA sees it as /calibre-library; stock Calibre-Web commonly used /books.
  5. Keep the old Compose and untouched original configuration for rollback. Start only one library writer during the migration.

For example, after stopping the old service:

cp -a /path/to/old/config/. /opt/ebook-stack/config/
chown -R 1000:1000 /opt/ebook-stack/config
chmod 700 /opt/ebook-stack/config

The paths above are placeholders. Do not overwrite a configuration directory that already contains another active CWA installation.

CWA documents migration by reusing the configuration and library mounts. This retains existing accounts and settings, while CWA adds its own schema and features. Some binary paths and UI settings can change; v4’s interface is not identical to stock Calibre-Web. Official migration instructions.

Libraries on NFS or SMB

Set NETWORK_SHARE_MODE=true for a network library. CWA uses this to avoid SQLite WAL on the share, skip problematic ownership changes and use polling watchers. Verify the share is genuinely mounted and the expected metadata.db is readable before starting. A local directory left behind by a missing network mount is not the library. CWA network-share guidance.

If you disable automatic library detection, explicitly configure CWA’s library directory as /calibre-library. Leave automatic detection enabled for an empty new library so CWA can initialize it.

5. One Compose file for all three services

Save this as docker-compose.yml:

name: ebook-stack

services:
  cwa:
    image: &cwa-image crocodilestick/calibre-web-automated:v4.0.8@sha256:5e00373854247750cc3e4479b492ae09293ff5e06ed10177f226634d97888679
    restart: unless-stopped
    stop_grace_period: 60s
    mem_limit: 768m
    memswap_limit: 896m
    pids_limit: 256
    cpus: 1.5
    environment:
      PUID: ${PUID}
      PGID: ${PGID}
      TZ: ${TZ}
      NETWORK_SHARE_MODE: ${NETWORK_SHARE_MODE}
      DISABLE_LIBRARY_AUTOMOUNT: ${DISABLE_LIBRARY_AUTOMOUNT}
    volumes:
      - ./config:/config
      - ./ingest:/cwa-book-ingest
      - type: bind
        source: ${LIBRARY_PATH}
        target: /calibre-library
        bind:
          create_host_path: false
    ports:
      - "127.0.0.1:8083:8083"
    networks: [default, proxy]
    labels:
      traefik.enable: ${PUBLISH_ROUTES}
      traefik.docker.network: proxy
      traefik.http.routers.ebook-library.rule: Host(`${LIBRARY_DOMAIN}`)
      traefik.http.routers.ebook-library.entrypoints: websecure
      traefik.http.routers.ebook-library.tls: "true"
      traefik.http.routers.ebook-library.tls.certresolver: tls_resolver
      traefik.http.routers.ebook-library.middlewares: crowdsec-bouncer@file,default@file
      traefik.http.routers.ebook-library.service: ebook-library
      traefik.http.services.ebook-library.loadbalancer.server.port: "8083"
    logging: &logging
      driver: json-file
      options: {max-size: "10m", max-file: "3"}

  kosync:
    image: koreader/kosync:v2.1.1@sha256:bb3f13615365703315a43b9059f65e71e876440f867e23a42bf27f2fa18264e1
    restart: unless-stopped
    stop_grace_period: 30s
    mem_limit: 128m
    memswap_limit: 128m
    pids_limit: 64
    cpus: 0.5
    environment:
      TZ: ${TZ}
      ENABLE_USER_REGISTRATION: ${KOSYNC_ALLOW_REGISTRATION}
    volumes:
      - ./sync/redis:/var/lib/redis
      - ./sync/logs/redis:/var/log/redis
      - ./sync/logs/app:/app/koreader-sync-server/logs
    networks: [default, proxy]
    labels:
      traefik.enable: ${PUBLISH_ROUTES}
      traefik.docker.network: proxy
      traefik.http.routers.ebook-sync.rule: Host(`${LIBRARY_DOMAIN}`) && (Path(`/koreader`) || PathPrefix(`/koreader/`))
      traefik.http.routers.ebook-sync.priority: "100"
      traefik.http.routers.ebook-sync.entrypoints: websecure
      traefik.http.routers.ebook-sync.tls: "true"
      traefik.http.routers.ebook-sync.tls.certresolver: tls_resolver
      traefik.http.routers.ebook-sync.middlewares: crowdsec-bouncer@file,default@file,ebook-sync-prefix
      traefik.http.middlewares.ebook-sync-prefix.stripprefix.prefixes: /koreader
      traefik.http.routers.ebook-sync.service: ebook-sync
      traefik.http.services.ebook-sync.loadbalancer.server.port: "17200"
    logging: *logging

  bridge:
    image: *cwa-image
    restart: unless-stopped
    stop_grace_period: 20s
    mem_limit: 128m
    memswap_limit: 128m
    pids_limit: 32
    cpus: 0.25
    user: "${PUID}:${PGID}"
    entrypoint: [python3, -m, kosync_cwa_bridge]
    working_dir: /bridge
    environment:
      TZ: ${TZ}
      PYTHONDONTWRITEBYTECODE: "1"
      KOSYNC_CWA_BRIDGE_CONFIG: /config/config.yml
    env_file:
      - path: ./bridge/config/bridge.env
        format: raw
        required: false
    volumes:
      - ./bridge/upstream/src:/bridge:ro
      - ./bridge/config:/config:ro
      - ./bridge/data:/data
      - type: bind
        source: ${LIBRARY_PATH}
        target: /calibre-library
        read_only: true
        bind:
          create_host_path: false
    tmpfs:
      - /cwa-book-ingest:size=1m,mode=0700
    depends_on:
      cwa: {condition: service_healthy}
      kosync: {condition: service_healthy}
    healthcheck: {disable: true}
    labels: {traefik.enable: "false"}
    logging: *logging

networks:
  default:
    internal: true
  proxy:
    external: true

The sync server’s image includes Redis; you do not need another Redis container. Its 7200 endpoint uses a self-signed TLS certificate. Traefik instead talks to the server’s plain HTTP endpoint on 17200 over Docker networking, and terminates public HTTPS itself. Only CWA has a host port, bound to loopback. KOReader server deployment notes.

The /koreader route has a higher priority than the library route and strips that prefix before forwarding. CWA keeps its separate /kosync API for the bridge. Adapt router and middleware names if another application already uses them. Traefik StripPrefix.

The bridge does not run an HTTP server, so CWA’s inherited web healthcheck is disabled for it. A running bridge process alone does not prove synchronization: inspect its preflight and cycle results as described below.

6. Bootstrap CWA privately

Validate without printing expanded configuration or secrets, then start only the two servers:

docker compose config --quiet
docker compose up -d cwa kosync
docker compose ps

On the host, open http://127.0.0.1:8083. From another computer, create an SSH tunnel:

ssh -N -L 18083:127.0.0.1:8083 user@server.example.com

Then browse to http://127.0.0.1:18083 on that computer. Replace the SSH account and server with your own.

For a fresh CWA installation, the documented initial account is admin with password admin123. Change it during this private setup, before enabling the public routes. For a migration, use the existing account instead. Verify the library path and inspect several books. Do not assume a successful login proves the collection is complete. CWA post-installation instructions.

In CWA Settings, enable Enable KOReader Sync (CWA Plugin) and save. Although the label mentions a plugin, the bridge also needs this setting: it enables CWA’s /kosync API and the library checksum table. The bridge supplies the HTTP requests that the CWA plugin would otherwise send. CWA setting and explanation.

Restart CWA to run its checksum backfill after enabling the setting:

docker compose restart cwa
docker compose logs --tail 60 cwa

Let the backfill finish before testing the bridge. It can briefly lock the metadata database. If logs report a missing book_format_checksums table, return to CWA Settings, save the sync setting through the UI and check again; repeatedly restarting without initializing the schema will not create useful progress mappings. CWA’s supplied checksum generator can rerun the backfill once the tables exist:

docker compose exec -T cwa python3 \
  /app/calibre-web-automated/scripts/generate_book_checksums.py --batch-size 25

7. Create the sync account and bridge configuration

The sync server and CWA are separate account systems. You can choose the same username, but the passwords do not need to match. The bridge needs both account credentials. For the pinned CWA version, its /kosync authentication code checks the actual local CWA password; do not assume a newer branch’s app-password feature is present. Pinned CWA authentication implementation.

Save the following as setup-bridge.py in the stack directory. It prompts without echoing passwords, checks the CWA login, registers the private sync account and writes the two protected bridge files. It does not reset an existing account’s password.

#!/usr/bin/env python3
import base64
import getpass
import json
import os
from pathlib import Path
import subprocess
import urllib.request

os.umask(0o077)
root = Path(__file__).resolve().parent
os.chdir(root)

cwa_user = input("Existing CWA username: ").strip()
sync_user = input("New sync username [same]: ").strip() or cwa_user
cwa_password = getpass.getpass("Existing CWA password: ")
sync_password = getpass.getpass("Password for the new sync account: ")
if getpass.getpass("Repeat sync password: ") != sync_password:
    raise SystemExit("Passwords differ; no sync account created.")
if not cwa_user or not sync_user or ":" in sync_user:
    raise SystemExit("Use nonempty usernames; sync usernames cannot contain ':'.")
if not cwa_password or not sync_password or any(
    c in cwa_password for c in "\r\n"
):
    raise SystemExit("Use nonempty passwords without line breaks.")

headers = {
    "Accept": "application/vnd.koreader.v1+json",
    "Authorization": "Basic " + base64.b64encode(
        f"{cwa_user}:{cwa_password}".encode()
    ).decode(),
}
request = urllib.request.Request(
    "http://127.0.0.1:8083/kosync/users/auth", headers=headers
)
try:
    with urllib.request.urlopen(request, timeout=10) as response:
        assert json.load(response)["authorized"] == "OK"
except Exception:
    raise SystemExit("CWA login failed; check credentials and sync settings.")

# Registration runs inside CWA's network, with credentials supplied on stdin.
# The sync server has no published host port.
register = r'''
import hashlib, json, sys, urllib.request, urllib.error
q = json.load(sys.stdin)
key = hashlib.md5(q["password"].encode()).hexdigest()
accept = {"Accept": "application/vnd.koreader.v1+json"}
request = urllib.request.Request(
    "http://kosync:17200/users/create",
    data=json.dumps({"username": q["username"], "password": key}).encode(),
    headers={**accept, "Content-Type": "application/json"},
)
try:
    with urllib.request.urlopen(request, timeout=10) as response:
        assert response.status == 201
except urllib.error.HTTPError as error:
    # An existing name is acceptable only if these credentials authenticate it.
    if json.load(error).get("code") != 2002:
        raise SystemExit("Sync registration refused; check private bootstrap.")
request = urllib.request.Request(
    "http://kosync:17200/users/auth",
    headers={**accept, "x-auth-user": q["username"], "x-auth-key": key},
)
try:
    with urllib.request.urlopen(request, timeout=10) as response:
        assert json.load(response)["authorized"] == "OK"
except Exception:
    raise SystemExit("Sync login failed; existing account left unchanged.")
print(json.dumps({"password_md5": key}))
'''
result = subprocess.run(
    ["docker", "compose", "exec", "-T", "cwa", "python3", "-c", register],
    input=json.dumps({"username": sync_user, "password": sync_password}),
    text=True, capture_output=True, timeout=30,
)
if result.returncode:
    raise SystemExit("Sync setup failed; no bridge files written.")
key = json.loads(result.stdout)["password_md5"]

config = json.loads(subprocess.check_output(
    ["docker", "compose", "config", "--format", "json"], text=True
))
env = config["services"]["cwa"]["environment"]
uid, gid = int(env["PUID"]), int(env["PGID"])
bridge = {
    "kosync_rocks": {
        "url": "http://kosync:17200",
        "username": sync_user,
        "password_md5": key,
    },
    "cwa": {
        "url": "http://cwa:8083",
        "username": cwa_user,
        "password": "__CWA_PASSWORD_JSON__",
    },
    "metadata_db_path": "/calibre-library/metadata.db",
    "cache_db_path": "/data/bridge_cache.db",
    "poll_interval_seconds": 300,
    "request_delay_seconds": 0.2,
    "log_level": "INFO",
}
config_text = json.dumps(bridge, indent=2).replace("$", r"\u0024")
config_text = config_text.replace(
    '"password": "__CWA_PASSWORD_JSON__"',
    '"password": ${CWA_PASSWORD_JSON}',
)
folder = root / "bridge/config"
folder.mkdir(parents=True, exist_ok=True)
os.chown(folder, uid, gid)
folder.chmod(0o700)
for name, data in {
    "config.yml": config_text + "\n",
    "bridge.env": "CWA_PASSWORD_JSON=" + json.dumps(cwa_password) + "\n",
}.items():
    path = folder / name
    path.write_text(data)
    os.chown(path, uid, gid)
    path.chmod(0o600)
print("Accounts checked; protected bridge configuration written.")

Run it from the stack directory:

chmod 700 setup-bridge.py
python3 setup-bridge.py

The configuration key is named kosync_rocks upstream, but its URL points to your own kosync service here. No reading progress is sent to the public KOReader sync service by this example.

The private environment value is JSON-encoded because the upstream bridge substitutes variables before parsing YAML. This preserves quotes, backslashes and dollar signs in a password; JSON encoding is not encryption. format: raw keeps Compose from interpreting the encoded value.

The sync protocol uses an MD5-derived password key. Treat that key as a credential, not as harmless diagnostic data. Keep bridge/config/ private and out of public repositories and screenshots. The private environment file also contains a recoverable CWA password; administrators with Docker access can inspect container environments. KOReader authentication format, bridge configuration reference.

8. Disable registration and publish the stack

Change these values in .env:

KOSYNC_ALLOW_REGISTRATION=false
PUBLISH_ROUTES=true

Then start the complete stack:

docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --tail 30 bridge

Look for successful authentication preflight and completed polling cycles. A cycle with checked=0 is not evidence of successful progress transfer: first check the CWA checksum backfill. Network or authentication errors must be resolved before relying on the bridge.

Confirm both public routes:

curl --fail --silent --show-error --output /dev/null \
  https://books.example.com/login

curl --fail --silent --show-error \
  -H 'Accept: application/vnd.koreader.v1+json' \
  https://books.example.com/koreader/healthcheck

The sync healthcheck should return {"state":"OK"}. Its API expects the vendor-specific Accept header; an ordinary request without it can return a protocol-format error even while the server is healthy. Healthchecks demonstrate availability, not a completed reading-state transfer. Server healthcheck example.

9. Connect KOReader and verify a real book

In KOReader’s built-in Progress Sync settings:

  1. Set Custom sync server to https://books.example.com/koreader.
  2. Log in using the sync account created above.
  3. Use Binary document matching. CWA’s stored checksums identify the file; filename-based matching will not match this bridge’s checksum lookup.
  4. Download a book from this CWA library, open that exact file on the reader, move to a recognizable position and push its progress.
  5. Wait for a bridge cycle and check that CWA reflects the position.
  6. Open the same file on a second KOReader device and explicitly retrieve the progress to verify device-to-device synchronization.

The menu labels and binary-matching option are defined in KOReader’s Progress Sync plugin. The bridge obtains document identifiers from CWA’s checksum table.

If you previously used another sync server, its stored positions do not automatically migrate. Start with the device that has your current reading position and deliberately upload that state to the new server before asking other devices to retrieve it. Keep the reader’s local sidecar data backed up during this transition.

CWA imports and conversions can change a document’s bytes. For a first test, use a freshly downloaded library copy on both readers. If an older sideloaded file has a different checksum, the sync server can store its position while the bridge has no matching CWA entry.

You can also add CWA’s https://books.example.com/opds catalog to KOReader or an OPDS client for browsing and downloads. Catalog access and reading-position synchronization are separate configurations. CWA quick-start endpoints.

10. Backups, updates and rollback

Back up the stack’s configuration, library, bridge source and sync data. Important items include:

ItemWhy it matters
config/app.db and .keyCWA accounts, application state and the key needed for encrypted settings
config/cwa.db and the rest of config/CWA settings and additional persistent configuration
Library files and metadata.dbThe collection and its catalog
sync/redis/Sync accounts and reading positions
bridge/config/Both account mappings and the private bridge credential
Compose, .env and bridge/upstream/Reproducible service configuration and pinned bridge code

The bridge cache is reconstructible; it is less important than account credentials and progress data. CWA’s backups of processed books are useful, but are not a backup of this entire stack. CWA backup feature.

For a simple consistent backup, stop the stack and other writers to the same library, take a filesystem snapshot or copy all required data to your backup destination, then restart. On a shared library, stopping these containers alone does not stop a desktop Calibre instance. An online SQLite backup can produce a consistent individual database copy, but does not make the database, book files and Redis state one atomic recovery point. SQLite Online Backup API, Redis persistence.

Existing backup jobs may name the old Calibre-Web configuration directory explicitly. Update those paths when migrating, include cwa.db, and verify a new backup actually contains readable copies. Do not infer coverage from a scheduler’s green status.

Restore without overwriting the working library

Restore into a separate directory or isolated test environment first. Keep its routes disabled, use distinct Compose project and router names, avoid the live loopback port, and mount the restored library. Verify account access, representative book downloads, settings and progress retrieval. Only then plan a controlled replacement of production data. Prevent a restored test instance from sending mail or triggering other external integrations.

Updates and rollback

Before changing a version, read its release notes, preserve the current image identifiers and take a usable data backup. Change the image reference deliberately, review the bridge dependency pairing and run:

docker compose pull
docker compose up -d

An old image is only part of a rollback. A database migrated by a newer application may require its matching earlier backup. Preserve newer imports and reading progress before restoring an older snapshot. Keeping the old Compose alongside the new file helps recovery, but does not by itself preserve changed data.

11. Troubleshooting

SymptomCheck
Library appears emptyCorrect host bind, mounted share, metadata.db, and CWA’s internal /calibre-library path
CWA login works but bridge auth failsKOReader sync enabled in CWA, correct CWA user/password, and the pinned version’s actual authentication support
Sync registration fails during setupRoutes still private and KOSYNC_ALLOW_REGISTRATION=true; an existing account is not overwritten
Reader sync works but CWA does not changeBinary matching, matching library checksum, bridge account mapping and completed poll cycles
no such table: book_format_checksumsInitialize sync through CWA Settings before checksum generation; inspect startup logs
database is locked on a network libraryNETWORK_SHARE_MODE=true, other writers and the actual NFS/SMB mount
/koreader/healthcheck reports a header errorInclude Accept: application/vnd.koreader.v1+json
Readers work but CWA-only changes do not reach themExpected one-way bridge behavior
Bridge container repeatedly exitsIts two credentials, configuration-file access, source mount and Python dependencies

Useful local diagnostics:

docker compose ps
docker compose logs --tail 60 cwa kosync bridge

Read logs locally before sharing them: they can contain account names, book titles and device identifiers. Avoid dumping expanded Compose configuration or container environments into a support request.

Sources

The migration and the three-service progress path were checked using isolated configuration/database copies and an artificial reading position. The example still requires the real-reader and restore checks described above; it is not a claim of universal reader compatibility or a completed audit of every bundled dependency.