Documentation
Deployment, Configuration, Shift Planning & Extensions
1. Docker Images
| Tag | Description |
|---|---|
stable | Production-ready. Recommended for self-hosting. |
latest | Latest release, may include recent fixes not yet promoted to stable. |
2. Quick Start
The simplest way to run Klokk is with SQLite — no external database needed. For production, a volume mount is required to persist data across container restarts.
Docker run (SQLite)
docker run -d \
--name klokk \
-p 8080:8080 \
-v klokk-data:/app/data \
-e SESSION_SECRET="$(openssl rand -hex 32)" \
-e CSRF_KEY="$(openssl rand -hex 32)" \
-e SESSION_SECURE_COOKIE=false \
-e BASE_URL=http://localhost:8080 \
klokkme/klokk:stable docker-compose.yml (SQLite)
services:
klokk:
image: klokkme/klokk:stable
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- klokk-data:/app/data
environment:
BASE_URL: "http://localhost:8080"
SESSION_SECRET: "change-me-min-32-chars-random-string"
CSRF_KEY: "change-me-min-32-chars-random-string"
SESSION_SECURE_COOKIE: "false"
volumes:
klokk-data: admin (email admin@localhost) and the default password, then change it immediately. See DEFAULT_ADMIN_PASSWORD.
3. Environment Variables
All configuration is done via environment variables. Variables marked Required have no default and must be set before starting Klokk.
Application
| Variable | Default | Description |
|---|---|---|
APP_ENV | dev | Runtime mode: production or dev. Set production for real deployments; dev relaxes some security checks. |
PORT | 8080 | HTTP listen port inside the container. |
BASE_URL | http://localhost:8080 | Public base URL (no trailing slash). Used for links in emails. |
LOG_LEVEL | warn | Log verbosity: debug, info, warn, error. |
LOCALES_PATH | locales | Path to the locale files directory (relative to the binary or absolute). |
Database
| Variable | Default | Description |
|---|---|---|
DB_DRIVER | sqlite | Database backend: sqlite or postgres. |
DB_SQLITE_PATH | ./data/klokk.db | Path to the SQLite file. Mount /app/data as a volume to persist it. |
DB_DSN | — | PostgreSQL connection string, e.g. postgres://user:pass@host:5432/db?sslmode=disable. Required when DB_DRIVER=postgres. |
DB_MAX_OPEN_CONNS | 25 | Max open DB connections (PostgreSQL only). |
DB_MAX_IDLE_CONNS | 5 | Max idle DB connections (PostgreSQL only). |
DB_CONN_MAX_LIFETIME_MINUTES | 5 | Connection max lifetime in minutes (PostgreSQL only). |
Security Required
| Variable | Default | Description |
|---|---|---|
SESSION_SECRET | — | Secret for signing session cookies. Min 32 chars. Must be changed. Generate: openssl rand -hex 32 |
CSRF_KEY | — | Secret for CSRF token signing. Min 32 chars. Must be changed. Generate: openssl rand -hex 32 |
SESSION_SECURE_COOKIE | false | Set true in production (requires HTTPS). false for HTTP/localhost. |
SESSION_SAME_SITE | lax | Cookie SameSite policy: strict, lax, or none. |
SESSION_MAX_AGE_HOURS | 24 | Default session lifetime in hours. |
Authentication
| Variable | Default | Description |
|---|---|---|
REMEMBER_ME_ENABLED | true | Show "Stay logged in" checkbox on the login page. |
MFA_TRUST_ENABLED | true | Show "Trust this device" checkbox on the MFA page. |
MFA_TRUST_DAYS | 30 | Duration in days for remembered sessions and trusted MFA devices. |
DEFAULT_ADMIN_PASSWORD | Admin@Klokk1 | Password for the initial admin account (username admin, email admin@localhost). Only used on first startup. Change immediately after login. |
Admin Access Control
| Variable | Default | Description |
|---|---|---|
ADMIN_IP_ALLOWLIST | empty | Comma-separated IPs/CIDRs allowed to access /admin/*. Empty = no restriction. X-Forwarded-For is respected for reverse proxies. |
SUPER_ADMIN_IP_ALLOWLIST | empty | Additional allowlist for super_admin-only routes (/admin/settings, /admin/companies, /admin/audit). Stacks on top of ADMIN_IP_ALLOWLIST. |
Password Policy
| Variable | Default | Description |
|---|---|---|
PASSWORD_MIN_LENGTH | 12 | Minimum password length. |
PASSWORD_REQUIRE_UPPER | true | Require at least one uppercase letter. |
PASSWORD_REQUIRE_LOWER | true | Require at least one lowercase letter. |
PASSWORD_REQUIRE_NUMBER | true | Require at least one digit. |
PASSWORD_REQUIRE_SPECIAL | true | Require at least one special character. |
PASSWORD_MAX_HISTORY | 5 | Number of previous passwords to remember (prevent reuse). 0 = disabled. |
PASSWORD_EXPIRY_DAYS | 0 | Password expiry in days. 0 = never expires. |
Rate Limiting
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_LOGIN_PER_MINUTE | 10 | Max login attempts per IP per minute. |
RATE_LIMIT_MFA_PER_MINUTE | 10 | Max MFA attempts per IP per minute. |
RATE_API_PER_MINUTE | 100 | Max API / general requests per IP per minute. |
AUTH_LOCKOUT_THRESHOLD | 0 | Failed login attempts before account lockout. 0 = disabled. |
AUTH_LOCKOUT_DURATION_MINUTES | 15 | Account lockout duration in minutes. |
Internationalisation
| Variable | Default | Description |
|---|---|---|
I18N_DEFAULT_LOCALE | en | Default locale for new users and the login page: en or de. |
I18N_AVAILABLE_LOCALES | en,de | Comma-separated list of enabled locales. |
Working Time Defaults
Applied to the admin seed user on first startup. Per-user values are managed in the app afterwards.
| Variable | Default | Description |
|---|---|---|
DEFAULT_WEEKLY_HOURS | 40 | Default weekly working hours. |
DEFAULT_ANNUAL_LEAVE_DAYS | 30 | Default annual leave days. |
Shift Planning
Shift planning itself is enabled per company in the app (Admin → Companies), not via environment variables. See Shift Planning below for the full workflow. The one server-level setting is the pre-shift push reminder:
| Variable | Default | Description |
|---|---|---|
SHIFT_REMINDER_HOURS | 0 | Send a push reminder this many hours before a published shift starts. Sent once per shift to employees who enabled the "Shift plan published or changed" push notification. 0 = disabled. |
SMTP (Email)
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | empty | SMTP server hostname. Leave empty to disable email sending entirely. |
SMTP_PORT | 587 | SMTP server port. 587 for STARTTLS, 465 for implicit TLS. |
SMTP_USER | — | SMTP username. |
SMTP_PASS | — | SMTP password. |
SMTP_TLS | false | true = implicit TLS (port 465). false = STARTTLS (port 587). |
SMTP_FROM | empty | From address for outgoing emails. Leave empty to disable. |
API & Extensions
Klokk ships a read-only public REST API under /api/v1/*
(/status, /users,
/clock/states, /time/entries,
/companies). Requests are authenticated with an API key and authorised per scope
(e.g. read:users, read:time_entries),
and rate-limited to 60 requests per minute per IP.
Super-admins manage integrations under API & Extensions
(/admin/extensions). Each integration bundles an API key, its scopes, and one
connection mode:
- API only — the extension calls the REST API above using its key.
- Webhooks — Klokk sends outbound HTTP POSTs on clock-in, clock-out, and leave events.
- WebSocket — the extension opens a persistent inbound connection at
GET /api/v1/extension/connect.
Set EXTENSIONS_ENABLED=false to switch off the extension subsystem at the network level.
The read-only REST API endpoints listed above remain available; only the webhook/WebSocket extension features below are disabled.
| Variable | Default | Description |
|---|---|---|
EXTENSIONS_ENABLED | true |
Enable the extension API subsystem. When set to false:
|
4. Reverse Proxy
When running Klokk behind a reverse proxy, set these two variables in addition to your proxy config:
- Set
SESSION_SECURE_COOKIE=true— required when served over HTTPS. - Set
BASE_URL=https://yourdomain.com— no trailing slash.
nginx
server {
listen 443 ssl;
server_name yourdomain.com;
# TLS config omitted — use certbot or similar
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
} Caddy
yourdomain.com {
reverse_proxy localhost:8080
} Caddy automatically provisions and renews TLS certificates via Let's Encrypt — no additional TLS configuration needed.
5. Shift Planning
Shift planning lets admins and managers build weekly rosters from reusable shift templates, publish them to employees, and handle day-to-day changes through swaps, open shifts and availability — with German working-time law (ArbZG) checks built in. It is opt-in: a tenant must have it enabled, and only users on the shift-based work-time model draw their target hours from the roster.
a. Enabling shifts
A super-admin turns the feature on per company under Admin → Companies:
- Shift planning — enables shift templates and rosters for the company.
- Roster notice period (days) — advance notice required before a roster may be published (e.g. from a collective or works agreement).
0= none. The planner sees a "publish by" hint and a warning once the period has passed.
Once enabled, a Shifts entry appears for admins and managers, and a My shifts entry for every employee.
b. Work-time model
Each user's daily target hours come from their work-time model:
- Weekly — fixed/flexible weekly hours from the work schedule (the default).
- Shift-based — target hours are derived from the published roster instead of weekly hours.
- Inherit — use the department default, then fall back to weekly.
Set it per user under Admin → Users, or as a department default under Admin → Departments. Only shift-based users are affected by the roster; weekly users keep their schedule.
c. Templates & roster
Templates are reusable shift definitions (e.g. "Früh 06:00–14:00") created under Shifts. Each has a name, start and end time (if the end is not after the start, the shift crosses midnight, e.g. 22:00–06:00), a break in minutes (subtracted from the gross to give paid net time), a colour for the grid, and an active flag. Shifts in the statutory night window (23:00–06:00) are detected automatically for the ArbZG checks.
The roster is a weekly grid of employees × days. Assign a template per cell, add an optional note, and use Copy previous week to carry a recurring plan forward. Drafts are invisible to employees and do not affect target hours. When ready, choose Publish & notify: assignments become visible in My shifts, feed shift-based target hours, notify affected employees (email and/or push, per their preferences), and show each employee a precise change diff versus the last published version.
d. Swaps, open shifts & availability
- Shift swaps (give-away). From My shifts an employee gives away a published shift; a colleague takes it over and a manager/admin approves the reassignment (
open → claimed → approved, or rejected/withdrawn). Only an approved swap moves the assignment. - Open shifts. A planner offers an unassigned slot to the pool; employees apply and a manager approves, turning it into a normal assignment (
open → claimed → filled). - Availability. Employees mark days Unavailable or Preferred (no entry = no preference). Assigning someone on a day they marked unavailable flags a conflict on the roster.
e. Employee view, calendar & reminders
My shifts shows the employee's own published roster as a week or month; they can acknowledge the plan ("I have seen my shifts") and manage availability, swaps and open-shift applications from here.
Calendar subscription. Employees can enable a calendar feed and subscribe to the generated iCalendar URL (/shifts/feed/<token>) from any calendar app; it auto-updates, and disabling it revokes the link.
Pre-shift reminders. Set SHIFT_REMINDER_HOURS (see Shift Planning under environment variables) to push a reminder that many hours before a shift starts — once per shift, to employees who enabled the "Shift plan published or changed" push notification.
ArbZG audit. Klokk checks rosters against the German Working Time Act — daily/weekly maximums, the 11-hour rest period, and night-work rules — and the Yearly ArbZG audit view gives a per-employee, full-year compliance overview.
6. Extensions
Extensions are external services that integrate with Klokk through its API & extension system: a scoped, read-only REST API, outbound webhooks, and inbound WebSocket connections. The configuration variables live in API & Extensions above; this section covers the in-app workflow and the first-party extensions.
a. In-app usage
A super-admin manages integrations under Admin → API & Extensions (/admin/extensions). Each integration bundles an
API key (shown once — store it securely), the scopes it may use, and one connection mode:
- API only — the extension polls the REST API using its key (
/api/v1/status,/users,/clock/states,/time/entries,/shifts/events,/companies); rate-limited to 60/min per IP. - Webhooks — Klokk sends outbound HTTP POSTs on clock-in, clock-out and leave events.
- WebSocket — the extension opens a persistent inbound connection at
GET /api/v1/extension/connect.
Scopes (e.g. read:users, read:time_entries, read:companies) authorise each key; grant only what an extension needs. The public API is read-only. Setting EXTENSIONS_ENABLED=false removes the admin routes, the WebSocket endpoint and all webhooks (the plain REST API stays available).
b. 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
services:
klokk-anchor:
image: klokkme/anchor:stable
restart: unless-stopped
ports:
- "127.0.0.1:9090:9090"
volumes:
- anchor-data:/app/data
environment:
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: "testnet"
TZ: "Europe/Berlin"
TIMEZONE: "Europe/Berlin"
volumes:
anchor-data: openssl rand -hex 32 twice for ENCRYPTION_KEY and COMPANY_TOKEN_PEPPER. Changing ENCRYPTION_KEY after wallets exist makes them permanently unreadable.
Environment variables
| Variable | Default | Description |
|---|---|---|
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 | testnet | testnet, mainnet or localnet. IOTA_ANCHOR_PACKAGE defaults to Klokk's deployed package. |
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 | — | Optional RFC 3161 timestamping authority (may be a qualified eIDAS QTSP). Best-effort; never blocks anchoring. |
LEDGER_URLS | — | Comma-separated Klokk Ledger URLs to cross-check proofs during verification — an operator-independent fourth check. |
ADMIN_PORT | 9090 | Port for the web UI and REST API (bind behind a reverse proxy for remote access). |
c. Klokk Ledger
Klokk Ledger is an independent indexer and verification service. It reads Klokk Anchor proofs back off the public IOTA ledger, stores them, and serves a read-only verification API — so anyone can verify a proof without trusting the operator, and even if the ledger node later prunes old transactions. It needs no API key and no secrets; it only reads public data, and on first run backfills the full package history automatically.
Image: klokkme/ledger / ghcr.io/klokk-me/ledger (tags stable, latest). SQLite is pure-Go (no CGO); PostgreSQL optional.
docker-compose.yml
services:
klokk-ledger:
image: klokkme/ledger:stable
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
volumes:
- ledger-data:/app/data
environment:
IOTA_NETWORK: "mainnet"
IOTA_ANCHOR_PACKAGE: "0x..." # same package your anchor writes to
DB_DRIVER: "sqlite"
DB_DSN: "data/klokk-ledger.db"
POLL_INTERVAL: "60s"
volumes:
ledger-data: Environment variables
| Variable | Default | Description |
|---|---|---|
IOTA_ANCHOR_PACKAGE Required | — | Address of the klokk_anchor Move package to index — the same one your anchor writes to. |
IOTA_NETWORK | testnet | testnet, mainnet or devnet (selects the default GraphQL endpoint; override with IOTA_GRAPHQL_URL). |
DB_DRIVER / DB_DSN | sqlite / data/… | sqlite or postgres, with the matching file path or DSN. |
LISTEN_ADDR | :8080 | Address the HTTP API listens on. |
POLL_INTERVAL | 60s | How often to poll the ledger for new proof events (e.g. 30s, 5m). |
ADDRESSES | all | Optional comma-separated anchor sender addresses to index. Empty = every proof for the package. |
Verification API
All endpoints are served openly. GET /api/v1/verify/{tx}?root=<expected> returns the independently-indexed proof and whether it matches the expected Merkle root — the same cross-check Klokk Anchor performs via LEDGER_URLS.
| Endpoint | Description |
|---|---|
GET /api/v1/status | Network, package, indexed count, last index time. |
GET /api/v1/verify/{tx} | Verification verdict; pass ?root= to compare against the indexed root. |
GET /api/v1/proofs/{tx} | A single indexed proof by transaction digest. |
GET /api/v1/proofs | List proofs for a sender, with company_token/period filters. |
GET /api/v1/companies/{token}/proofs | Proofs for a company token, optionally by period. |