Skip to content

paoloronco/Lynx

Repository files navigation

Lynx

Your personal links hub

Version

Lynx is an open-source, self-hosted link page manager: public profile page, admin panel, SQLite storage, theme editor, analytics, and secure admin access in one small app.

📌 Contents

🎬 Demo

Watch the demo

Watch the demo

✨ Features

  • 🎛 Admin panel: edit profile, links, theme, password, and reset options from one dashboard.
  • 👤 Public profile: name, bio, avatar, social links, page title, meta description, favicon, and footer text.
  • 🔗 Flexible cards: classic links, text cards, bulleted/grouped content, separators, icons, emojis, and images.
  • 🎨 Theme control: colors, gradients, fonts, spacing, radius, blur, glow, button styles, link styles, and custom CSS.
  • 👁 Live preview: check the public page while editing.
  • 📊 Analytics: click tracking with an admin chart.
  • 🗓 Scheduling: show or hide links by date range.
  • 🙈 Visibility toggles: hide links without deleting them.
  • 📱 Mobile-friendly editing: responsive UI and touch drag-and-drop ordering.
  • 📦 Import/export: backup and restore links and themes as JSON.
  • 🗄 Standalone storage: SQLite by default, no Firebase or Supabase required.
  • 🔒 Optional HTTPS: enable a self-signed HTTPS listener with ENABLE_HTTPS=true.

🔐 Security

  • Passwords are hashed with bcryptjs using 12 salt rounds.
  • Sessions use signed JWTs with a 12-hour expiry.
  • The frontend stores JWTs encrypted in localStorage with AES-GCM.
  • SQLite access uses parameterized queries.
  • API, auth, login, reset, and SPA routes are rate-limited.
  • Docker startup requires JWT_SECRET to avoid unstable sessions after restart.
  • Optional RESET_TOKEN supports token-protected recovery if you are locked out.

🛠 Tech Stack

🚀 Quick Start

Pick the path that matches what you want to do.

⚡ Run Lynx locally

Use this if you want to try Lynx on your machine with a production-style flow: the React app is built first, then the Express server serves both the frontend and the API.

Requirements

  • Node.js ^20.19.0 or >=22.12.0
  • npm
  • Git

1. Clone the repository

git clone https://github.com/paoloronco/Lynx.git
cd Lynx/LYNX

2. Install dependencies

npm ci
npm run install:server

3. Build and start

npm run start

npm run start runs the frontend build and then starts the backend server.

Open:

On the first admin visit, Lynx asks you to create the admin password. The username is always admin.

Local data is stored in LYNX/server/lynx.db unless you set DATA_DIR.

🧑‍💻 Development mode

Use this when you are editing the code and want frontend hot reload.

Development mode uses two running processes:

The Vite server gives you hot reload for React and proxies /api requests to the Express server.

1. Install dependencies once

cd Lynx/LYNX
npm ci
npm run install:server

2. Start the backend in the first terminal

cd Lynx/LYNX
npm run server:dev

3. Start the frontend in the second terminal

cd Lynx/LYNX
npm run dev

Keep both terminals open while developing.

Open:

Stop either process with Ctrl+C.

🐳 Quick Docker run

Use this if you want the fastest container-based setup.

From the repository root:

docker compose up -d

The included docker-compose.yml uses:

  • image: paueron/lynx:latest
  • port: 8080
  • volume: ./lynx-data:/app/data

Before exposing the app, replace the sample JWT_SECRET in docker-compose.yml.

Open:

⚙️ Configuration

