Skip to content
HarshsanketPublic

About

Lynky is an open-source URL cleaner and shortener designed to make shared links simpler, cleaner, and less intrusive.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

49 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lynky

Lynky is an open-source URL cleaner and shortener designed to make shared links simpler, cleaner, and less intrusive.

Overview

Lynky cleans known tracking parameters from a URL before it is shared, shortens the result, and redirects public short links to the cleaned destination. It provides a public web UI, an authenticated shortening API, and a dev-only sysadmin dashboard. The cleaning, storage, redirect, and limit behavior is implemented in a TypeScript/Express backend with a React frontend.

Requirements

  • Node.js 24+
  • npm 10+
  • MongoDB 7+
  • Upstash Redis credentials for production (development uses the in-memory cache)
  • Local mongod for development

Getting Started

Start MongoDB, then install and configure the backend:

cd backend
npm install
cp .env.example .env
npm run blocklists:fetch
npm run config:compile
npm run dev

Edit backend/.env before starting: set a real ENCRYPTION_KEY, MONGODB_URI, PUBLIC_URL, and (for production) FRONTEND_URL.

Install and start the frontend in a second terminal:

cd frontend
npm install
cp .env.example .env.local
npm run dev

Local endpoints:

  • Web UI: http://localhost:5173
  • API: http://localhost:5001/api/v1
  • Health/readiness: http://localhost:5001/health, http://localhost:5001/ready
  • Sysadmin: http://localhost:5173/sysadmin (set SYSADMIN_USERNAME and SYSADMIN_PASSWORD_HASH first)

Configuration

Backend values mirror backend/.env.example; frontend values mirror frontend/.env.example. All examples are placeholders.

Core

Variable Example Meaning
MONGODB_URI mongodb://127.0.0.1:27017/lynky_example MongoDB connection string (required)
PUBLIC_URL http://localhost:5001 Public base URL for generated short links; http(s), no query/fragment/credentials, subpath allowed
ENCRYPTION_KEY a9Pz/oGv9YZMsHZ7xkpchPwfrMWW7dr9eRdupIu8CBI= Destination encryption and HMAC key material; at least 32 characters
EXPIRATION_DAYS 3 Link lifetime in days
PORT 5001 Backend listen port
NODE_ENV development development, test, or production
FRONTEND_URL http://localhost:5173 Allowed frontend origin; required in production
MIN_CODE_LENGTH 4 Minimum generated short-code length
MAX_CODE_LENGTH 6 Maximum generated short-code length
MAX_CODE_ATTEMPTS 10 Short-code retry limit after collisions
MAX_URL_LENGTH 640 Maximum accepted input URL length
ALLOW_PRIVATE_SUFFIXES false Allow hosts on private public-suffix entries such as *.eu.org
BLOCKLIST_FAILURE_MODE open open continues creates with degraded readiness; closed rejects them
CONFIG_FULL_RUNTIME_VALIDATION false Development-only full artifact validation; ignored in production
UPSTASH_REDIS_REST_URL https://your-database.upstash.io Upstash REST URL; required in production, unused in development
UPSTASH_REDIS_REST_TOKEN your-token-here Upstash REST token; required in production, unused in development

Limits, caching, and clicks

Variable Example Meaning
GLOBAL_RATE_LIMIT_PER_HOUR 5000 Shared global request limit across all routes
LINK_RATE_LIMIT_PER_15_MIN 100 Per-IP create-route limit (dev/benchmark override; normal default is 100)
SERVER_STAGE_TIMINGS false Dev-only per-stage create timing headers; production rejects true
HOT_VISIT_THRESHOLD 4 Visits before a link is marked hot
HOT_WINDOW_SECONDS 300 Sliding hot-link window
HOT_CACHE_TTL_SECONDS 900 Maximum hot-link cache lifetime
CLICK_STORAGE_MODE hybrid hybrid, exact, or dynamic click accounting
CLICK_EXACT_BELOW_PER_HOUR 2000 Dynamic-mode threshold for write-through clicks
CLICK_FLUSH_EVERY_N 10 Pending click flush threshold
CLICK_FLUSH_INTERVAL_MS 60000 Pending click flush interval
CLICK_PENDING_TTL_SECONDS 86400 Pending click buffer TTL
API_KEY_MONTHLY_LIMIT 10000 Monthly links per API key
API_KEY_LENGTH 45 Generated API key length (16–512); changing it invalidates old keys

Sysadmin

The Mongo-session dashboard is available whenever both credentials below are set; there is no separate enable flag.

