Skip to content

Runtime Keys and API Workflow ​

This page focuses on runtime config keys (stored in DB) and practical update workflow via Admin API.

Update Runtime Config via Admin API ​

Runtime config can be updated via the Admin API /{routes.admin_prefix}/v1/config (default: /admin/v1/config).

For first-time setup, set an admin password first: ./shortlinker reset-password (api.admin_token is empty by default, and Admin API is unavailable until set).

Because Admin API uses JWT cookies, you need to login first:

bash
# 1) Login to obtain cookies
curl -sS -X POST \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"password":"your_admin_token"}' \
  http://localhost:8080/admin/v1/auth/login

# Extract CSRF token (required for PUT/POST/DELETE write operations)
CSRF_TOKEN=$(awk '$6=="csrf_token"{print $7}' cookies.txt | tail -n 1)

# 2) List all configs
curl -sS -b cookies.txt \
  http://localhost:8080/admin/v1/config

# 3) Get a single config
curl -sS -b cookies.txt \
  http://localhost:8080/admin/v1/config/features.random_code_length

# 4) Update a config
curl -sS -X PUT \
  -b cookies.txt \
  -H "X-CSRF-Token: ${CSRF_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"value":"8"}' \
  http://localhost:8080/admin/v1/config/features.random_code_length

# 5) Reload config
# Note: CLI `config set/reset` auto-attempts hot reload only for keys that don't require restart.
# `config import` performs one best-effort reload attempt after import.
# If IPC is unreachable (server not running, ipc.enabled=false, socket mismatch, etc.),
# call this endpoint manually.
curl -sS -X POST -b cookies.txt \
  -H "X-CSRF-Token: ${CSRF_TOKEN}" \
  http://localhost:8080/admin/v1/config/reload

# 6) Config history (optional limit, default 20, max 100)
curl -sS -b cookies.txt \
  "http://localhost:8080/admin/v1/config/features.random_code_length/history?limit=10"

Sensitive values (e.g. api.admin_token, api.jwt_secret) are masked as [REDACTED] in API responses.

Config action note: currently only api.jwt_secret supports the generate_token action.

Runtime Config Keys ​

These settings are stored in the database and can be changed at runtime via the admin panel / Admin API.

API ​

KeyTypeDefaultRestartDescription
api.admin_tokenString(empty)NoAdmin login password for POST /admin/v1/auth/login. Empty by default; when empty, Admin API and frontend panel return 404
api.health_tokenString(empty)NoBearer token for Health API (Authorization: Bearer ..., recommended for monitoring/probes; if empty, only JWT cookie auth is available). Note: health endpoints are treated as disabled only when both api.admin_token and api.health_token are empty (returns 404)
api.jwt_secretString(auto-generated)YesJWT signing secret
api.access_token_minutesInteger15YesAccess token TTL (minutes)
api.refresh_token_daysInteger7YesRefresh token TTL (days)
api.cookie_secureBooleantrueNoHTTPS-only cookies (browser-facing; re-login recommended after changes)
api.cookie_same_siteEnumLaxNoSameSite policy: Strict / Lax / None (re-login recommended after changes)
api.cookie_domainString(empty)NoCookie domain (re-login recommended after changes)
api.trusted_proxiesStringArray[]YesTrusted direct peer IPs or CIDRs for TCP reverse proxies. When empty, every TCP request uses the connection peer IP and ignores X-Forwarded-For. When configured, X-Forwarded-For is accepted only if the direct peer matches the list, e.g. ["10.0.0.1", "172.17.0.0/16"]. Unix socket mode trusts the local proxy transport automatically.

Notes:

  • Cookie names are fixed: shortlinker_access / shortlinker_refresh / csrf_token (not configurable).
  • api.admin_token is stored as an Argon2 hash in the database. Use ./shortlinker reset-password to rotate the admin password.
  • Current versions do not auto-generate an admin password file; run ./shortlinker reset-password during initial deployment.
  • Current implementation detail: JWT service reads config on first use and then caches it in-process (OnceLock). These keys are marked as requiring restart; after changing api.jwt_secret, api.access_token_minutes, or api.refresh_token_days, restart the service to affect newly issued/validated tokens.

Routes ​

Note: these prefixes are treated as “reserved short-code prefixes”. Short link code cannot equal these prefixes (without the leading /) and cannot start with {prefix}/, otherwise it will conflict with system routes.

KeyTypeDefaultRestartDescription
routes.admin_prefixString/adminYesAdmin API prefix
routes.health_prefixString/healthYesHealth API prefix
routes.frontend_prefixString/panelYesAdmin panel (frontend) prefix

Features ​