Environment variables
Variable Default Notes
JWT_SECRET random outside Docker Required in Docker. Use a long random value in production.
NODE_ENV unset Set to production for production deployments.
PORT 3001 local, 8080 Docker HTTP server port.
DATA_DIR LYNX/server local, /app/data Docker Stores lynx.db and persistent data.
ENABLE_HTTPS false Set to true or 1 for self-signed HTTPS.
SSL_PORT 8443 HTTPS port.
FRONTEND_URL same-origin mode Optional dev CORS/CSP origin, e.g. http://localhost:8080.
BASE_PATH unset Optional additional mount path, e.g. /lynx. When set, the same instance works at both / and /lynx.
PUBLIC_BASE_PATH unset Backward-compatible alias for BASE_PATH.
VITE_BASE_PATH unset Optional dev-server equivalent of BASE_PATH when using npm run dev; production uses runtime BASE_PATH.
PUBLIC_SITE_URL request host Optional canonical public URL, e.g. https://links.example.com. Set this behind proxies or platforms where request host headers are not stable.
PUBLIC_SITE_NAME Lynx Site name used in generated Open Graph, Twitter Card, and Schema.org metadata.
SEO_INDEXING true Set to false, 0, no, or off to emit noindex metadata and a blocking robots.txt for private/staging deployments.
RESET_TOKEN unset Enables token-protected reset endpoints. Use at least 32 characters.
VITE_ENABLE_USERCENTRICS_PRIVACY_PAGE true Build-time flag for the optional public /privacy Usercentrics embed. Set to false to disable.
VITE_USERCENTRICS_PRIVACY_POLICY_ID fd1ffcdf-b560-4ea0-ba72-da943d39d953 Build-time Usercentrics privacy policy ID used by /privacy.
VITE_USERCENTRICS_PRIVACY_POLICY_LANGUAGE en Build-time language passed to the Usercentrics privacy policy script.
VITE_DEFAULT_PRIVACY_POLICY_URL /privacy Build-time public Privacy Policy URL. Set to an empty string to rely only on Admin > Profile > Legal links.
💾 Data persistence
  • Local: data is stored in LYNX/server/lynx.db unless DATA_DIR is set.
  • Docker: mount /app/data so the database and uploads survive container updates.

🐳 Docker

Docker Compose

From the repository root:

docker compose up -d

The included docker-compose.yml uses:

  • image: paueron/lynx:latest
  • port: 8080
  • volume: ./lynx-data:/app/data

Before exposing the app, replace the sample JWT_SECRET in docker-compose.yml.

Docker CLI
docker pull paueron/lynx:latest

docker run -d --name lynx \
  -p 8080:8080 \
  -e NODE_ENV=production \
  -e PORT=8080 \
  -e JWT_SECRET="replace-with-a-long-random-secret" \
  -v lynx_data:/app/data \
  paueron/lynx:latest

Open:

Optional HTTPS
docker run -d --name lynx \
  -p 8080:8080 \
  -p 8443:8443 \
  -e NODE_ENV=production \
  -e PORT=8080 \
  -e JWT_SECRET="replace-with-a-long-random-secret" \
  -e ENABLE_HTTPS=true \
  -v lynx_data:/app/data \
  paueron/lynx:latest

Then open https://localhost:8443. The browser will warn because the certificate is self-signed.

☁️ Railway

Deploy on Railway

Railway can deploy the repository using the root Dockerfile.

  1. Create a new Railway project from the GitHub repository.
  2. Let Railway use the Dockerfile in the repository root.
  3. Add:
NODE_ENV=production
PORT=8080
JWT_SECRET=replace-with-a-long-random-secret
  1. Deploy the service.
  2. Add a public domain from the Railway service settings.

Railway already provides HTTPS at the edge, so ENABLE_HTTPS is normally not needed there.

🌍 Other hosting options

Lynx can run anywhere that supports a Node app or a Docker container: Render, Fly.io, DigitalOcean App Platform, Google Cloud Run, Heroku Container Runtime, Azure App Service, AWS Elastic Beanstalk, Koyeb, Northflank, CapRover, Dokku, and Coolify.

For any container deployment, persist /app/data and set JWT_SECRET.

📝 Changelog

v4.1.8

Legal policy embed fix

  • Allows the current Usercentrics embed domains in the production CSP.
  • Keeps embedded legal policy scripts executable and ordered when rendered on /privacy and /cookies.
  • Adds a regression test for legal policy provider CSP sources.
v4.1.7

CMP and release workflow fix

  • Executes custom external CMP scripts reliably, including pasted iubenda widget snippets.
  • Publishes Docker images only from release tags to avoid duplicate Docker builds on main and v*.
  • Keeps latest, semantic version, and short SHA Docker tags on release builds.
