HTTP Routes
lnkify exposes several non-GraphQL HTTP endpoints served by the Express server.
GET /:slug — Redirect Handler
The primary redirect endpoint. When a visitor opens a shortlink URL, this handler:
- Looks up the slug — served from an in-memory slug→target cache when warm, otherwise from the database (the result is then cached)
- Validates that the target is an
http(s)://URL (else 400Invalid redirect target) - If tracking is enabled: records the visitor's IP, user agent, and increments the hit count (asynchronously, non-blocking — fired on both cache hits and misses)
- Issues a 302 Found redirect to the target URL
- If the slug is not found: returns 404 with
Target not found
Client IP Resolution
The client IP — used for both hit tracking and rate limiting — comes from a single trusted source: Express's req.ip. Behind a reverse proxy, set TRUST_PROXY=true so Express derives req.ip from the proxy's X-Forwarded-For and ignores spoofed values from untrusted hops. The handler no longer reads X-Forwarded-For/X-Real-IP directly. Stored IPs can be anonymized with IP_ANONYMIZE=true.
Rate Limiting
Redirects are rate-limited per IP at 600 req/min (configurable via RATELIMIT_REDIRECT_PER_MIN).
GET /healthz — Liveness
Unauthenticated liveness probe. Always returns 200 with {"status":"ok"} as long as the process is running — it touches no dependencies. Excluded from the redirect rate-limit bucket and the slug catch-all. Used by the Docker Compose server healthcheck.
GET /readyz — Readiness
Unauthenticated readiness probe. Runs a trivial SELECT 1 against Postgres and returns 200 {"status":"ok"} when the database is reachable, or 503 {"status":"error"} otherwise. Use this (not /healthz) to gate traffic on database availability.
GET /llms.txt — AI Discovery
Returns an AI agent discovery file describing available tools and endpoints.
- Content-Type:
text/markdown; charset=utf-8 - Cache-Control:
public, max-age=3600 - Lists the MCP endpoint, tool names with descriptions, and the GraphQL endpoint
- Links to docs for full reference
GET /llms-full.txt — Expanded AI Reference
Extended version of /llms.txt with additional details:
- Same content type and caching headers
- Full tool input schemas with field types and constraints
- Worked examples for each tool
GET /:slug/qr.:format — QR Code
Returns a QR code image for a shortlink. :format is svg (default) or png. Optional query params control size and error-correction level. Rate-limited per IP. Used by the dashboard and the MCP get_qr_code tool.
GET /bio/:slug — Bio Page
Renders a public link-in-bio page (server-rendered HTML) for the given bio slug. Rate-limited per IP. See the Bio Pages guide.
POST /webhooks/stripe — Stripe Webhook
Receiver for Stripe billing events. Mounted with a raw body parser so Stripe's signature can be verified against STRIPE_WEBHOOK_SECRET. Only relevant when billing is configured; ignore it for a default self-host.
GET /internal/tls-check — On-Demand TLS Probe
Internal endpoint Caddy calls (ask) before issuing an on-demand certificate for a custom domain. When CADDY_TLS_CHECK_SECRET is set, it requires a matching X-Tls-Check-Secret header and fails closed otherwise. Not for public use.
GET / — Dashboard Redirect
The apex / on the server issues a 302 redirect to ${APP_URL}/dash. The dashboard single-page app itself is not served by the Express server — it is a separate static (nginx) container reached at app.lnkify.io. See Architecture.
Route Resolution Order
For self-hosters, the Express server's request routing priority is:
| Priority | Path | Handler |
|---|---|---|
| 1 | POST /graphql | Apollo Server |
| 2 | POST/GET/DELETE /mcp | MCP server (when MCP_ENABLED) |
| 3 | /healthz, /readyz | Health probes |
| 4 | /llms.txt, /llms-full.txt | AI discovery handlers |
| 5 | POST /webhooks/stripe, GET /internal/tls-check | Billing & TLS internal handlers |
| 6 | /:slug/qr.:format, /bio/:slug | QR + bio page handlers |
| 7 | GET / | 302 → ${APP_URL}/dash |
| 8 | * (everything else) | Slug redirect handler |
Architecture Note
These routes are served by the Express server container. The dashboard SPA (app) and the docs site (docs) are separate static containers. Caddy acts as the reverse proxy, routing each hostname to the correct container. For example:
lnkify.io/graphql→ server container (GraphQL)lnkify.io/<slug>→ server container (redirect)app.lnkify.io/→ app container (dashboard SPA)mcp.lnkify.io/→ server container (MCP)docs.lnkify.io/→ docs container (VitePress static site)
See Self-Hosting for deployment details.