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.
- 🎬 Demo
- ✨ Features
- 🔐 Security
- 🚀 Quick Start
- ⚙️ Configuration
- 🐳 Docker
- ☁️ Railway
- 📝 Changelog
- 📄 License
- 🌐 Public page: https://lynx-demo.paoloronco.it
- 🛠 Admin panel: https://lynx-demo.paoloronco.it/admin
- 👤 Username:
admin - 🔑 Password:
ChangeMe123!
- 🎛 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.
- Passwords are hashed with
bcryptjsusing 12 salt rounds. - Sessions use signed JWTs with a 12-hour expiry.
- The frontend stores JWTs encrypted in
localStoragewith AES-GCM. - SQLite access uses parameterized queries.
- API, auth, login, reset, and SPA routes are rate-limited.
- Docker startup requires
JWT_SECRETto avoid unstable sessions after restart. - Optional
RESET_TOKENsupports token-protected recovery if you are locked out.
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.0or>=22.12.0 - npm
- Git
1. Clone the repository
git clone https://github.com/paoloronco/Lynx.git
cd Lynx/LYNX2. Install dependencies
npm ci
npm run install:server3. Build and start
npm run startnpm run start runs the frontend build and then starts the backend server.
Open:
- 🌐 Public page: http://localhost:3001
- 🛠 Admin panel: http://localhost:3001/admin
- ❤️ Health check: http://localhost:3001/health
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:
- Backend/API: Express server on http://localhost:3001
- Frontend: Vite dev server on http://localhost:8080
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:server2. Start the backend in the first terminal
cd Lynx/LYNX
npm run server:dev3. Start the frontend in the second terminal
cd Lynx/LYNX
npm run devKeep both terminals open while developing.
Open:
- 🌐 Frontend: http://localhost:8080
- 🛠 Admin panel: http://localhost:8080/admin
- ❤️ API health check: http://localhost:3001/health
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 -dThe 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:
- 🌐 Public page: http://localhost:8080
- 🛠 Admin panel: http://localhost:8080/admin
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. |
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.dbunlessDATA_DIRis set. - Docker: mount
/app/dataso the database and uploads survive container updates.
Docker Compose
From the repository root:
docker compose up -dThe 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:latestOpen:
- 🌐 Public page: http://localhost:8080
- 🛠 Admin panel: http://localhost:8080/admin
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:latestThen open https://localhost:8443. The browser will warn because the certificate is self-signed.
Deploy on Railway
Railway can deploy the repository using the root Dockerfile.
- Create a new Railway project from the GitHub repository.
- Let Railway use the Dockerfile in the repository root.
- Add:
NODE_ENV=production
PORT=8080
JWT_SECRET=replace-with-a-long-random-secret- Deploy the service.
- 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.
v4.1.8
- Allows the current Usercentrics embed domains in the production CSP.
- Keeps embedded legal policy scripts executable and ordered when rendered on
/privacyand/cookies. - Adds a regression test for legal policy provider CSP sources.
v4.1.7
- Executes custom external CMP scripts reliably, including pasted iubenda widget snippets.
- Publishes Docker images only from release tags to avoid duplicate Docker builds on
mainandv*. - Keeps
latest, semantic version, and short SHA Docker tags on release builds.
v4.1.6
- 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
/privacyand/cookiesin demo mode. - Ensures iubenda embedded policy scripts load reliably in SPA-rendered legal pages.
v4.1.5
- Stops demo-mode write protection from being shown as an expired admin session.
- Ensures embedded legal policy scripts execute on
/privacyand/cookies. - Infers hosted legal pages from existing
/privacyand/cookiesprofile URLs for upgraded installs.
v4.1.4
- 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
/privacyand/cookiesalways render the latest selected source without stale provider fallback. - Keeps form edits on screen when the admin session expires during save.
v4.1.3
- 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
- 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
/privacyand/cookies. - Added a public
/cookiesplaceholder page.
v4.1.1
- 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
/privacypage to render in a readable light layout.
v4.1.0
- 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
- 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.
- 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
- 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 agtag.jsscript on the public page only — the admin panel is never tracked. - Content Security Policy updated to allow
googletagmanager.comandgoogle-analytics.comscript and connect sources. - Measurement ID is validated client-side before saving (format
G-XXXXXXXXXX).
v3.7.0
- 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
distbefore building. - Fixed legacy database migration handling.
- Fixed missing
fsimport indatabase.js.
- 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.
MIT License. See LICENSE.txt.

