Running the server
cargo run --bin lonkd
lonkd serves the REST API and the web UI from the same port (the web UI
is embedded in the binary — see Customizing the UI to
swap it out or run it separately in development). Data is stored in a
single SQLite file.
Environment variables
| Variable | Meaning | Default |
|---|---|---|
LONK_CONFIG_DIR |
Directory used to resolve the default LONK_DB path. |
$XDG_CONFIG_HOME/lonk if set, else ~/.config/lonk |
LONK_DB |
Path to the SQLite database file. | <LONK_CONFIG_DIR>/lonk.db (the directory is created automatically when this default is used; if you set LONK_DB explicitly, its parent directory must already exist) |
LONK_WEB_DIR |
Serve the web UI from this directory instead of the build embedded in the binary. Any non-empty value switches to directory mode; unset (or empty) serves the embedded UI. Short-link routes take precedence over your files for some paths — see Route precedence. | unset (embedded UI) |
ROCKET_ADDRESS |
Address lonkd binds to. |
127.0.0.1 |
ROCKET_PORT |
Port lonkd binds to. |
8000 |
LONK_CONFIG_DIR and LONK_DB are independent: LONK_CONFIG_DIR only
matters for computing the default database path (and, for the CLI, its
config.toml); setting LONK_DB directly overrides that computation
entirely.
API reference
| Endpoint | Description |
|---|---|
POST /api/links |
Create a short link, optionally with custom response headers. |
POST /api/valid |
Validate a URL locally, no link is created. |
GET /<id> |
303 redirect to the original URL, carrying the link’s custom response headers. |
GET /<id>/qr |
SVG QR code that encodes the short link’s URL. |
GET /<id>/status |
Live dead-link check against the stored destination. |
All error responses share one shape: {"error": "<message>"}, with an
HTTP status appropriate to the failure (400 for validation errors, 404
for unknown links, 500 for server-side failures).
POST /api/links
Body: {"url": "<http(s) url>", "headers": [["Name", "value"], ...]} —
headers is optional and defaults to []; at most 16 pairs (see
Custom response headers for the validation rules).
Without headers:
curl -s -X POST http://127.0.0.1:8000/api/links \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/some/long/url"}'
{"id":"QnbppZy","url":"https://example.com/some/long/url","short_url":"/QnbppZy","qr_url":"/QnbppZy/qr","headers":[]}
With headers:
curl -s -X POST http://127.0.0.1:8000/api/links \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/a","headers":[["Set-Cookie","seen=1; Path=/"],["Cache-Control","no-store"]]}'
{"id":"tm5kE5M","url":"https://example.com/a","short_url":"/tm5kE5M","qr_url":"/tm5kE5M/qr","headers":[["Set-Cookie","seen=1; Path=/"],["Cache-Control","no-store"]]}
The response is 201 Created. Rejected input comes back 400, e.g. too
many header pairs:
{"error":"too many headers (max 16)"}
or an invalid URL:
{"error":"invalid url: relative URL without a base"}
POST /api/valid
Body: {"url": "<string>"}. Always 200; validates locally, no link is
created and no network request is made.
curl -s -X POST http://127.0.0.1:8000/api/valid \
-H 'Content-Type: application/json' -d '{"url":"https://example.com/a"}'
# {"valid":true}
curl -s -X POST http://127.0.0.1:8000/api/valid \
-H 'Content-Type: application/json' -d '{"url":"not a url"}'
# {"valid":false,"error":"invalid url: relative URL without a base"}
GET /<id>
303 See Other to the stored URL. Any custom response headers saved on
the link are attached to this response (see
Custom response headers):
curl -i -s http://127.0.0.1:8000/QnbppZy
HTTP/1.1 303 See Other
location: https://example.com/some/long/url
content-length: 0
curl -i -s http://127.0.0.1:8000/tm5kE5M
HTTP/1.1 303 See Other
location: https://example.com/a
set-cookie: seen=1; Path=/
cache-control: no-store
content-length: 0
Unknown ids return 404: {"error":"no such link"}.
GET /<id>/qr
Returns 200 with Content-Type: image/svg+xml and an SVG QR code that
encodes the full short URL (scheme + host from the request, or
X-Forwarded-Proto/Host behind a proxy):
curl -s http://127.0.0.1:8000/QnbppZy/qr | head -c 120
# <?xml version="1.0" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" ...
Unknown ids return 404: {"error":"no such link"}.
GET /<id>/status
Makes lonkd probe the stored destination live (see the note below) and
reports whether it’s reachable. Always 200 for a known id; 404 for an
unknown one.
Alive:
curl -s http://127.0.0.1:8000/jtrstNQ/status
{"id":"jtrstNQ","url":"https://example.com/","alive":true,"http_status":200}
Dead, with an HTTP status from the destination:
{"id":"LZiPehB","url":"https://example.com/nonexistent-lonk-test-path","alive":false,"http_status":404}
Dead/unknown, transport failure (DNS, connection refused, timeout — no HTTP status to report):
{"id":"fzpAKv9","url":"https://this-domain-should-not-exist-lonk-test.invalid/","alive":false,"error":"https://this-domain-should-not-exist-lonk-test.invalid/: Dns Failed: resolve dns name 'this-domain-should-not-exist-lonk-test.invalid:443': failed to lookup address information: nodename nor servname provided, or not known"}
(the exact transport-error text is platform-dependent; the shape — alive:
false with error set and no http_status — is what’s guaranteed.)
Unknown id:
curl -s -i http://127.0.0.1:8000/doesnotexist/status
HTTP/1.1 404 Not Found
content-type: application/json
{"error":"no such link"}
The probe is HEAD, falling back to a one-shot GET on 405/501, up to 5
redirects, 5s timeout; alive is true when the final response is 2xx.
Note: anyone who can reach your lonkd can use /status to make it
issue requests to any URL that’s ever been shortened and observe whether
it answered — on a self-hosted instance this is the same trust level as
being able to create links, but it’s worth knowing before binding
ROCKET_ADDRESS beyond localhost (see Deployment).