smoll-url#
smoll-url is a high-performance, self-hosted URL shortener written in Go, with a redesigned Retro-Terminal web interface, SQLite storage, Redis-backed redirect caching, and asynchronous click analytics.
Features#
- Blazing Fast: Written in Go with a performance-tuned SQLite backend (WAL mode enabled).
- Aesthetic UI: A distinctive “Cyberpunk Brutalist” web interface embedded directly into the binary.
- Secure: Argon2id password hashing and API key authentication.
- Flexible: Supports custom slugs with letters, digits,
_,-,:, and;, automatic expiry (TTL), and hit counting. - Production Caching: Optional Redis cache for hot redirect lookups.
- Click Analytics Pipeline: Non-blocking, in-memory queue + background batch writes to SQLite
click_events. - Geo Enrichment (Optional): MaxMind City DB lookup for country/city on click events.
Redirect and Click Flow#
The redirect path is intentionally optimized for low latency:
- User requests
/<slug>. - Server resolves URL (Redis cache first, SQLite fallback).
- Server returns redirect immediately (
307or308depending on config). - Click metadata is pushed to an in-memory queue (non-blocking).
- Background worker batches events, does optional MaxMind lookup, and writes to SQLite (
click_events) in WAL mode.
This keeps the user-facing redirect fast while preserving durable analytics.
Quick Start#
1. Installation#
Ensure you have Go 1.21+ installed, then clone and build:
git clone https://github.com/taf/smoll-url.git
cd smoll-url
go build ./cmd/smoll-url
2. Configuration#
smoll-url uses environment variables only.
For local/dev usage, copy and edit .env.example:
cp .env.example .env
Example .env:
listen_address=0.0.0.0
port=4567
db_url=urls.sqlite
password=your-admin-password
api_key=your-secret-api-key
slug_length=6
use_wal_mode=true
redis_url=redis://localhost:6379/0
2.1 Configuration Reference#
The app loads .env from the current directory by default. You can point to a different env file with env_file.
| Variable | Default | Description |
|---|---|---|
listen_address |
0.0.0.0 |
Interface/address to bind the HTTP server. |
port |
4567 |
HTTP port for the server. |
db_url |
urls.sqlite |
SQLite database file path. |
database |
(unset) | Alias for db_url (supported for compatibility). |
db_path |
(unset) | Alias for db_url (supported for compatibility). |
password |
(empty) | Admin password for web/session login. |
api_key |
(empty) | API key for header auth via X-API-Key. |
hash_algorithm |
(empty) | Set to Argon2 to treat password and/or api_key as Argon2 hashes. |
slug_style |
Pair |
Slug generation style: Pair or UID. |
slug_length |
8 |
Length used for UID slug generation (minimum enforced: 4). |
allow_capital_letters |
false |
Allow uppercase characters in generated/validated slugs. |
try_longer_slug |
false |
If slug_style=UID, retry conflicts with a longer slug. |
public_mode |
false |
Allow unauthenticated short-link creation. |
public_mode_expiry_delay |
0 |
Max/forced expiry for public mode (seconds). 0 means unlimited. |
use_temp_redirect |
false |
Use temporary redirects (307) instead of permanent (308). |
redirect_method |
(unset) | Legacy override: TEMPORARY forces temporary redirect mode. |
redis_url |
(empty) | Redis connection URL for redirect caching (for example redis://localhost:6379/0). |
redis_cache_key_prefix |
smoll-url:redirect: |
Prefix used for Redis cache keys. |
redis_cache_timeout_ms |
200 |
Redis operation timeout in milliseconds. |
click_queue_size |
4096 |
Buffered in-memory queue size for async click events. |
click_batch_size |
200 |
Number of click events written per SQLite batch transaction. |
click_flush_interval_ms |
500 |
Flush interval for draining queued click events. |
maxmind_db_path |
(empty) | Optional MaxMind City DB (.mmdb) path for country/city lookup. |
use_wal_mode |
false |
Enable SQLite WAL mode. |
ensure_acid |
true |
Keep strict SQLite durability/sync settings. |
disable_frontend |
false |
Disable embedded admin frontend. |
site_url |
(empty) | Canonical base URL for generated absolute short URLs. |
cache_control_header |
(empty) | Optional Cache-Control response header value. |
custom_landing_directory |
(empty) | Serve files from a custom landing directory. |
frontend_page_size |
10 |
Default page size for frontend list pagination APIs. |
posthog_key |
(empty) | Optional PostHog project API key. When set, the embedded frontend loads PostHog through the local /ph proxy. |
posthog_project_id |
(empty) | Optional PostHog project ID used by the admin PostHog analytics dashboard. |
posthog_personal_api_key |
(empty) | Optional PostHog personal API key used server-side to query aggregate analytics. |
posthog_base_url |
https://eu.posthog.com |
PostHog app/API base URL for HogQL aggregate analytics queries. |
posthog_api_host |
https://eu.i.posthog.com |
PostHog ingestion host proxied from /ph/*. |
posthog_assets_host |
https://eu-assets.i.posthog.com |
PostHog asset host proxied from /ph/static/*. |
posthog_ui_host |
https://eu.posthog.com |
PostHog UI host passed to the browser SDK. |
env_file |
.env |
Path to env file loaded at startup. |
Notes:
- Boolean values accept
true/false,1/0,yes/no,on/off,enable/disable. - Values already present in the process environment take precedence over
.envvalues. - If
passwordandapi_keyare both empty andpublic_mode=false, protected endpoints require login and will reject unauthenticated access. - For Docker Compose, set
DOCKER_DB_URL=/data/urls.db(included in.env.example) so SQLite writes to the mounted volume. redis_urlis optional; when unset, redirect caching is disabled.maxmind_db_pathis optional; when unset, click events are still stored without geo fields.
Click pipeline tuning notes:
- Increase
click_queue_sizeif you expect traffic bursts and want fewer dropped events under pressure. - Increase
click_batch_sizeto reduce SQLite write frequency at the cost of slightly slower analytics visibility. - Lower
click_flush_interval_msto persist events sooner, or raise it to reduce write overhead.
2.2 Docker Compose#
docker-compose.yml uses the same app variables from .env.
cp .env.example .env
docker compose up -d --build
With the current docker-compose.yml, these are the effective service URLs:
- Browser / API from host machine:
http://localhost:${HOST_PORT:-8080}(default:http://localhost:8080) - App service from another container on the same Compose network:
http://smoll-url:${port:-4567}(default:http://smoll-url:4567) - Redis service from another container on the same Compose network:
redis://redis:6379/0
3. Execution#
Run the binary:
./smoll-url
The server will initialize the SQLite database and start listening on the configured port. Access the web UI at http://localhost:<port>.
Self-Hosting Guide#
This section covers practical production deployment patterns for VPS/home-server setups.
Option A: Run as a Binary + systemd (recommended for minimal setups)#
- Build and place the binary:
go build -o smoll-url ./cmd/smoll-url
sudo mkdir -p /opt/smoll-url
sudo mv smoll-url /opt/smoll-url/
- Create
.envat/opt/smoll-url/.env:
listen_address=0.0.0.0
port=8080
db_url=/opt/smoll-url/urls.db
password=change-me
api_key=change-me
use_wal_mode=true
- Create
/etc/systemd/system/smoll-url.service:
[Unit]
Description=smoll-url URL shortener
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/opt/smoll-url
ExecStart=/opt/smoll-url/smoll-url
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
- Start and enable service:
sudo systemctl daemon-reload
sudo systemctl enable --now smoll-url
sudo systemctl status smoll-url
Option B: Run with Docker Compose#
- Copy env template and configure secrets:
cp .env.example .env
- Use persistent DB path for container mode:
DOCKER_DB_URL=/data/urls.db
- Start container:
docker compose up -d --build
docker compose logs -f smoll-url
docker-compose.yml includes a Redis service by default and wires redis_url=redis://redis:6379/0 for the app container.
If you want geo enrichment in Docker, mount your MaxMind DB into the app container and set maxmind_db_path to that mounted path.
Reverse Proxy (Nginx Proxy Manager)#
If using NPM with Docker-network routing:
- Attach
smoll-urland NPM to the same Docker network. - In NPM Proxy Host:
Scheme:httpForward Hostname/IP:smoll-urlForward Port:<port inside container>(default4567, or your configuredportvalue)
- Enable WebSocket support in NPM.
- Use Cloudflare SSL mode
Full(orFull (strict)if certs are valid end-to-end).
Security Checklist#
- Change
passwordandapi_keyfrom defaults. - Keep
.envout of source control. - Run behind HTTPS (NPM/Caddy/Nginx/Traefik).
- Restrict server firewall to required ports only (
80/443for proxy, app internal only). - Back up SQLite DB file regularly.
Health and Troubleshooting#
- Check app logs:
docker compose logs -f smoll-url
- Confirm container network routing:
docker inspect smoll-url --format '{{json .NetworkSettings.Networks}}'
- Validate upstream from reverse-proxy container:
docker exec -it nginx-proxy-manager-app-1 sh -c "wget -S -O- http://smoll-url:<port>/"
- Common issue:
unable to open database file- Cause: container DB path points to non-writable location.
- Fix: set
DOCKER_DB_URL=/data/urls.dband mount/datavolume.
API Usage#
You can interact with smoll-url via the web UI or directly through the REST API.
Create a Short URL#
Endpoint: POST /api/new
Auth: Pass X-API-KEY header or a valid session cookie.
curl -X POST http://localhost:8080/api/new \
-H "X-API-KEY: your-secret-api-key" \
-H "Content-Type: application/json" \
-d '{
"longlink": "https://github.com/taf/smoll-url",
"shortlink": "repo",
"expiry_delay": 3600
}'
If you changed HOST_PORT in .env, replace 8080 with your host port.
Authentication#
- Login:
POST /api/loginwith the password in the request body. - Logout:
DELETE /api/logout.
Development#
The project structure is organized as follows:
cmd/smoll-url/: Entry point and CLI logic.internal/server/: HTTP server, API routes, and embedded frontend.internal/store/: SQLite storage layer usingmodernc.org/sqlite.internal/auth/: Argon2id password hashing and session management.internal/slug/: Random slug generation logic.
To modify the frontend, edit the files in internal/server/web/. The Go compiler will automatically bundle them into the binary on the next build.
License#
MIT