v4.1.6

Demo legal policy preset

  • Adds a server-side demo preset for Privacy Policy, Cookie Policy, and external CMP.
  • Serves the demo legal pages from the requested iubenda embeds when DEMO_MODE=true.
  • Keeps public footer links on /privacy and /cookies in demo mode.
  • Ensures iubenda embedded policy scripts load reliably in SPA-rendered legal pages.
v4.1.5

Privacy hotfix

  • Stops demo-mode write protection from being shown as an expired admin session.
  • Ensures embedded legal policy scripts execute on /privacy and /cookies.
  • Infers hosted legal pages from existing /privacy and /cookies profile URLs for upgraded installs.
v4.1.4

Privacy and CMP configuration

  • Separates legal pages from consent management in the Privacy tab.
  • Adds provider-agnostic legal policy sources: external link, hosted text, and embedded code.
  • Replaces provider-specific CMP fields with a single external script flow.
  • Ensures /privacy and /cookies always render the latest selected source without stale provider fallback.
  • Keeps form edits on screen when the admin session expires during save.
v4.1.3

Consent mode

  • Ensures Google Consent Mode v2 defaults are set before analytics scripts load.
  • Defaults ad storage, analytics storage, user data, and personalization to denied when consent is enabled.
  • Avoids duplicate default consent blocks when an advanced provider already supplies one.
v4.1.2

Legal policies

  • Redesigned the Privacy tab legal policy setup with a guided, non-technical flow.
  • Added a footer visibility toggle, configured/missing status, and preview links for /privacy and /cookies.
  • Added a public /cookies placeholder page.
v4.1.1

Privacy and legal UX

  • Moved Privacy Policy and Cookie Policy editing from Profile to Privacy.
  • Kept the existing profile-backed fields as the single persistence source.
  • Forced the public /privacy page to render in a readable light layout.
v4.1.0

Legal links

  • Made Admin > Profile > Legal links the single editable source for Privacy Policy and Cookie Policy URLs.
  • Shows configured legal links in the public footer and hides them cleanly when empty.
  • Makes the Privacy & Cookies tab read-only for policy URLs, with an Edit in Profile shortcut.
  • Ensures the native cookie banner derives policy URLs from the profile instead of storing duplicate consent-config URLs.
v4.0.0

Admin experience

  • Redesigned the Admin panel with a clearer dashboard layout, status metrics, sticky centered navigation, and a lighter operational workspace.
  • Improved the Links editor with a clearer toolbar, content creation cards, save state visibility, and a more helpful empty state.
  • Added animated profile checklist guidance and save confirmation feedback for theme changes.
  • Kept the public page preview isolated from the Admin styling so it continues to reflect the saved public theme.

Loading and compatibility

  • Added a single public-page payload endpoint to load profile, links, and theme together and avoid flashes of default content.
  • Preserved compatibility with existing SQLite databases through additive migrations only.
v3.8.0

Integrations

  • Added Google Analytics 4 integration in the Admin panel (new Integrations tab).
  • The GA4 Measurement ID (G-XXXXXXXXXX) is stored in the database and injected as a gtag.js script on the public page only — the admin panel is never tracked.
  • Content Security Policy updated to allow googletagmanager.com and google-analytics.com script and connect sources.
  • Measurement ID is validated client-side before saving (format G-XXXXXXXXXX).
v3.7.0

Critical fixes

  • Fixed production blank page behavior caused by CORS/CSP headers blocking API calls in production containers.
  • Fixed stale frontend assets in Docker builds by cleaning dist before building.
  • Fixed legacy database migration handling.
  • Fixed missing fs import in database.js.

Production stability

  • Improved production CORS handling for same-origin and reverse-proxy deployments.
  • Refined Content Security Policy settings.
  • Added static asset serving logs for deployment troubleshooting.
  • Improved database migration validation and error handling.
