Klokk Anchor
Klokk Anchor is a companion service that writes a daily, tamper-evident proof of your time-tracking data to the IOTA Rebased (Starfish) blockchain. Each event is reduced to a one-way SHA-256 commitment combined into a Merkle tree; only the daily Merkle root is anchored, so no personal data ever leaves toward the chain. Optionally adds an RFC 3161 trusted timestamp.
Prerequisites: a running Klokk with EXTENSIONS_ENABLED=true; a Klokk API key with the read:time_entries and read:companies scopes; and two 32-byte hex secrets (one for at-rest encryption, one for company-token hashing). For mainnet, a funded IOTA wallet (created in the anchor UI); Klokk’s anchor smart contract is the built-in default.
Image: klokkme/anchor / ghcr.io/klokk-me/anchor (tags stable, latest). PostgreSQL is supported via DATABASE_URL.
docker-compose.yml
Section titled “docker-compose.yml”services: klokk-anchor: image: klokkme/anchor:stable restart: unless-stopped ports: - "127.0.0.1:9090:9090" volumes: - anchor-data:/app/data environment: ACCEPT_TERMS: "true" # accepts the terms of use: https://klokk.me/terms KLOKK_BASE_URL: "https://klokk.example.com" KLOKK_API_KEY: "klokk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" DATABASE_URL: "sqlite:/app/data/klokk-anchor.db" ENCRYPTION_KEY: "change-me-openssl-rand-hex-32" COMPANY_TOKEN_PEPPER: "change-me-openssl-rand-hex-32" IOTA_NETWORK: "mainnet" TZ: "Europe/Berlin" TIMEZONE: "Europe/Berlin"
volumes: anchor-data:services: klokk-anchor: image: klokkme/anchor:stable restart: unless-stopped ports: - "127.0.0.1:9090:9090" environment: ACCEPT_TERMS: "true" # accepts the terms of use: https://klokk.me/terms KLOKK_BASE_URL: "https://klokk.example.com" KLOKK_API_KEY: "klokk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" DATABASE_URL: "postgresql://klokk:klokk@postgres:5432/klokk_anchor" ENCRYPTION_KEY: "change-me-openssl-rand-hex-32" COMPANY_TOKEN_PEPPER: "change-me-openssl-rand-hex-32" IOTA_NETWORK: "mainnet" TZ: "Europe/Berlin" TIMEZONE: "Europe/Berlin" depends_on: postgres: condition: service_healthy
postgres: image: postgres:18-alpine restart: unless-stopped environment: POSTGRES_DB: klokk_anchor POSTGRES_USER: klokk POSTGRES_PASSWORD: klokk volumes: - postgres-data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U klokk -d klokk_anchor"] interval: 10s timeout: 5s retries: 5
volumes: postgres-data:services: klokk-anchor: image: klokkme/anchor:stable restart: unless-stopped read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true pids_limit: 200 ports: - "127.0.0.1:9090:9090" volumes: - anchor-data:/app/data environment: ACCEPT_TERMS: "true" # accepts the terms of use: https://klokk.me/terms KLOKK_BASE_URL: "https://klokk.example.com" KLOKK_API_KEY: "klokk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" DATABASE_URL: "sqlite:/app/data/klokk-anchor.db" ENCRYPTION_KEY: "change-me-openssl-rand-hex-32" COMPANY_TOKEN_PEPPER: "change-me-openssl-rand-hex-32" IOTA_NETWORK: "mainnet" TZ: "Europe/Berlin" TIMEZONE: "Europe/Berlin"
volumes: anchor-data:services: klokk-anchor: image: klokkme/anchor:stable restart: unless-stopped read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true pids_limit: 200 ports: - "127.0.0.1:9090:9090" environment: ACCEPT_TERMS: "true" # accepts the terms of use: https://klokk.me/terms KLOKK_BASE_URL: "https://klokk.example.com" KLOKK_API_KEY: "klokk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" DATABASE_URL: "postgresql://klokk:klokk@postgres:5432/klokk_anchor" ENCRYPTION_KEY: "change-me-openssl-rand-hex-32" COMPANY_TOKEN_PEPPER: "change-me-openssl-rand-hex-32" IOTA_NETWORK: "mainnet" TZ: "Europe/Berlin" TIMEZONE: "Europe/Berlin" depends_on: postgres: condition: service_healthy
postgres: image: postgres:18-alpine restart: unless-stopped read_only: true tmpfs: - /var/run/postgresql - /tmp cap_drop: [ALL] cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETGID, SETUID] security_opt: - no-new-privileges:true environment: POSTGRES_DB: klokk_anchor POSTGRES_USER: klokk POSTGRES_PASSWORD: klokk volumes: - postgres-data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U klokk -d klokk_anchor"] interval: 10s timeout: 5s retries: 5
volumes: postgres-data:Generate the secrets first: run openssl rand -hex 32 twice — for ENCRYPTION_KEY and for COMPANY_TOKEN_PEPPER. Changing ENCRYPTION_KEY after wallets exist makes them permanently unreadable. Every variable can also come from a file (Docker secrets): <NAME>_FILE, for example ENCRYPTION_KEY_FILE.
Environment variables
Section titled “Environment variables”| Variable | Default | Description |
|---|---|---|
ACCEPT_TERMS Required |
— | Set to true to accept the terms of use. Without it Klokk Anchor logs the link and exits. |
KLOKK_BASE_URL Required |
— | Base URL of your Klokk instance. |
KLOKK_API_KEY Required |
— | Extension API key with read:time_entries + read:companies. A company-scoped key limits the anchor to that company. Keep secret. |
DATABASE_URL |
sqlite:data/klokk-anchor.db | SQLite path (sqlite:…) or PostgreSQL DSN (postgresql://…). |
ENCRYPTION_KEY Required |
— | 64-char hex (32 bytes). AES-256-GCM key for stored wallet private keys. openssl rand -hex 32. |
COMPANY_TOKEN_PEPPER Required |
— | Secret mixed into company tokens before SHA-256 hashing, so company IDs cannot be reversed from on-chain values. |
IOTA_NETWORK |
mainnet | mainnet, testnet or localnet. IOTA_ANCHOR_PACKAGE defaults to Klokk’s deployed package for the selected network. |
TIMEZONE / DAILY_BOOKING_TIME |
UTC / 00:00 | IANA timezone and fixed daily booking time (HH:MM) for anchoring the previous day. |
ANCHOR_EMPTY_DAYS |
true | Anchor a proof even on days with no entries (positive “nobody worked” evidence). One transaction per company per empty day. |
TSA_URL |
https://freetsa.org/tsr | RFC 3161 timestamping authority; only the daily Merkle root is sent to it. The default FreeTSA is not a qualified eIDAS service — for that, set a qualified QTSP. Empty (TSA_URL=) turns timestamping off. Best-effort; never blocks anchoring. |
LEDGER_URLS |
— | Comma-separated Klokk Ledger URLs to cross-check proofs during verification — an operator-independent fourth check. |
ERROR_REPORTS |
— | Fixes whether this installation shares anonymised error reports with the Klokk team. Unset: superadmins decide under Error log (off until switched on). true: always on. false: off, and Report a bug and Suggest a feature are disabled too. What is sent: see the privacy policy. |
ERROR_REPORTS_URL |
https://radar.klokk.me | Klokk Radar: where error reports, bug reports and feature requests go, and where the update notice reads the newest version. |
UPDATE_CHECK |
true | Twice a day, reads the newest Klokk Anchor version from Klokk Radar’s public release list and shows superadmins an update notice. false turns it off. |
ADMIN_PORT |
9090 | Port for the web UI and REST API (bind behind a reverse proxy for remote access). |
Notifications
Section titled “Notifications”Klokk Anchor can notify you via ntfy, a webhook or e-mail when anchoring fails, a submitted proof is not confirmed (or its on-chain root does not match), Klokk cannot be read during a booking, or a wallet balance falls below WALLET_LOW_BALANCE_MIST. Problems of the same kind are combined into one message (at most one every 15 minutes), so a catch-up over many days does not flood you. Superadmins can send a test notification from the dashboard, or run klokk-anchor --test-notification in the container.
| Variable | Default | Description |
|---|---|---|
NOTIFY_NTFY_URL |
— | ntfy topic URL, e.g. https://ntfy.sh/my-anchor or your own server. |
NOTIFY_NTFY_TOKEN |
— | ntfy access token for protected topics. |
NOTIFY_NTFY_USER / _PASSWORD |
— | Alternatively: ntfy user and password. |
NOTIFY_WEBHOOK_URL |
— | Receives the same notifications as a JSON POST (event, title, message, priority, count, url). |
NOTIFY_WEBHOOK_SECRET |
— | Signs the webhook body: header X-Klokk-Anchor-Signature: sha256=<hex HMAC-SHA256>. |
NOTIFY_SMTP_HOST |
— | Mail server; switches e-mail notifications on. |
NOTIFY_SMTP_FROM |
— | Sender, e.g. klokk-anchor <[email protected]> (required for e-mail). |
NOTIFY_SMTP_TO |
— | Recipients, comma-separated (required for e-mail). |
NOTIFY_SMTP_USER / _PASSWORD |
— | Sign-in at the mail server; only over an encrypted connection. |
NOTIFY_SMTP_SECURITY |
starttls | starttls (port 587), tls (465) or none (25, only for a relay in the local network, without sign-in). |
NOTIFY_SMTP_PORT |
— | Overrides the port that follows from NOTIFY_SMTP_SECURITY. |
NOTIFY_EVENTS |
all | Comma-separated selection of anchor_failed, proof_failed, klokk_unreachable, wallet_low_balance. |
NOTIFY_LANGUAGE |
en | Language of the messages: en or de. |
NOTIFY_NAME |
klokk-anchor | Prefix of every title, to tell several instances apart. |
WALLET_LOW_BALANCE_MIST |
100000000 | Low-balance threshold in MIST (1 IOTA = 10⁹ MIST); one notification per drop below it. |
PUBLIC_BASE_URL |
— | The address of the interface as the browser opens it, e.g. https://anchor.example.com. Passkeys belong to this address (relying-party id and origin, set apart with WEBAUTHN_RPID and WEBAUTHN_ORIGIN); without it passkeys can only be added at localhost. Notifications link to this address. |