Administrator Guide
Operating the KimiDoc document-rendering service. For the REST API, see the web app's API docs view.
First run
KimiDoc is licensed per server. Your licence email carries the build and the install steps for your platform, if you do not have them, write to hello@kimidoc.com. The rest of this guide covers running it once it is installed.
snap services kimidoc # daemon should be active curl http://127.0.0.1:8080/health
The service starts automatically and listens on port 8080, bound to localhost only until an API key is configured. Open http://<server>:8080/ in a browser for the web app: template management, an editor with live PDF preview, a filterable library of built-in starter templates, stats, and API documentation. Dependencies (the Qt runtime content snap) install automatically.
Configuration
All configuration is via snap set; any change validates and restarts the service automatically:
sudo snap set kimidoc port=8080 api-key=CHANGE_ME pool=4 sudo snap set kimidoc allow-remote=true allowed-hosts=cdn.example.com,*.gstatic.com snap get kimidoc # view current configuration
| Key | Default | Meaning |
|---|---|---|
| port | 8080 | Listen port |
| bind | auto | Explicit bind address. Auto = 127.0.0.1 without an API key, 0.0.0.0 with one |
| api-key | unset | Enables auth on /v1/* (X-Api-Key / Bearer) and non-loopback binding |
| pool | 3 | Concurrent render pages (see Capacity) |
| webhook-allowed-hosts | unset | CSV of allowed webhook destinations (*.suffix ok). Unset = any public host; internal/LAN receivers must be listed here explicitly (entries override the internal-range block) |
| chromium-flags | unset | Extra Chromium flags appended to the built-in set (--disable-gpu + a V8 heap cap that bounds render memory) |
| recycle-renders | 100 | Replace each render page after N documents (bounds Chromium's per-render memory growth during large batches; lower = flatter memory, slightly slower) |
| allow-remote | false | Let rendered content fetch http(s) resources; also gates /v1/render-url |
| allowed-hosts | all | CSV allowlist when remote is on; *.suffix supported |
| max-body | 8388608 | Request size cap in bytes (also caps asset uploads) |
| public-api-url | (this host) | Base URL shown in the console's copy-paste integration snippets, e.g. https://api.example.com. Set it when the API answers on a different hostname than the console, or behind a reverse proxy. Presentation only, it never affects routing |
| ai-enabled | 1 | Master switch. 0 hides and disables every AI feature, overriding the other ai-* keys, which keep their values for when you turn it back on |
| ai-base-url | (unset) | Server-wide OpenAI-compatible endpoint for AI template generation (with ai-model). Operator-trusted, so a local endpoint such as an Ollama on this machine works |
| ai-api-key | (unset) | Key for that endpoint. Never logged, never shown. In accounts mode, calls on this key consume each tenant's plan budget for AI generations; user-supplied endpoints and self-hosted installs are never metered |
| ai-model | (unset) | Model name, e.g. gpt-4o-mini |
| ai-allowed-hosts | unset | CSV of hosts user-supplied AI endpoints may reach in addition to public ones (*.suffix ok). User endpoints are otherwise blocked from internal addresses, exactly like webhooks |
| ai-byok | 1 | Users may paste their own AI endpoint in the console; it rides each request and is never stored. Set 0 to refuse user-supplied endpoints |
| ai-timeout-ms | 120000 | Upstream AI timeout. Raise it for local models on CPU, which can need several minutes per template |
| billing-secret | (unset) | Shared secret for the payment broker. Leave unset on self-hosted installs: without it the billing routes do not exist |
| billing-checkout-url | (unset) | The broker endpoint the console sends buyers to. Both settings must be present for the Upgrade action to appear |
| storage-quota | 4294967296 | Byte cap for stored blobs: uploaded assets and batch-job outputs (documents + ZIP; a 40k job needs ~2.5 GB). Asset uploads beyond it return 507; a job that hits it fails with retry instructions |
Security model
- Safe by default: without an API key the service refuses to bind beyond localhost. Set
api-keybefore exposing it to a network. - Rendering sandbox: templates execute inside Chromium's sandbox (kept fully enabled; the service itself runs as the unprivileged
snap_daemonuser under strict snap confinement). - SSRF policy: remote fetches by rendered content are off by default. When enabled, requests to loopback, link-local (cloud metadata), and private address ranges are always blocked, and
allowed-hostsrestricts the rest. Blocked requests are logged ([ssrf] blocked …) and reported to API clients viaX-Kimidoc-Blocked-Remoteresponse headers. - Webhooks: outbound job-completion callbacks are SSRF-guarded, internal/loopback/link-local destinations are blocked (checked against resolved addresses at send time) unless explicitly allowlisted via
webhook-allowed-hosts. Payloads are HMAC-signed when the submitter provides a secret. - Multi-tenant mode:
snap set kimidoc multi-tenant=trueturns the install into a self-serve service: visitors sign up, get isolated workspaces (templates, assets, jobs, quotas) and their own hashed API keys, metered per plan (each tenant's allowance and retention window are shown in their Account view). Your masterapi-keyremains the unmetered operator credential, sees every tenant's jobs, and unlocks the Tenants page in the web app: per-workspace usage, last activity (so dormant signups are obvious), storage held, plan changes (paid plans activate automatically through checkout; the manual control remains for exceptions and refunds), and tenant deletion. Off by default: nothing changes for single-team installs. - Document encryption & stamping:
output.encrypt(on synchronous renders and batch jobs alike) produces AES-256 PDFs with per-recipient passwords and optional permission restrictions (allowPrint/allowCopy/allowModify);output.stampwatermarks every page (DRAFT/COPY/VOID). Passwords are passed to the bundledqpdfthrough private argument files, never on process command lines; the server does not store resolved passwords, they exist only in the submitted document data and the job's stored configuration. - TLS: the service speaks plain HTTP, put a reverse proxy (nginx, Caddy) in front for TLS, rate limiting, and request logging at the edge if exposed beyond a trusted network.
- Web app: static assets are served without auth (they contain no secrets); every API call the app makes is authenticated. API docs and this guide are intentionally readable without a key.
Fonts
KimiDoc bundles the Noto families (including CJK and color emoji) so complex-script rendering works identically on every install. Host system fonts are deliberately not used, this keeps output deterministic across machines. To add fonts server-wide:
sudo cp CorpFont-*.ttf /var/snap/kimidoc/common/fonts/ sudo snap restart kimidoc
The directory survives upgrades. Per-template fonts can instead be embedded as @font-face data URIs (no admin access needed). At startup the service logs a warning if coverage for major writing systems is missing.
Templates
- Built-ins: the starter library (invoices, receipts, quotes, proposals, reports, HR, certificates, contracts, forms, healthcare, labels, tickets and more) is synced from the snap at every start and marked built-in: read-only, cannot be deleted, refreshed on upgrades. Each starter carries a category, and the picker filters on it. Users customize them via "Use as base", which saves a copy.
- User templates live in the service database with their sample data and render options.
- Storage: everything is in a single SQLite file at
/var/snap/kimidoc/<rev>/state/templates.db(see Backup).
Monitoring & logs
journalctl -u snap.kimidoc.kimidoc -f # follow logs curl -H "X-Api-Key: KEY" http://127.0.0.1:8080/v1/stats
Log line formats:
POST /v1/render -> 200 from 10.0.0.5 # access log, every request
render ok: 843 ms, 51234 bytes # render detail
GET /wp-admin/x -> 404 (no route) from 1.2.3.4 # unmatched hits (scanners!)
[ssrf] blocked https://... - reason # subresource policy
[page console] ... # JS errors from templates -
# first stop for "renders blank"
/v1/stats returns uptime, render counters (ok / failed / timedOut / rejectedBusy), average render time, pool utilization, and the active remote-fetch config. Expected benign startup warnings (harmless headless artifacts, do not chase): the org.freedesktop.portal D-Bus line, QRhiGles2 / Failed to create RHI GL-probe lines, and the udev monitor error.
Capacity & performance
- Pool:
poolrender pages work concurrently; further requests queue up to 8× pool, beyond which the API returns503 server busy, clients should retry with backoff. Size the pool roughly to CPU cores minus one for sustained load. - Memory: expect on the order of ~250 MB resident for the service with a pool of 3, growing modestly under load. Rendering is software-based by design (
--disable-gpu): no GPU required or used. - Timeouts: renders are bounded per request (
timeoutMs, default 20 s) and surface as504. - Simple renders complete in well under a second; remote assets,
waitMs, and Paged.js add their own time.
Backup & restore
Three things hold all state:
/var/snap/kimidoc/current/state/templates.db # templates + asset metadata /var/snap/kimidoc/common/storage/ # uploaded asset files /var/snap/kimidoc/common/fonts/ # operator-installed fonts
sudo snap stop kimidoc sudo cp /var/snap/kimidoc/current/state/templates.db /backup/ sudo cp -r /var/snap/kimidoc/common/storage /backup/ sudo snap start kimidoc # restore = copy the files back (same paths) and restart
Built-in templates need no backup, they re-sync from the snap. Configuration is snapshotted by snap save or re-applied with snap set.
Upgrades
Snaps auto-refresh; sudo snap refresh kimidoc forces it. On stop the service drains gracefully: it stops accepting connections, finishes in-flight renders (up to 30 s), then exits. API clients see completed responses, not dropped connections. After upgrade, the database schema migrates automatically and built-in templates refresh. Roll back with sudo snap revert kimidoc.
Licensing
KimiDoc is licensed per server, per year, with every feature enabled: batch jobs, encryption, watermarking, webhooks, the editor. There is no free tier for self-hosted installs, the licence issued to you states the servers it covers and its term.
multi-tenant mode, serving separate workspaces to parties outside your own organisation, and support
commitments are covered by your licence terms rather than assumed; ask if you need either.
If asked, we request only the number of servers you have installed on and the version: never an audit of your systems or documents. Licences and renewals: hello@kimidoc.com.
Troubleshooting
| Symptom | Check |
|---|---|
| Service inactive after install | Port conflict? journalctl -u snap.kimidoc.kimidoc; change with snap set kimidoc port=… |
| Template renders blank | [page console] lines in the journal, usually a JS error in the template |
| Images/fonts missing from PDFs | X-Kimidoc-Blocked-Remote header / [ssrf] blocked logs → enable allow-remote, extend allowed-hosts, or inline as data URIs |
| A script renders as boxes (tofu) | Startup "font coverage missing" warning → add fonts to common/fonts |
| 504 with waitForReady | The template never calls window.pdfReady() |
| Frequent 503s | Raise pool, add client backoff, or scale out behind a load balancer |
| Can't reach from another machine | No API key set → service binds localhost only by design |