Variable Example Meaning
SYSADMIN_USERNAME example.admin Lowercase letters/dots, 1–15 characters
SYSADMIN_PASSWORD_HASH Hash generated by npm run admin:hash-password

Frontend

Variable Example Meaning
VITE_API_URL http://localhost:5001/api/v1 Backend API base including /api/v1
VITE_SITE_URL http://localhost:5173 Public site origin for canonical/Open Graph metadata
VITE_SHORT_URL http://localhost:5001 Short-link origin used in documentation examples
VITE_API_KEY_LENGTH 45 Must match backend API_KEY_LENGTH
VITE_MAX_URL_LENGTH 640 Must match backend MAX_URL_LENGTH

Keep the frontend length values aligned with the backend. Vite values are compiled at build time, so changing them requires a frontend rebuild.

How It Works

Link cleaning flow

A create request parses and validates the input, rejects Lynky's own links, then cleans the URL in a bounded pipeline:

  1. Parse the URL and reject unsupported schemes, credentials, private/special-use hosts, and unsafe syntax.
  2. Unwrap known redirect wrappers, up to the configured depth.
  3. Match the destination against compiled provider rules.
  4. Apply the allowlist before any removal; protected parameters always survive.
  5. Remove tracking parameters using global tracker groups, referral rules, raw-URL rules, and provider-specific rules.
  6. Rebuild the URL, re-validate the final destination, then hash, encrypt, and store the cleaned value.

The cleaned URL is what gets deduplicated and stored. The original input is not persisted.

Algorithms

  • Static sets are compiled into a minimal perfect hash (MPHF) with 64-bit verification and exact collision checks.
  • A sorted-hash fallback table handles entries the MPHF cannot place.
  • Provider matching uses a host-hint prefilter, host-suffix walks, and the original provider regex as the final authority.
  • Dedupe uses keyed HMAC-SHA-256 (urlHash); redirect lookup uses keyed HMAC-SHA-256 (codeHash).
  • Short codes are re-derived from urlHash + codeSalt; codeSalt makes repeat shorten requests stable across cache loss.
  • Destinations are stored as AES-256-GCM ciphertext; the authentication tag detects corruption.

Filters

Provider rules compile into exact Set, prefix-array, and regex-array classes. Compilation deduplicates and merges overlapping rules, warns about overlaps, and preserves provider order. The allowlist is evaluated before every removal path, so a protected parameter is never stripped by a tracker or referral rule.

Blocklists

Create-time screening uses HaGeZi wildcard lists (light, NSFW, gambling, URL-shortener), Phishing Army extended, and the URLhaus host file. Lists are compiled into artifacts during blocklists:fetch and config:compile. Screening checks blocked host suffixes and exact URL keys; own-link detection blocks Lynky's own origins. Redirects never re-run blocklist screening, so a list update cannot break an existing short link.

Shortening, redirects, and limits

A repeated destination is adopted through its urlHash rather than duplicated. Code collisions retry within MAX_CODE_ATTEMPTS; exhausted attempts return a clean 503. Upstash Redis caches hot links in production for at most 15 minutes (default), capped by the link's remaining lifetime; development uses the in-memory store. Cold and failed cache paths fall back to MongoDB. The global limit is 5,000 requests/hour by default, with a separate per-IP create limit. Click attempts, successes, and failures are buffered and rolled up into minute, hour, day, and month metrics; 5xx errors go to a bounded error log. Admin sessions use a 30-minute sliding idle timeout and an 8-hour absolute cap.

URL validation

Only public HTTP(S) destinations are accepted. Credentials, localhost/private/special-use networks, internal hostnames, obfuscated IP literals, control/bidi characters, backslash/raw-@ smuggling, and IDN/xn-- hosts are rejected on create and redirect. The frontend mirrors cheap checks for feedback, but the backend always re-derives the verdict.

Config engine

Human-editable rules and blocklists live under backend/config/. npm run config:validate checks structure, semantics, cross-file references, and blocklist counts. npm run config:compile writes deterministic artifacts under dist/config/compiled/ with source hashes and a manifest. Production loads only compiled artifacts and refuses to start when they are missing or invalid. Trackers, providers, allowlist, and policy fail closed; blocklist availability follows BLOCKLIST_FAILURE_MODE. /ready reports config, blocklist, database, and cache state. Logs are structured JSON with request IDs and redacted context; raw URLs, queries, headers, API keys, ciphertext, and secrets are never logged.

Deployment

