Server configuration lives in config/toki-sync.toml. Environment variables are expanded using ${VAR_NAME} syntax — if the variable is unset, it expands to an empty string.
[server]
# bind = "0.0.0.0"
tcp_port = 9090
http_port = 9091
# trust_proxy = false
# max_concurrent_writes = 10
[auth]
jwt_secret = "${JWT_SECRET}"
# access_token_ttl_secs = 3600
# refresh_token_ttl_secs = 7776000
# brute_force_max_attempts = 5
# brute_force_window_secs = 300
# brute_force_lockout_secs = 900
# registration_mode = "closed"
[storage]
db_path = "/data/toki_sync.db"
[events]
backend = "fjall"
fjall_path = "/data/events.fjall"
# backend = "clickhouse"
# clickhouse_url = "http://clickhouse:8123"
[features]
# max_query_scope = "365d"
[log]
level = "info"
json = true| Key | Type | Default | Description |
|---|---|---|---|
bind |
string | 0.0.0.0 |
Network interface to bind |
http_port |
integer | 9091 |
HTTP API port (REST, dashboard, PromQL proxy) |
tcp_port |
integer | 9090 |
TCP sync protocol port (toki daemon connections) |
external_url |
string | (empty) | Public URL used for JWT iss claim and OIDC redirect URI derivation. Example: https://sync.example.com |
max_concurrent_writes |
integer | 10 |
Maximum parallel event store batch writes. Limits thundering-herd pressure when many devices sync simultaneously |
trust_proxy |
boolean | false |
Trust X-Forwarded-For and X-Real-IP headers from a reverse proxy for client IP resolution (brute force tracking). Only enable when behind a trusted reverse proxy |
| Key | Type | Default | Description |
|---|---|---|---|
jwt_secret |
string | — | Required. HS256 signing key for JWT tokens. Use ${JWT_SECRET} to read from environment. Generate with openssl rand -base64 32 |
access_token_ttl_secs |
integer | 3600 |
Access token lifetime in seconds (default: 1 hour) |
refresh_token_ttl_secs |
integer | 7776000 |
Refresh token lifetime in seconds (default: 90 days) |
brute_force_max_attempts |
integer | 5 |
Maximum failed login attempts before lockout |
brute_force_window_secs |
integer | 300 |
Time window for tracking failed attempts (default: 5 minutes) |
brute_force_lockout_secs |
integer | 900 |
Lockout duration after max attempts exceeded (default: 15 minutes) |
registration_mode |
string | "closed" |
Controls self-registration: "open" (anyone can register), "approval" (registration requires admin approval), "closed" (only admins can create users) |
oidc_issuer |
string | (empty) | OIDC provider URL (e.g., https://accounts.google.com). Empty = OIDC disabled |
oidc_client_id |
string | (empty) | OIDC client ID from your identity provider |
oidc_client_secret |
string | (empty) | OIDC client secret |
oidc_redirect_uri |
string | (empty) | OIDC callback URL (e.g., https://sync.example.com/auth/callback) |
Brute force protection tracks failed login attempts per IP + username combination. When brute_force_max_attempts is exceeded within brute_force_window_secs, the IP+username pair is locked out for brute_force_lockout_secs. This applies to /login, /register, and /token/refresh endpoints.
To enable OIDC (Google, GitHub, etc.), set all four OIDC fields. The server performs standard OIDC discovery on startup and caches the result with a 1-hour TTL.
[auth]
jwt_secret = "${JWT_SECRET}"
oidc_issuer = "https://accounts.google.com"
oidc_client_id = "${OIDC_CLIENT_ID}"
oidc_client_secret = "${OIDC_CLIENT_SECRET}"
oidc_redirect_uri = "https://sync.example.com/auth/callback"| Key | Type | Default | Description |
|---|---|---|---|
backend |
string | sqlite |
Database backend: sqlite or postgres |
sqlite_path |
string | ./data/toki_sync.db |
SQLite database file path. Used when backend = "sqlite" |
db_path |
string | (empty) | Legacy alias for sqlite_path (backward compatible). If set and sqlite_path is default, this value is used |
postgres_url |
string | (empty) | PostgreSQL connection string. Used when backend = "postgres". Example: postgres://user:pass@host/dbname |
- SQLite (default): zero configuration, single-file database. Recommended for personal use and small teams.
- PostgreSQL: better concurrency for large teams. Requires an external PostgreSQL server.
# SQLite (default)
[storage]
backend = "sqlite"
sqlite_path = "/data/toki_sync.db"
# PostgreSQL
[storage]
backend = "postgres"
postgres_url = "postgres://toki:password@db:5432/toki_sync"| Key | Type | Default | Description |
|---|---|---|---|
backend |
string | fjall |
Event store backend: fjall (embedded, no external dependencies) or clickhouse (external ClickHouse server) |
fjall_path |
string | /data/events.fjall |
Fjall database directory path. Used when backend = "fjall" |
clickhouse_url |
string | http://clickhouse:8123 |
ClickHouse HTTP endpoint. Used when backend = "clickhouse" |
- Fjall (default): embedded LSM-tree storage. Zero external dependencies. Data deduplication via
idx_msgunique index onmsg_id. Recommended for personal use and small teams. - ClickHouse: external column-oriented database. Better query performance for large datasets. Data deduplication via
ReplacingMergeTreeengine. Recommended for large teams or when advanced analytics are needed.
# Fjall (default — no external dependencies)
[events]
backend = "fjall"
fjall_path = "/data/events.fjall"
# ClickHouse (requires external ClickHouse server)
[events]
backend = "clickhouse"
clickhouse_url = "http://clickhouse:8123"In Docker Compose, enable ClickHouse with docker compose --profile clickhouse up -d. The default URL (http://clickhouse:8123) works out of the box with the included ClickHouse service.
| Key | Type | Default | Description |
|---|---|---|---|
vm_url |
string | (empty) | VictoriaMetrics HTTP endpoint. Only needed if using the optional PromQL proxy with an external VictoriaMetrics instance |
This section is optional. It is only required for legacy PromQL proxy compatibility (e.g., toki report query --remote or Toki Monitor server mode). If not configured, the /api/v1/query and /api/v1/query_range endpoints will return an error indicating that VictoriaMetrics is not configured.
| Key | Type | Default | Description |
|---|---|---|---|
level |
string | info |
Log level: trace, debug, info, warn, error |
json |
boolean | false |
Output logs in JSON format. Recommended for production (structured logging) |
| Key | Type | Default | Description |
|---|---|---|---|
max_query_scope |
string | (empty) | Maximum time range for PromQL queries (e.g., "365d", "90d"). Empty = unlimited. Prevents expensive queries spanning too much data |
Environment variables are used in two ways:
- In TOML:
${VAR_NAME}syntax for expanding values insidetoki-sync.toml - In
.env: Docker Compose reads.envand injects variables into containers
| Variable | Required | Description |
|---|---|---|
TOKI_ADMIN_PASSWORD |
Yes | Admin account password. Created automatically on first server start |
JWT_SECRET |
Yes | JWT signing key. Generate: openssl rand -base64 32 |
TOKI_EXTERNAL_URL |
Yes | Public URL (e.g., https://yourserver.duckdns.org). Used for JWT iss and OIDC redirects |
DUCKDNS_TOKEN |
Caddy profile only | DuckDNS API token for Let's Encrypt DNS-01 challenge |
TOKI_VERSION |
No | Docker image tag (default: latest) |
# Required
TOKI_ADMIN_PASSWORD=your-strong-password
JWT_SECRET=base64-encoded-secret-here
TOKI_EXTERNAL_URL=https://yourserver.duckdns.org
# Caddy TLS (optional)
DUCKDNS_TOKEN=your-duckdns-token
# Image version (optional)
TOKI_VERSION=0.1.0Security: never commit
.envto version control. The.env.examplefile is provided as a template.
The server loads configuration in this order:
- Read
config/toki-sync.toml(or the path specified by the--configflag) - Expand
${VAR_NAME}placeholders with environment variable values - Parse TOML into the configuration struct
- Apply defaults for any missing fields
If the config file does not exist, the server uses built-in defaults with JWT_SECRET read from the environment (falling back to change-me-in-production if unset).