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
KeyDefaultMeaning
port8080Listen port
bindautoExplicit bind address. Auto = 127.0.0.1 without an API key, 0.0.0.0 with one
api-keyunsetEnables auth on /v1/* (X-Api-Key / Bearer) and non-loopback binding
pool3Concurrent render pages (see Capacity)
webhook-allowed-hostsunsetCSV 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-flagsunsetExtra Chromium flags appended to the built-in set (--disable-gpu + a V8 heap cap that bounds render memory)
recycle-renders100Replace each render page after N documents (bounds Chromium's per-render memory growth during large batches; lower = flatter memory, slightly slower)
allow-remotefalseLet rendered content fetch http(s) resources; also gates /v1/render-url
allowed-hostsallCSV allowlist when remote is on; *.suffix supported
max-body8388608Request 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-enabled1Master 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-hostsunsetCSV 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-byok1Users 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-ms120000Upstream 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-quota4294967296Byte 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-key before exposing it to a network.
  • Rendering sandbox: templates execute inside Chromium's sandbox (kept fully enabled; the service itself runs as the unprivileged snap_daemon user 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-hosts restricts the rest. Blocked requests are logged ([ssrf] blocked …) and reported to API clients via X-Kimidoc-Blocked-Remote response 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=true turns 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 master api-key remains 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.stamp watermarks every page (DRAFT/COPY/VOID). Passwords are passed to the bundled qpdf through 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: pool render pages work concurrently; further requests queue up to 8× pool, beyond which the API returns 503 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 as 504.
  • 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

SymptomCheck
Service inactive after installPort 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 PDFsX-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 waitForReadyThe template never calls window.pdfReady()
Frequent 503sRaise pool, add client backoff, or scale out behind a load balancer
Can't reach from another machineNo API key set → service binds localhost only by design