This project is a self-hosted redirect + analytics service with:
- Unique tracking links
- Per-hit server data capture
- Optional client-side telemetry capture
- SQLite storage
- Admin portal with simple username/password auth from env vars
- Bot preview metadata passthrough so chat previews reflect the destination URL
Use this only where you have legal basis and consent as required by your jurisdiction and platform rules.
- IP address
- IP version (IPv4/IPv6)
- Timestamp
- User-Agent
- Browser / OS / device (from User-Agent parse)
- Referrer
- Accept-Language
- Approximate location (GeoIP lookup)
- ASN / ISP (GeoIP lookup)
- Screen resolution
- Viewport size
- Time zone
- Language
- Color depth
- Touch support
- Hardware concurrency
- Device memory (supported browsers)
- Platform
- Install dependencies:
npm install express better-sqlite3 dotenv express-session ua-parser-js ipaddr.js zod cheerio maxmind- Download local GeoLite2 database files (City + ASN):
- Create a MaxMind account and generate a license key.
- Download and extract these files into
./data:GeoLite2-City.mmdbGeoLite2-ASN.mmdb
- Copy env file and update secrets:
cp .env.example .env- Start app:
npm run dev- Open admin panel:
http://localhost:3000/admin
- Set
REDIRECT_PAGE_THEME=blackfor a black redirect interstitial. - Set
REDIRECT_PAGE_THEME=white(orlight) for a white interstitial.
GEO_LOOKUP_MODE=local- Uses local MaxMind
.mmdbfiles only.
- Uses local MaxMind
GEO_LOOKUP_MODE=local-then-remote- Uses local
.mmdb, falls back to remote API when local files are unavailable/no match.
- Uses local
GEO_LOOKUP_MODE=remote- Uses remote API only.
For fully local operation, use GEO_LOOKUP_MODE=local and keep both GeoLite2 files in ./data.
GET /admin- dashboardPOST /admin/links- create new tracked linkPOST /admin/links/:id/delete- delete tracker and all hit dataPOST /admin/links/:id/hits/delete- delete all hits for a trackerGET /admin/links/:id- list hits for one linkGET /admin/hits/:id- full hit detailsPOST /admin/hits/:id/delete- delete a single hitGET /t/:slug- tracked redirect endpointPOST /c/:slug/:hitId- client telemetry collectorGET /health- health check
When bots/crawlers visit GET /t/:slug, the app fetches the destination page metadata (og:*, title, description, image), caches it in SQLite, and serves preview tags based on the destination. This keeps most chat/social preview cards aligned with the final redirect target.
On link creation, metadata is also prefetched by default so preview tags are usually ready before first share.
PREFETCH_METADATA_ON_CREATE=true(default): prefetch on createPREFETCH_METADATA_ON_CREATE=false: fetch on first bot/crawler hit only
- Put behind HTTPS reverse proxy (Nginx/Caddy/Cloudflare Tunnel).
- Keep
SESSION_SECRETstrong and unique. - Set
BASE_URLto your public domain. - Restrict network access to admin route if possible.
Build image:
docker build -t signal-redirect .Run container:
docker run --rm -p 3000:3000 --env-file .env -v $(pwd)/data:/app/data signal-redirectNotes:
- Mount
./dataso SQLite and GeoLite2 files persist and remain accessible. - Ensure your
GeoLite2-City.mmdbandGeoLite2-ASN.mmdbfiles exist in./dataon the host. - For fully deterministic Docker builds, keep
package-lock.jsonin sync by runningnpm installafter dependency changes and committing the updated lock file.