Self-hosted link tracking & analytics platform
A lightweight, open-source Bitly alternative with full click analytics, geolocation, device detection, and a built-in admin dashboard.
- Custom slugs — create short links with your own readable slugs
- Link expiration — optional expiry date with automatic 410 Gone response
- Labels & UTM builder — tag links and auto-append UTM parameters
- QR code generation — one-click QR codes with PNG download
- Bulk operations — select and delete multiple links at once
- CSV export — export all links or per-link events
Every click is tracked asynchronously (non-blocking redirect) and captures:
- IP address (respects
X-Forwarded-For behind proxies)
- Browser & version (parsed from User-Agent)
- Device type — desktop, mobile, or tablet
- Operating system — Windows, macOS, iOS, Android, Linux, etc.
- Geolocation — country & city via IP lookup (geoip-lite)
- Referer — where the click came from
- Timestamp — precise UTC datetime
- KPI cards — total links, clicks (today / week / month / custom range)
- Time range filter — 7 days, 30 days, or 90 days
- Clicks over time — interactive line chart
- Top links, referrers, devices, countries — bar chart breakdowns
- Hourly heatmap — click intensity by day of week and hour
- Per-link analytics — detailed stats, breakdowns, and raw event log
- User management — create accounts, assign roles (admin / viewer), reset passwords
- Role-based access — viewers can read; admins can create, edit, delete
- Audit log — full history of who did what and when
- Session auth — secure HTTP-only cookies, 24h expiry with renewal
- Dark / Light / Auto theme — per-user preference saved in database
- Zero framework frontend — vanilla HTML/CSS/JS, no build step
- No ORM — direct SQL queries with
pg (node-postgres)
- Idempotent schema —
schema.sql safe to replay
- PM2 ready — production config included
- nginx reverse proxy — sample config included
git clone https://github.com/alexjuillardoff/linker.git
cd linker
npm install
sudo -u postgres psql -c "CREATE ROLE linker_app WITH LOGIN PASSWORD 'your_password' CREATEDB;"
sudo -u postgres psql -c "CREATE DATABASE linker OWNER linker_app;"
PGPASSWORD=your_password psql -h localhost -U linker_app -d linker -f schema.sql
cp .env.example .env
# Edit .env with your database credentials, admin account, and cookie secret
| Variable |
Description |
PG_HOST, PG_PORT, PG_USER, PG_PASSWORD, PG_DATABASE |
PostgreSQL connection |
PORT |
Server port (default: 8010) |
ADMIN_DEFAULT_USER, ADMIN_DEFAULT_PASSWORD |
Initial admin account |
COOKIE_SECRET |
Cookie signing secret |
# Development (auto-reload)
npm run dev
# Production
pm2 start ecosystem.config.js
pm2 save
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8010;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
listen [::]:80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}
| Method |
Route |
Description |
POST |
/admin/login |
Log in (returns session cookie) |
POST |
/admin/logout |
Log out (invalidates session) |
GET |
/admin/me |
Current session info + theme |
PUT |
/admin/me/theme |
Update theme preference |
| Method |
Route |
Description |
GET |
/admin/api/links |
List links (search, sort, paginate) |
POST |
/admin/api/links |
Create a link |
PUT |
/admin/api/links/:slug |
Update destination URL, label, expiry |
DELETE |
/admin/api/links/:slug |
Delete a link |
DELETE |
/admin/api/links/:slug/events |
Purge tracking events |
GET |
/admin/api/links/:slug/events |
Paginated event log |
GET |
/admin/api/links/:slug/stats |
Aggregated analytics |
POST |
/admin/api/links/bulk-delete |
Bulk delete by slugs |
GET |
/:slug |
Public redirect + tracking |
| Method |
Route |
Description |
GET |
/admin/api/stats/summary |
KPIs (filterable by ?days=) |
GET |
/admin/api/stats/clicks-over-time |
Daily click counts |
GET |
/admin/api/stats/top-links |
Top links by clicks |
GET |
/admin/api/stats/top-referrers |
Top referrer domains |
GET |
/admin/api/stats/devices |
Device type breakdown |
GET |
/admin/api/stats/countries |
Country breakdown |
GET |
/admin/api/stats/browsers |
Browser breakdown |
GET |
/admin/api/stats/heatmap |
Clicks by day-of-week × hour |
| Method |
Route |
Description |
GET |
/admin/api/users |
List users (admin only) |
POST |
/admin/api/users |
Create user (admin only) |
DELETE |
/admin/api/users/:id |
Delete user (admin only) |
PUT |
/admin/api/users/:id/password |
Reset password (admin only) |
GET |
/admin/api/audit |
Paginated audit log (admin only) |
| Method |
Route |
Description |
GET |
/admin/api/export/links |
CSV export of all links |
GET |
/admin/api/export/events/:slug |
CSV export of link events |
linker/
├── server.js # Express entry point
├── db.js # PostgreSQL pool + default admin setup
├── schema.sql # Idempotent DDL (tables + indexes)
├── ecosystem.config.js # PM2 production config
├── middleware/
│ └── auth.js # Session verification + role guards
├── routes/
│ ├── auth.js # Login, logout, theme preference
│ ├── admin.js # Link CRUD, events, stats, export, audit
│ ├── users.js # User management
│ ├── stats.js # Global analytics endpoints
│ └── redirect.js # Public redirect + async tracking
├── public/
│ ├── login.html # Login page
│ ├── admin.html # Single-page admin dashboard
│ └── favicon.svg # App icon
├── .env.example
└── package.json
| Layer |
Technology |
| Runtime |
Node.js 18+ |
| Framework |
Express 4 |
| Database |
PostgreSQL 14+ (via pg) |
| Auth |
bcrypt + server-side sessions |
| UA Parsing |
ua-parser-js |
| Geolocation |
geoip-lite |
| Frontend |
Vanilla HTML/CSS/JS (no framework, no build) |
| Process Manager |
PM2 |
MIT