KeyTypeDefaultRestartDescription
features.enable_admin_panelBooleanfalseYesEnable web admin panel
features.random_code_lengthInteger6NoRandom short code length
features.default_urlStringhttps://esap.cc/repoNoDefault redirect URL for /

Click tracking ​

KeyTypeDefaultRestartDescription
click.enable_trackingBooleantrueYesEnable click tracking
click.flush_intervalInteger30YesFlush interval (seconds)
click.max_clicks_before_flushInteger100YesMax clicks before flush

Cache Maintenance ​

KeyTypeDefaultRestartDescription
cache.bloom_rebuild_intervalInteger14400YesPeriodic Bloom filter rebuild interval in seconds (0 disables periodic rebuild)

Notes:

  • This value is read at startup to create the background periodic task; restart is required after changes.
  • The task triggers ReloadTarget::Data to rebuild Bloom filter periodically and reduce long-running false-positive accumulation.

Detailed Analytics ​

KeyTypeDefaultRestartDescription
analytics.enable_detailed_loggingBooleanfalseYesEnable detailed click logging (writes to click_logs table)
analytics.enable_auto_rollupBooleantrueYesEnable automatic data retention / rollup-table cleanup task (runs every 4 hours by default)
analytics.log_retention_daysInteger30NoRaw click log retention in days (cleaned by the background task; requires analytics.enable_auto_rollup)
analytics.hourly_retention_daysInteger7NoHourly rollup retention in days (cleans click_stats_hourly / click_stats_global_hourly; requires analytics.enable_auto_rollup)
analytics.daily_retention_daysInteger365NoDaily rollup retention in days (cleans click_stats_daily / click_stats_global_daily; requires analytics.enable_auto_rollup)
analytics.enable_ip_loggingBooleantrueNoWhether to record IP addresses
analytics.enable_geo_lookupBooleanfalseNoReserved GeoIP switch (currently not consumed in click-write path; country/city remain null by default)
analytics.sample_rateFloat1.0NoDetailed logging sample rate (0.0-1.0; 1.0 = log all clicks, 0.1 = log ~10% of clicks)
analytics.max_log_rowsInteger0NoMaximum rows in click_logs (0 = unlimited)
analytics.max_rows_actionEnumcleanupNoBehavior when max_log_rows is exceeded: cleanup (delete oldest rows) or stop (stop detailed logging)

Note:

  • analytics.enable_detailed_logging is marked as restart-required. After changing it, restart the server for the setting to take effect. When enabled, each click is recorded to the click_logs table with detailed fields (timestamp, referrer, user_agent_hash, etc). User-Agent strings are deduplicated into the user_agents table and linked by hash (used by device/browser analytics).
  • analytics.enable_ip_logging controls whether IPs are recorded. In the current implementation, GeoIP lookup is not wired into the click-write path yet, so click_logs.country/city remain null by default and geo-distribution endpoints may return empty arrays (unless historical data already contains geo fields).
  • click_logs.source is derived by this order: use utm_source from request query first; if absent, extract domain from Referer and store ref:{domain}; if both are missing, store direct.
  • Data retention/cleanup is controlled by analytics.enable_auto_rollup: when enabled, it periodically cleans expired data according to analytics.log_retention_days / analytics.hourly_retention_days / analytics.daily_retention_days.
  • In the current implementation, retention parameters are read when the background task starts; after changing retention days, you may need to restart the server for the cleanup task to pick up new values.

UTM passthrough ​

KeyTypeDefaultRestartDescription
utm.enable_passthroughBooleanfalseNoEnable UTM passthrough during redirect (only utm_source / utm_medium / utm_campaign / utm_term / utm_content)

Notes:

  • Disabled by default. When enabled, UTM params are appended only if those keys exist in the incoming request URL.
  • If target URL already has a query string, params are appended with &; otherwise with ?.
  • Current implementation appends raw incoming UTM query fragments directly (no extra URL decode/re-encode step).

CORS ​

KeyTypeDefaultRestartDescription
cors.enabledBooleanfalseYesEnable CORS (when disabled, no CORS headers are added; browser keeps same-origin policy)
cors.allowed_originsStringArray[]YesAllowed origins (JSON array; ["*"] = allow any origin; empty array = same-origin only / no cross-origin)
cors.allowed_methodsEnumArray["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"]YesAllowed methods
cors.allowed_headersStringArray["Content-Type","Authorization","Accept"]YesAllowed headers (for cross-origin + cookie write ops, you typically also need X-CSRF-Token)
cors.max_ageInteger3600YesPreflight cache TTL (seconds)
cors.allow_credentialsBooleanfalseYesAllow credentials (needed for cross-origin cookies; when configured together with ["*"], credentials are forcibly disabled for safety)

For config priority, see Configuration Guide to keep a single source of truth.

Released under the MIT License