v3.6.0
  • Added live preview inside the admin panel.
  • Added a View Public Page action from the admin header.
  • Added link visibility toggles.
  • Added mobile drag-and-drop ordering.
  • Removed sensitive authentication logs.
  • Removed unused Supabase and Firebase code.
  • Fixed duplicate database migration logic.
  • Removed debug logging from PublicLinkCard.
v3.5.1
  • Updated vulnerable frontend, backend, and Docker dependencies.
  • Resolved Dependabot alerts and Docker image CVEs reported at the time of release.
  • Optimized Docker build time by avoiding source builds where precompiled binaries are available.
v3.5.0
  • Added editable profile fields with line-break support in the bio.
  • Added social link controls and profile picture display controls.
  • Added text cards and bulleted lists.
  • Added JSON import/export for links and themes.
  • Added theme controls for page styling, typography, title, meta description, and footer text.
  • Added Docker startup validation for JWT_SECRET.
  • Added optional self-signed HTTPS support with ENABLE_HTTPS=true.

SEO and indexing setup

Lynx generates SEO metadata from each deployment's saved profile, so forks and self-hosted instances do not need to edit source files for basic discoverability.

Optional base path

Set BASE_PATH when the same Lynx instance should also be available below a subpath:

BASE_PATH=/lynx

With that setting, Lynx serves the same app from both / and /lynx:

  • Public page: / and /lynx
  • Admin: /admin and /lynx/admin
  • API: /api/... and /lynx/api/...
  • Static assets/uploads: /assets/..., /uploads/..., /lynx/assets/..., and /lynx/uploads/...

The server injects the configured base path into the frontend at runtime, so production Docker/Node deployments only need BASE_PATH. When running the Vite dev server directly, also set VITE_BASE_PATH=/lynx so the dev frontend can choose the same React Router basename.

What Lynx does by default

  • Serves profile-specific <title>, meta description, canonical URL, robots meta, Open Graph tags, Twitter Cards, and Schema.org JSON-LD from the Express server.
  • Generates /robots.txt dynamically and points crawlers to /sitemap.xml.
  • Generates /sitemap.xml with the canonical home page and local legal pages when /privacy or /cookies are configured as profile policy URLs.
  • Marks /admin, /api/*, /health, and unknown SPA paths as noindex to avoid duplicate or private pages in search results.
  • Adds a no-JavaScript fallback for the public profile links so crawlers can discover the main outbound links before the React app hydrates.

Recommended production configuration

PUBLIC_SITE_URL=https://links.example.com
PUBLIC_SITE_NAME="Your Name or Brand"
SEO_INDEXING=true

Use the Admin panel to set the profile name, bio, page title, meta description, avatar, social links, and legal policy URLs. Lynx reuses those values for search snippets, social previews, and structured data.

Staging, private, and preview deployments

For staging or private forks, disable indexing without changing code:

SEO_INDEXING=false

This makes robots.txt disallow crawling and makes served SPA pages emit noindex, nofollow, noarchive.

Canonical URLs and forks

If PUBLIC_SITE_URL is not set, Lynx derives canonical URLs from the incoming request host and protocol, including common reverse proxy headers. Set PUBLIC_SITE_URL when the app is behind a proxy, tunnel, CDN, or platform that may send internal hostnames.

Contributor SEO checklist

  • Public routes must have one canonical URL and should not create duplicate indexable paths.
  • New private or admin routes must send X-Robots-Tag: noindex, nofollow, noarchive.
  • New public pages should be added to the sitemap only when they have durable public content.
  • Public links should be real <a href="..."> elements, not click-only JavaScript handlers.
  • Images that are not above the fold should use lazy loading and useful alt text.
  • Do not block /assets, CSS, JavaScript, or uploaded public images in robots.txt.
  • Keep metadata configurable through profile data or environment variables, not hardcoded to the upstream demo domain.
  • If the app becomes multilingual, add language-specific routes and reciprocal hreflang links before enabling localized indexing.

License

MIT License. See LICENSE.txt.

About

Lynx is an open-source, self-hosted link manager that helps you gather all your digital touchpoints in a single page, with secure authentication and a fully customizable design.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors