Config reference
All services are configured via environment variables. Each service reads its own set; variables shared across services (Postgres, Redis, retention) are listed per service so every section is self-contained.
To run the stack, start from the repo-root .env.example:
copy it to .env and adjust — Compose reads it
automatically. Standalone runs need it sourced first: set -a; source .env; set +a.
Redirector
The Redis-only public server that 302s visitors to their Destination and records each Visit onto the Redis stream. It reads Links from Redis only, never from Postgres.
| Variable | Default | Purpose |
|---|---|---|
| SHRL_REDIRECTOR_ADDR | :8080 | Redirector listen address |
| SHRL_REDIRECTOR_RATE_LIMIT_IP | 600 | Per-IP redirects per minute; 0 disables |
| SHRL_REDIRECTOR_RATE_LIMIT_LINK | 3000 | Per-Link redirects per minute; 0 disables |
| SHRL_REDIS_ADDR | localhost:6379 | Redis address |
| SHRL_REDIS_POOL_SIZE | 50 | Redis connection pool size |
| SHRL_REDIS_MIN_IDLE_CONNS | 5 | Minimum idle Redis connections |
Worker
The analytics aggregator: consumes the Redis visit stream in batches and upserts daily, lifetime, and breakdown rollups into Postgres in a single transaction.
| Variable | Default | Purpose |
|---|---|---|
| SHRL_DATABASE_URL | postgres://shrl:shrl@localhost:5432/shrl | PostgreSQL connection string |
| SHRL_DB_MAX_OPEN_CONNS | 20 | Max open Postgres connections |
| SHRL_DB_MAX_IDLE_CONNS | 5 | Max idle Postgres connections |
| SHRL_DB_CONN_MAX_LIFETIME | 30m | Max lifetime of a Postgres connection |
| SHRL_DB_CONN_MAX_IDLE_TIME | 5m | Max idle time of a Postgres connection |
| SHRL_REDIS_ADDR | localhost:6379 | Redis address |
| SHRL_REDIS_POOL_SIZE | 0 (auto, 10×CPU) | Redis connection pool size |
| SHRL_REDIS_MIN_IDLE_CONNS | 2 | Minimum idle Redis connections |
| SHRL_RETENTION_DAYS | 365 | Analytics retention window (daily rollups) |
| SHRL_GEOLITE_LICENSE | (unset) | MaxMind license key; enables GeoIP attribution |
| SHRL_GEOLITE_DB_PATH | /data/GeoLite2-City.mmdb | Path to the GeoLite2 City database |
Internal API
The API that serves the UI — reachable only by the frontend, which proxies every request on the signed-in user’s behalf and presents the session token.
| Variable | Default | Purpose |
|---|---|---|
| SHRL_API_ADDR | :8080 | Internal API listen address |
| SHRL_API_INTERNAL_SECRET | dev-internal-secret | Shared secret the Internal API demands on every request (set to the same value on the frontend) |
| SHRL_ADMIN_USERNAME | admin | Username of the first-run Admin account |
| SHRL_ADMIN_PASSWORD | (random, shown once) | First-run Admin password (bcrypt-hashed) |
| SHRL_TOKEN_TTL | 86400 | Bearer token lifetime in seconds |
| SHRL_CODE_LENGTH | 6 | Seed for the per-instance Code Length setting (4–12) |
| SHRL_DEFAULT_BASE_URL | http://localhost:8080 | Base URL auto-registered on first run and pre-selected when creating a Link |
| SHRL_DATABASE_URL | postgres://shrl:shrl@localhost:5432/shrl | PostgreSQL connection string |
| SHRL_DB_MAX_OPEN_CONNS | 20 | Max open Postgres connections |
| SHRL_DB_MAX_IDLE_CONNS | 5 | Max idle Postgres connections |
| SHRL_DB_CONN_MAX_LIFETIME | 30m | Max lifetime of a Postgres connection |
| SHRL_DB_CONN_MAX_IDLE_TIME | 5m | Max idle time of a Postgres connection |
| SHRL_REDIS_ADDR | localhost:6379 | Redis address |
| SHRL_REDIS_POOL_SIZE | 0 (auto, 10×CPU) | Redis connection pool size |
| SHRL_REDIS_MIN_IDLE_CONNS | 2 | Minimum idle Redis connections |
| SHRL_RETENTION_DAYS | 365 | Analytics retention window (daily rollups) |
Auth API
The public /v1 API for scripts and CI, authenticated by an API key on every request and rate-limited per IP and per key.
| Variable | Default | Purpose |
|---|---|---|
| SHRL_AUTH_ADDR | :8080 | Auth API listen address |
| SHRL_AUTH_RATE_LIMIT_IP | 60 | Per-IP requests per minute |
| SHRL_AUTH_RATE_LIMIT_KEY_READ | 300 | Per-key reads per minute |
| SHRL_AUTH_RATE_LIMIT_KEY_WRITE | 30 | Per-key writes per minute |
| SHRL_AUTH_RATE_LIMIT_FAIL | 10 | Failed key validations per minute per IP |
| SHRL_DEFAULT_BASE_URL | http://localhost:8080 | Base URL pre-selected when creating a Link |
| SHRL_DATABASE_URL | postgres://shrl:shrl@localhost:5432/shrl | PostgreSQL connection string |
| SHRL_DB_MAX_OPEN_CONNS | 20 | Max open Postgres connections |
| SHRL_DB_MAX_IDLE_CONNS | 5 | Max idle Postgres connections |
| SHRL_DB_CONN_MAX_LIFETIME | 30m | Max lifetime of a Postgres connection |
| SHRL_DB_CONN_MAX_IDLE_TIME | 5m | Max idle time of a Postgres connection |
| SHRL_REDIS_ADDR | localhost:6379 | Redis address |
| SHRL_REDIS_POOL_SIZE | 0 (auto, 10×CPU) | Redis connection pool size |
| SHRL_REDIS_MIN_IDLE_CONNS | 2 | Minimum idle Redis connections |
| SHRL_RETENTION_DAYS | 365 | Analytics retention window (daily rollups) |
Frontend
The SvelteKit admin UI: signs users in with an HttpOnly session cookie and proxies every API call to the Internal API.
| Variable | Default | Purpose |
|---|---|---|
| SHRL_API_URL | http://localhost:8080 | Internal API address the UI proxies to |
| SHRL_API_INTERNAL_SECRET | dev-internal-secret | Shared secret the Internal API demands on every request (set to the same value on the api) |
| SHRL_DEFAULT_BASE_URL | http://localhost:8080 | Base URL pre-selected when creating a Link |
| SHRL_SESSION_SECRET | (random per boot) | HMAC secret for signing UI session cookies |
| SHRL_SESSION_TTL | 86400 | UI session cookie lifetime in seconds |
| SHRL_COOKIE_SECURE | false | Set true to send the session cookie over TLS only |
Images & production notes
shrl.io publishes one image per service to ghcr.io, built for linux/amd64 and
linux/arm64. The compose.yaml in the repo root assembles
the full stack: these five images plus PostgreSQL and Redis.
| Image | Runs | Ports |
|---|---|---|
| ghcr.io/barats/shrl-io-api | Internal API (UI-only) | none, compose-network only |
| ghcr.io/barats/shrl-io-auth | Auth API for API Keys | 8083 |
| ghcr.io/barats/shrl-io-redirect | Redirector | 8080 |
| ghcr.io/barats/shrl-io-worker | Analytics worker | none |
| ghcr.io/barats/shrl-io-frontend | UI server | 8082 |
First run: the admin account
The first user is an Admin, provisioned on first run. Its password is the value of SHRL_ADMIN_PASSWORD if you exported one, otherwise a random value printed once to the api service logs. Change it promptly.
The two secrets
Generate SHRL_API_INTERNAL_SECRET and SHRL_SESSION_SECRET with openssl rand -hex 32, and never reuse values from another deployment. The frontend and the api must agree on the internal secret.
Quickstart
Download compose.yaml from the repo root, export the two secrets, and run podman compose up -d (or docker compose up -d). Then sign in at localhost:8082.
GeoIP attribution (optional)
Set SHRL_GEOLITE_LICENSE on the worker — a free MaxMind account — to attribute country, region, and city. Without it, locations report as unknown. The database defaults to /data/GeoLite2-City.mmdb, mounted as a volume in compose.
TLS
Serve the redirector and frontend behind an HTTPS reverse proxy, set SHRL_COOKIE_SECURE=true on the frontend, and point SHRL_DEFAULT_BASE_URL at the redirector’s public URL.
Network exposure
Keep PostgreSQL, Redis, and the Internal API off the public internet. The production compose file publishes only the redirector (:8080), the Auth API (:8083), and the frontend (:8082).
Upgrades
The compose file tracks the latest images, so podman compose pull && podman compose up -d upgrades to the newest release. To control when you upgrade, pin a release tag (e.g. :0.1.0); every release also tags its minor version (:0.1).
Pre-1.0 caution
shrl.io is pre-1.0: watch releases, pin versions when surprises are expensive, and read the release notes before upgrading — security fixes land only in the latest release. API keys never expire, so rotate them when in doubt.
This page mirrors the Configuration and Production sections of the shrl-io README, which is the canonical source.