Configuration

TOML config file with environment variable overrides.

Config file

Keplor looks for keplor.toml by default. Override with --config:

$ keplor run --config /etc/keplor/keplor.toml

Full example

# keplor.toml

[server]
listen_addr = "0.0.0.0:8080"
shutdown_timeout_secs = 25
request_timeout_secs = 30
max_connections = 10000

[storage]
db_path = "/var/lib/keplor/keplor.db"
retention_days = 90          # legacy global GC (prefer [retention] tiers)
wal_checkpoint_secs = 300
max_db_size_mb = 0           # 0 = unlimited
read_pool_size = 4           # SQLite read connections (1-64)
gc_interval_secs = 3600     # how often GC runs (0 = disabled)

[auth]
api_keys = ["prod-svc:sk-prod-abc123", "staging:sk-staging-def456"]

# Extended format with tier assignment:
# [[auth.api_key_entries]]
# id = "pro-user"
# secret = "sk-pro-key"
# tier = "pro"

[retention]
default_tier = "free"

[[retention.tiers]]
name = "free"
days = 7

[[retention.tiers]]
name = "pro"
days = 90

# [[retention.tiers]]
# name = "team"
# days = 180

[cors]
allowed_origins = []     # empty = same-origin only; ["*"] = allow all

[pipeline]
batch_size = 64
max_body_bytes = 10485760
channel_capacity = 32768

[idempotency]
enabled = true
ttl_secs = 300
max_entries = 100000

[rate_limit]
enabled = false
requests_per_second = 100.0
burst = 200

# Optional: archive old events to S3/R2 (requires --features s3)
# [archive]
# bucket = "keplor-archive"
# endpoint = "https://<account-id>.r2.cloudflarestorage.com"
# region = "auto"
# access_key_id = "..."
# secret_access_key = "..."
# prefix = "events"
# archive_after_days = 30
# archive_threshold_mb = 500
# archive_batch_size = 10000

# [tls]
# cert_path = "/etc/keplor/cert.pem"
# key_path = "/etc/keplor/key.pem"

[server]

KeyTypeDefaultDescription
listen_addrstring"0.0.0.0:8080"Bind address
shutdown_timeout_secsu6425Graceful shutdown timeout (drain + checkpoint)
request_timeout_secsu6430Per-request timeout
max_connectionsusize10000Maximum concurrent connections

[storage]

KeyTypeDefaultDescription
db_pathstring"keplor.db"SQLite database path
retention_daysu6490Legacy global GC threshold (0 = disabled). Prefer [retention] tiers.
wal_checkpoint_secsu64300WAL truncation interval (0 = disabled)
max_db_size_mbu640Max database size in MB (0 = unlimited). Returns HTTP 507 when exceeded.
read_pool_sizeusize4Number of SQLite read connections (range: 1–64)
gc_interval_secsu643600How often GC runs in seconds (0 = disabled)

[auth]

KeyTypeDefaultDescription
api_keysstring[][]Simple API keys (id:secret or bare secret). Assigned to default_tier. Empty = open access.
api_key_entriesobject[][]Extended keys with explicit tier: { id, secret, tier }

[retention]

Per-tier retention policies. GC runs one pass per tier, deleting events older than the tier's days threshold.

KeyTypeDefaultDescription
default_tierstring"free"Tier assigned to simple-format keys and unauthenticated requests
tiersobject[]free (7d), pro (90d)Named tiers with retention days. days = 0 keeps events forever.

Tiers are just names — add "team", "enterprise", or any custom tier via config. No code changes needed.

[archive] (optional, --features s3)

Archive old events to an S3-compatible object store (Cloudflare R2, MinIO, AWS S3) as compressed JSONL files. Archived events are deleted from SQLite; daily rollups are preserved.

KeyTypeDefaultDescription
bucketstringS3 bucket name
endpointstringS3 endpoint URL
regionstringRegion ("auto" for R2, "us-east-1" for AWS)
access_key_idstringAccess key
secret_access_keystringSecret key
prefixstring""Key prefix in bucket (e.g. "events")
path_styleboolfalsePath-style addressing (required for MinIO)
archive_after_daysu6430Archive events older than this many days
archive_after_hoursu640Sub-day archival (hours). Overrides archive_after_days when non-zero. Set to 1 for hourly offload.
archive_threshold_mbu640Also archive when SQLite exceeds this size (MB). 0 = age-only.
archive_batch_sizeusize10000Maximum events per JSONL archive file
archive_interval_secsu643600How often the archive loop runs (seconds). Default: 1 hour.

Archival runs every archive_interval_secs (default 1 hour). Events are grouped by (user_id, day), serialized to JSONL, compressed with zstd, and uploaded to S3/R2. S3 connectivity is verified at startup. See Event Archival for the full lifecycle.

Important: Set archive_after_days lower than your shortest retention tier's days value, or GC will delete events before they can be archived.

[cors]

KeyTypeDefaultDescription
allowed_originsstring[][]CORS origin allowlist. Empty = same-origin only. ["*"] = allow all (not recommended).

[pipeline]

KeyTypeDefaultRangeDescription
batch_sizeusize641 – 100,000Events per batch flush
max_body_bytesusize104857601 – 100MBMax request body size
channel_capacityusize327681 – ∞Batch writer queue depth. Raise for bursty traffic.

[idempotency]

KeyTypeDefaultDescription
enabledbooltrueEnable idempotency key support
ttl_secsu64300Cache TTL for idempotency keys (seconds)
max_entriesusize100000Maximum cached idempotency keys (LRU eviction)

[rate_limit]

KeyTypeDefaultDescription
enabledboolfalseEnable per-key rate limiting
requests_per_secondf64100.0Token bucket refill rate per API key
burstusize200Maximum burst size per API key

[tls]

Optional. When present, the server listens with HTTPS via rustls.

KeyTypeDescription
cert_pathstringPath to PEM-encoded certificate chain
key_pathstringPath to PEM-encoded private key

Environment variables

Override any key with KEPLOR_ prefix and underscore nesting:

$ KEPLOR_SERVER_LISTEN_ADDR=0.0.0.0:9090 \
  KEPLOR_STORAGE_DB_PATH=/tmp/keplor.db \
  KEPLOR_AUTH_API_KEYS=sk-key1,sk-key2 \
  keplor run

Environment variables take precedence over the config file.

SQLite pragmas

Applied automatically at startup:

PragmaValuePurpose
journal_modeWALConcurrent reads during writes
synchronousNORMALFsync on checkpoint only
mmap_size256 MBMemory-mapped I/O
busy_timeout5000 msRetry on lock contention
cache_size64 MBPage cache
temp_storeMEMORYTemp tables in RAM
wal_autocheckpoint1000 pagesAuto-checkpoint WAL to bound growth

Validation

Config is validated at startup. Invalid values produce immediate errors:

Error: invalid config: pipeline.batch_size must be > 0