The frontend deploys to Vercel. Set VITE_API_URL to the backend /api/v1 URL and VITE_SITE_URL/VITE_SHORT_URL to the public origins; frontend/vercel.json rewrites SPA deep links and includes the BotID challenge/proxy rewrites.

The backend runs on any Node host:

cd backend
npm run build
npm start

npm run build typechecks, copies config/ into dist/, and compiles the deterministic artifacts. npm start runs node dist/src/server.js. MongoDB is required. Upstash Redis is required in production for cache, rate limits, click buffering, and admin lockouts; development uses the in-memory store. Set NODE_ENV=production, MONGODB_URI, PUBLIC_URL, FRONTEND_URL, ENCRYPTION_KEY, UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, and any SYSADMIN_* values. Do not set SERVER_STAGE_TIMINGS or other benchmark overrides in production. The benchmark report endpoint and sysadmin section are dev-only.

Troubleshooting

  • 413 Link too large: the JSON body exceeded the limit derived from MAX_URL_LENGTH; shorten the URL or raise the limit.
  • 429 responses: the shared global or per-IP create limit was reached; wait for Retry-After or adjust the limits for local load tests only.
  • Database is unavailable / 503: check MONGODB_URI and whether MongoDB is accepting connections.
  • Blocklist degraded or closed: check the files and hashes declared in config/blocklists/sources.json; open continues with degraded readiness, closed rejects creates.
  • Sysadmin login lockout: three failures in 15 minutes lock the (IP, username) pair; wait for the window to expire or clear the in-memory or Upstash lockout state.
  • Upstash configuration missing in production: startup fails by design; set both UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.
  • Stale compiled artifacts: rerun npm run blocklists:fetch and npm run config:compile, or npm run build.

Tests & Scripts

Backend:

  • npm test — unit, integration, config, and failure-path tests via the Node test runner; set TEST_MONGODB_URI to run database tests.
  • node --import tsx --test tests/*.test.ts — direct test-runner invocation.
  • npm run build — typecheck, copy config, and compile artifacts.
  • npm run config:validate — validate config sources and blocklists.
  • npm run config:compile — emit deterministic compiled artifacts.
  • npm run blocklists:fetch — refresh operator-supplied blocklists.
  • npm run admin:hash-password -- '<password>' — generate SYSADMIN_PASSWORD_HASH.
  • npm run provision:keys — provision API keys.

Frontend:

  • npm test — frontend unit tests via the Node test runner.
  • npm run lint — ESLint.
  • npm run build — production Vite build.
  • npm run dev — Vite development server.
  • npm run preview — preview the production build.

Security

Lynky derives separate HMAC keys from ENCRYPTION_KEY for URL dedupe, short-code lookup, and code-salt recovery, so a leaked database is not an offline URL-confirmation oracle. Unique urlHash and codeHash indexes prevent duplicate or placeholder rows, and own-link detection rejects Lynky's own origins. Destinations use AES-256-GCM with authentication tags, so corrupted ciphertext is detected rather than redirected.

Admin sessions use opaque Mongo-backed IDs, a 30-minute sliding idle window, an 8-hour absolute cap, HttpOnly/SameSite=Strict cookies (Secure in production), and CSRF tokens on state changes. Login failures are rate-limited and do not create Mongo auth-failure documents. BotID protects high-value create and usage routes; global and per-IP limits bound traffic.

Production errors are masked: known public validation/policy messages pass through, while internal errors collapse to the generic copy. Logs redact URLs to origins and never include raw URLs, query strings, headers, API keys, ciphertext, or secrets. Config artifacts fail closed when corrupt; blocklist screening is create-time only, so redirects remain stable when lists change.

Languages Used

  • TypeScript for the backend and tooling
  • JavaScript + React for the frontend
  • Tooling: Node test runner, tsx, Vite, Tailwind, gsap
  • No Python in the runtime

License

Lynky is licensed under the GNU General Public License v3. See LICENSE for the complete license text.

About

Proudly made in Mumbai by Harsh Sanket.

Claude 5.5 by Anthropic, GLM 5.3 by Z.ai, and DeepSeek 4.1 were used during development.

Acknowledgments

  • OpenPhish — phishing intelligence (credited source)
  • HaGeZi — DNS blocklists
  • ClearURLs — tracking-parameter rules
  • Phishing Army — phishing domain list
  • URLhaus — malicious URL hostfile
  • Vercel — frontend hosting and deployment
  • Upstash — Redis cache
  • MongoDB — primary database

a Harsh Sanket production

About

Lynky is an open-source URL cleaner and shortener designed to make shared links simpler, cleaner, and less intrusive.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages