Lynky is an open-source URL cleaner and shortener designed to make shared links simpler, cleaner, and less intrusive.
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.
- Node.js 24+
- npm 10+
- MongoDB 7+
- Upstash Redis credentials for production (development uses the in-memory cache)
- Local
mongodfor development
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 devEdit 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 devLocal 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(setSYSADMIN_USERNAMEandSYSADMIN_PASSWORD_HASHfirst)
Backend values mirror backend/.env.example; frontend values mirror frontend/.env.example. All examples are placeholders.
| 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 |
| 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 |
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 |
| 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.
A create request parses and validates the input, rejects Lynky's own links, then cleans the URL in a bounded pipeline:
- Parse the URL and reject unsupported schemes, credentials, private/special-use hosts, and unsafe syntax.
- Unwrap known redirect wrappers, up to the configured depth.
- Match the destination against compiled provider rules.
- Apply the allowlist before any removal; protected parameters always survive.
- Remove tracking parameters using global tracker groups, referral rules, raw-URL rules, and provider-specific rules.
- 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.
- 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;codeSaltmakes repeat shorten requests stable across cache loss. - Destinations are stored as AES-256-GCM ciphertext; the authentication tag detects corruption.
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.
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.
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.
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.
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.
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 startnpm 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.
- 413
Link too large: the JSON body exceeded the limit derived fromMAX_URL_LENGTH; shorten the URL or raise the limit. - 429 responses: the shared global or per-IP create limit was reached; wait for
Retry-Afteror adjust the limits for local load tests only. - Database is unavailable / 503: check
MONGODB_URIand whether MongoDB is accepting connections. - Blocklist degraded or closed: check the files and hashes declared in
config/blocklists/sources.json;opencontinues with degraded readiness,closedrejects 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_URLandUPSTASH_REDIS_REST_TOKEN. - Stale compiled artifacts: rerun
npm run blocklists:fetchandnpm run config:compile, ornpm run build.
Backend:
npm test— unit, integration, config, and failure-path tests via the Node test runner; setTEST_MONGODB_URIto 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>'— generateSYSADMIN_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.
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.
- TypeScript for the backend and tooling
- JavaScript + React for the frontend
- Tooling: Node test runner, tsx, Vite, Tailwind, gsap
- No Python in the runtime
Lynky is licensed under the GNU General Public License v3. See LICENSE for the complete license text.
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.
- 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