A high-performance URL shortener written in Rust with dual-server architecture, multi-backend storage support, and enterprise authentication.
- URL Shortening: Create short codes for long URLs with optional custom codes
- Extensible Storage: SQLite and PostgreSQL backends with automatic schema initialization
- Access Control: OAuth 2.0 and Cloudflare Zero Trust authentication with pass-through mode
- Dual Server Architecture: Separate API server (management) and redirect server (public-facing)
- Analytics: Click tracking with optional GeoIP-based visitor analytics (Analytics Guide)
- Immutable URLs: URLs can be deactivated/reactivated but not deleted or modified
- Delete Protection: Database-level triggers prevent accidental deletion (Delete Protection)
- Multi-User Support: User-specific link management with admin roles
- Web Frontend: React-based dashboard bundled into the binary
- High Performance: In-memory caching and write buffering (Performance Optimizations)
Lynx includes a modern React-based web frontend for managing short URLs. The frontend is bundled into the binary at compile time and served directly from the API server.
The frontend is automatically available at the API server's root path:
- Frontend UI:
http://localhost:8080/(default) - API endpoints:
http://localhost:8080/api/...
- OAuth 2.0 Bearer token authentication
- Creating short URLs with optional custom codes
- Viewing URL statistics (clicks, status, creation date)
- User-specific URL filtering (users see only their own links)
- Admin panel for managing all users' links
- Admin-only deactivation/reactivation of URLs
You can optionally serve frontend files from a custom directory instead of using the embedded version:
export FRONTEND_STATIC_DIR=/path/to/frontend/distThis is useful for:
- Serving a custom frontend without recompiling
- Static hosting scenarios where you extract the frontend separately
- Development with hot-reload (point to your dev server's output)
A separate frontend-static.tar.gz archive is available in releases and CI artifacts. Extract and serve with any static file server:
# Extract the frontend
tar -xzf frontend-static.tar.gz -C /var/www/lynx-frontend
# Serve with nginx, Apache, or any static file server
# Point FRONTEND_STATIC_DIR to the extracted directory
export FRONTEND_STATIC_DIR=/var/www/lynx-frontendSee the frontend README for development setup.
Lynx runs two separate HTTP servers:
API Server (default: port 8080)
- Serves the bundled React frontend at
/ - API endpoints at
/api/... - Optional authentication (OAuth 2.0, Cloudflare Zero Trust, or disabled)
- Create URLs with auto-generated or custom codes
- Deactivate/reactivate URLs
- List and search capabilities
Redirect Server (default: port 3000)
- Public-facing URL redirects
- No authentication required
- Fast redirects with click tracking
- Handles deactivated links with appropriate HTTP status codes
This separation allows you to expose the redirect server publicly while keeping the API server internal, apply different security policies, and use separate domains via reverse proxy.
For production deployments, use the stable release tag:
# Pull the latest stable release
docker pull ghcr.io/btreemap/lynx:stable
# Run with SQLite (simplest setup)
docker run -d \
-p 8080:8080 \
-p 3000:3000 \
-v $(pwd)/data:/data \
-e DATABASE_BACKEND=sqlite \
-e DATABASE_URL=sqlite:///data/lynx.db \
-e AUTH_MODE=none \
ghcr.io/btreemap/lynx:stableFor testing unreleased features, use the latest development tag:
# Pull the latest main branch build (updated on every commit, may be unstable)
docker pull ghcr.io/btreemap/lynx:latestAvailable Docker tags:
:stable- Latest stable release (recommended for production):latest- Latest main branch build (unstable, for testing):v1.0.0- Specific version tag
Access the web UI at http://localhost:8080 and test a redirect:
# Create a short URL (no auth required with AUTH_MODE=none)
curl -X POST http://localhost:8080/api/urls \
-H "Content-Type: application/json" \
-d '{"url": "https://github.com/BTreeMap/Lynx", "custom_code": "gh"}'
# Access the shortened URL
curl -L http://localhost:3000/ghDownload from GitHub Releases for Linux, macOS, or Windows:
# Example: Linux
wget https://github.com/BTreeMap/Lynx/releases/download/v1.0.0/lynx-linux-amd64
chmod +x lynx-linux-amd64
./lynx-linux-amd64Requires Rust 1.70+ and Node.js 20+ for frontend compilation:
git clone https://github.com/BTreeMap/Lynx.git
cd Lynx
cargo build --release
./target/release/lynxSimplest setup for local development:
docker run -d \
-p 8080:8080 \
-p 3000:3000 \
-e DATABASE_BACKEND=sqlite \
-e DATABASE_URL=sqlite:///tmp/lynx.db \
-e AUTH_MODE=none \
ghcr.io/btreemap/lynx:stableOr with environment file:
# .env
DATABASE_BACKEND=sqlite
DATABASE_URL=sqlite://./lynx.db
AUTH_MODE=none
API_HOST=127.0.0.1
API_PORT=8080
REDIRECT_HOST=127.0.0.1
REDIRECT_PORT=3000./lynxEnterprise deployment with OAuth authentication:
docker run -d \
-p 8080:8080 \
-p 3000:3000 \
-e DATABASE_BACKEND=postgres \
-e DATABASE_URL=postgresql://user:password@postgres:5432/lynx \
-e AUTH_MODE=oauth \
-e OAUTH_ISSUER_URL=https://auth.example.com/realms/lynx \
-e OAUTH_AUDIENCE=lynx-api \
-e API_HOST=0.0.0.0 \
-e REDIRECT_HOST=0.0.0.0 \
ghcr.io/btreemap/lynx:stableSee OAuth Setup for detailed configuration.
Use Cloudflare Access for authentication:
docker run -d \
-p 8080:8080 \
-p 3000:3000 \
-e DATABASE_BACKEND=postgres \
-e DATABASE_URL=postgresql://user:password@postgres:5432/lynx \
-e AUTH_MODE=cloudflare \
-e CLOUDFLARE_TEAM_DOMAIN=https://your-team.cloudflareaccess.com \
-e CLOUDFLARE_AUDIENCE=your-aud-tag \
-e API_HOST=0.0.0.0 \
-e REDIRECT_HOST=0.0.0.0 \
ghcr.io/btreemap/lynx:stableSee Cloudflare Setup Guide for complete instructions.
Complete stack with PostgreSQL:
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: lynx
POSTGRES_PASSWORD: secure_password
POSTGRES_DB: lynx
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lynx"]
interval: 10s
timeout: 5s
retries: 5
lynx:
image: ghcr.io/btreemap/lynx:stable
ports:
- "8080:8080"
- "3000:3000"
environment:
DATABASE_BACKEND: postgres
DATABASE_URL: postgresql://lynx:secure_password@postgres:5432/lynx
AUTH_MODE: oauth
OAUTH_ISSUER_URL: https://auth.example.com
OAUTH_AUDIENCE: lynx-api
API_HOST: 0.0.0.0
REDIRECT_HOST: 0.0.0.0
depends_on:
postgres:
condition: service_healthy
volumes:
postgres-data:Lynx supports three authentication modes configured via the AUTH_MODE environment variable.
AUTH_MODE=noneAll API endpoints are accessible without authentication. All users are automatically granted admin privileges. Not recommended for production.
Validates JWT Bearer tokens from any OpenID Connect provider (Keycloak, Auth0, Okta, etc.):
AUTH_MODE=oauth
OAUTH_ISSUER_URL=https://auth.example.com/realms/lynx
OAUTH_AUDIENCE=lynx-api
# Optional: OAUTH_JWKS_URL (if not using OIDC discovery)
# Optional: OAUTH_JWKS_CACHE_SECS=300API clients must include a valid Bearer token:
curl -H "Authorization: Bearer <access-token>" \
http://localhost:8080/api/urlsValidates Cloudflare Access JWT tokens when deployed behind Cloudflare Access:
AUTH_MODE=cloudflare
CLOUDFLARE_TEAM_DOMAIN=https://your-team.cloudflareaccess.com
CLOUDFLARE_AUDIENCE=your-aud-tag
# Optional: CLOUDFLARE_CERTS_CACHE_SECS=86400See the Cloudflare Setup Guide for complete configuration including:
- Creating a Cloudflare Access application
- Obtaining your team domain and audience tag
- Configuring identity providers
- Setting up admin users
All configuration is done via environment variables. See .env.example for a complete reference.
| Variable | Description | Default |
|---|---|---|
DATABASE_BACKEND |
Storage backend: sqlite or postgres |
sqlite |
DATABASE_URL |
Database connection string | sqlite://./lynx.db |
DATABASE_MAX_CONNECTIONS |
Connection pool size | 30 |
API_HOST |
API server bind address | 127.0.0.1 |
API_PORT |
API server port | 8080 |
REDIRECT_HOST |
Redirect server bind address | 127.0.0.1 |
REDIRECT_PORT |
Redirect server port | 3000 |
AUTH_MODE |
Authentication mode: none, oauth, or cloudflare |
none |
| Variable | Description | Default |
|---|---|---|
CACHE_MAX_ENTRIES |
Maximum entries in read cache | 500000 (~100MB) |
| Variable | Description |
|---|---|
FRONTEND_STATIC_DIR |
Optional: Serve frontend from custom directory instead of embedded version |
For advanced configuration including analytics, see the full documentation.
The React-based web frontend is bundled into the binary and served at the root path (/).
Features:
- Create short URLs with optional custom codes
- View statistics (clicks, status, creation date)
- User-specific URL filtering
- Admin panel for managing all links
- Deactivate/reactivate URLs
Access at http://localhost:8080/ (or your configured API server address).
For custom frontend deployment or development, see the Frontend README.
The API server exposes RESTful endpoints at /api/*. Authentication is required unless AUTH_MODE=none.
# Health check
GET /api/health
# Create short URL
POST /api/urls
{
"url": "https://example.com/very/long/url",
"custom_code": "mycode" // optional
}
# Get URL details
GET /api/urls/:code
# List URLs (paginated)
GET /api/urls?limit=50&offset=0
# Deactivate URL
PUT /api/urls/:code/deactivate
# Reactivate URL
PUT /api/urls/:code/reactivateThe redirect server handles public-facing redirects:
# Redirect to original URL
GET /:code
→ 301 Redirect to original URL
# Returns 410 Gone for deactivated URLs
# Returns 404 Not Found for non-existent codesThe redirect endpoint includes performance tracing headers:
X-Lynx-Cache-Hit: Whether served from cache (true/false)X-Lynx-Timing-Total-Ms: Total request time in millisecondsX-Lynx-Timing-Cache-Ms: Cache lookup timeX-Lynx-Timing-Db-Ms: Database lookup time (0 if cache hit)
For complete API documentation, see the API section in the old README or explore the OpenAPI schema (if available).
Promote users to admin role using the CLI:
# Promote user to admin
./lynx admin promote <user-id> <auth-method>
# Example for Cloudflare
./lynx admin promote "google-oauth2|123456" cloudflare
# List admins
./lynx admin list
# Demote admin
./lynx admin demote <user-id> <auth-method>Admin status from OAuth/Cloudflare JWT claims takes precedence over manual promotion.
Example Nginx configuration:
# API server (internal or with additional auth)
server {
listen 443 ssl;
server_name api.example.com;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# Redirect server (public)
server {
listen 443 ssl;
server_name short.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}For Caddy, Apache, or advanced configurations, see the deployment documentation.
# Unit and integration tests
cargo test
# Bash integration tests (requires running service)
bash tests/integration_test.sh http://localhost:8080 http://localhost:3000
# Concurrent load tests
bash tests/concurrent_test.sh http://localhost:8080 http://localhost:3000 100See tests/README.md for comprehensive testing documentation.
RUST_LOG=debug cargo run- Performance Optimizations - Caching strategies and actor pattern
- Performance Benchmarks - Benchmarking guide and methodology
- Analytics Guide - GeoIP-based visitor analytics
- Delete Protection - Database-level delete prevention
- Cloudflare Setup Guide - Cloudflare Zero Trust configuration
- Tests Overview - Integration and benchmark test documentation
- Frontend README - Frontend development and deployment
- Authentication: Use OAuth or Cloudflare Zero Trust in production; never expose
AUTH_MODE=nonepublicly - HTTPS: Always use HTTPS in production (configure via reverse proxy)
- Network Isolation: Run the API server on a private network when possible
- Rate Limiting: Implement rate limiting at the reverse proxy level
- Database Credentials: Use strong passwords and restrict network access
- Token Rotation: Rotate OAuth credentials and secrets regularly
MIT License - see LICENSE file for details.
Contributions are welcome! Please submit a Pull Request.