Skip to content
/ Lynx Public

Lynx is a URL shortener written in Rust with support for multiple storage backends (SQLite and PostgreSQL), access control, and separated management API and redirector for enhanced security. The management API integrates with Zero Trust services and OAuth, while the redirector has read-only database access.

Notifications You must be signed in to change notification settings

BTreeMap/Lynx

Repository files navigation

Lynx URL Shortener

A high-performance URL shortener written in Rust with dual-server architecture, multi-backend storage support, and enterprise authentication.

Features

  • 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)

Frontend

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.

Accessing the Frontend

The frontend is automatically available at the API server's root path:

  • Frontend UI: http://localhost:8080/ (default)
  • API endpoints: http://localhost:8080/api/...

Frontend Features

  • 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

Serving from Custom Directory

You can optionally serve frontend files from a custom directory instead of using the embedded version:

export FRONTEND_STATIC_DIR=/path/to/frontend/dist

This 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)

Standalone Frontend Archive

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-frontend

See the frontend README for development setup.

Architecture

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.

Quick Start

Docker (Recommended)

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:stable

For 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:latest

Available 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/gh

Pre-built Binaries

Download 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-amd64

Building from Source

Requires 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/lynx

Sample Deployments

Development (SQLite, No Auth)

Simplest 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:stable

Or 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
./lynx

Production with PostgreSQL and OAuth 2.0

Enterprise 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:stable

See OAuth Setup for detailed configuration.

Production with Cloudflare Zero Trust

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:stable

See Cloudflare Setup Guide for complete instructions.

Docker Compose with PostgreSQL

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:

Authentication

Lynx supports three authentication modes configured via the AUTH_MODE environment variable.

No Authentication (Development Only)

AUTH_MODE=none

All API endpoints are accessible without authentication. All users are automatically granted admin privileges. Not recommended for production.

OAuth 2.0

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=300

API clients must include a valid Bearer token:

curl -H "Authorization: Bearer <access-token>" \
  http://localhost:8080/api/urls

Cloudflare Zero Trust

Validates 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=86400

See 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

Configuration

All configuration is done via environment variables. See .env.example for a complete reference.

Core Settings

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

Performance Tuning

Variable Description Default
CACHE_MAX_ENTRIES Maximum entries in read cache 500000 (~100MB)

Frontend

Variable Description
FRONTEND_STATIC_DIR Optional: Serve frontend from custom directory instead of embedded version

For advanced configuration including analytics, see the full documentation.

Web Frontend

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.

API Reference

The API server exposes RESTful endpoints at /api/*. Authentication is required unless AUTH_MODE=none.

Quick Examples

# 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/reactivate

Redirect Server

The 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 codes

The 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 milliseconds
  • X-Lynx-Timing-Cache-Ms: Cache lookup time
  • X-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).

Admin Management

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.

Deployment with Reverse Proxy

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.

Development

Running Tests

# 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 100

See tests/README.md for comprehensive testing documentation.

Running with Logging

RUST_LOG=debug cargo run

Documentation

Core Documentation

Testing Documentation

Security Considerations

  1. Authentication: Use OAuth or Cloudflare Zero Trust in production; never expose AUTH_MODE=none publicly
  2. HTTPS: Always use HTTPS in production (configure via reverse proxy)
  3. Network Isolation: Run the API server on a private network when possible
  4. Rate Limiting: Implement rate limiting at the reverse proxy level
  5. Database Credentials: Use strong passwords and restrict network access
  6. Token Rotation: Rotate OAuth credentials and secrets regularly

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please submit a Pull Request.

About

Lynx is a URL shortener written in Rust with support for multiple storage backends (SQLite and PostgreSQL), access control, and separated management API and redirector for enhanced security. The management API integrates with Zero Trust services and OAuth, while the redirector has read-only database access.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors 2

  •  
  •