Self-hosted URL shortener with statistics, QR codes, and password protection.
Part of the MSK ecosystem β open source, privacy-friendly, no tracking, no third parties.
Live demo β Β Β·Β Report a bug Β Β·Β Request a feature
| π Custom short codes | Define your own short URLs or let the system generate one |
| π Click statistics | Anonymous tracking with interactive charts and aggregations |
| π Password protection | Lock links behind a password (bcrypt, cost 12) |
| β±οΈ Link expiration | Auto-expire links at a date/time of your choice |
| π± QR code generator | Download as PNG or SVG |
| π REST API | Fully documented endpoints for automation |
| π Bilingual UI | German and English, switchable per user (cookie-based) |
| π‘οΈ Privacy-friendly | IP hashing, no GeoIP, no third-party trackers, no analytics cookies |
| β‘ Rate limiting | 20 links/hour per IP to prevent abuse |
| ποΈ Delete tokens | Every short link comes with a one-time delete token |
The reference deployment runs at s.msk-scripts.de.
You're welcome to use it, or self-host your own (see Installation below).
| Category | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript (strict mode) |
| Database | MariaDB via mysql2 |
| Styling | Tailwind CSS |
| Validation | Zod 4 |
| i18n | next-intl 4 |
| Hashing | bcryptjs 3 |
| Short codes | nanoid |
| QR codes | qrcode |
| Charts | Recharts |
| UA parsing | ua-parser-js 2 |
| Deployment | Debian/Ubuntu + Apache2 + systemd |
| CI/CD | GitHub Actions |
Deliberately not used: Prisma (too much overhead), Redis (single-server setup), third-party auth providers.
- Node.js 20 or later
- MariaDB 10.6 or later
- A modern terminal (Linux, macOS, or WSL on Windows)
# Clone the repository
git clone https://github.com/MSK-Scripts/msk-shortener.git
cd msk-shortener
# Install dependencies
npm install
# Copy the environment template and adjust the values
cp .env.example .env
# Run database migrations
npm run migrate
# Start the dev server
npm run devThe app will be available at http://localhost:3000.
For a full production setup on Debian/Ubuntu, see the dedicated Deployment Guide β.
TL;DR for a one-shot install:
curl -fsSL https://raw.githubusercontent.com/MSK-Scripts/msk-shortener/main/deployment/scripts/install.sh \
| sudo bashThis walks you through domain setup, MariaDB, Apache, Let's Encrypt SSL, systemd, and prints out the GitHub secrets you'll need for CI/CD.
Every feature in the web UI is available through the REST API.
Full documentation: docu.msk-scripts.de/shortener
curl -X POST https://s.msk-scripts.de/api/links \
-H "Content-Type: application/json" \
-d '{
"url": "https://msk-scripts.de",
"customCode": "msk",
"expiresAt": "2026-12-31T23:59:59Z"
}'Response:
{
"shortCode": "msk",
"shortUrl": "https://s.msk-scripts.de/msk",
"deleteToken": "dk_a7c4f2β¦",
"expiresAt": "2026-12-31T23:59:59.000Z",
"hasPassword": false
}| Method | Path | Description |
|---|---|---|
POST |
/api/links |
Create a new short link |
GET |
/api/links/:code |
Look up a link |
DELETE |
/api/links/:code |
Delete a link (requires deleteToken) |
GET |
/api/links/:code/stats |
Get click statistics for one link |
GET |
/api/links/:code/qr |
QR code as PNG or SVG |
GET |
/api/stats |
Global statistics across all links |
POST |
/api/verify |
Verify a password-protected link |
All configuration happens through environment variables. See .env.example for the full list.
Most important:
| Variable | Description |
|---|---|
NEXT_PUBLIC_BASE_URL |
Public base URL of your instance (used for QR codes and share URLs) |
DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME |
MariaDB connection |
IP_HASH_SECRET |
Secret for HMAC-based IP hashing β generate with openssl rand -hex 32 |
RATE_LIMIT_CREATE_PER_HOUR |
Max links per IP per hour (default: 20) |
SHORTCODE_LENGTH |
Length of auto-generated short codes (default: 7) |
MSK Shortener is built with GDPR compliance and personal privacy in mind.
- No IP addresses stored in plain text β only salted HMAC hashes
- No GeoIP lookups β country/region data is never collected
- No third-party trackers β no Google Analytics, no Plausible, nothing
- No tracking cookies β the only cookie sets the user's chosen language
- Password hashing β bcrypt with cost factor 12
- SSRF protection β private IP ranges (RFC 1918, loopback, link-local) are rejected as targets
- Rate limiting β abuse protection per IP
- Strict CSP headers β defense in depth against XSS
A full privacy policy in German and English is available at /privacy on every instance.
To report a security issue, see SECURITY.md.
The UI is available in German and English.
- Language is detected from the user's browser on first visit (
Accept-Languageheader) - Users can switch languages via the header dropdown β the choice is stored in a cookie
- No URL prefix (no
/de/β¦or/en/β¦) β short links stay as short as possible - All UI strings live in
messages/de.jsonandmessages/en.json
Want to add another language? PRs welcome!
Contributions are welcome! This is a single-maintainer project, so please:
- Open an issue first to discuss larger changes
- Keep PRs focused β one concern per PR
- Run
npm run lintandnpm run type-checkbefore submitting - Follow the existing code style
For bug reports, please include:
- Steps to reproduce
- Expected vs. actual behavior
- Your environment (Node version, browser, OS)
GNU Affero General Public License v3.0 or later Β© MSK Scripts
- β You can use, copy, and modify this software
- β You can distribute your own versions
β οΈ If you offer a modified version as a network service (e.g. a public web app), you must make your modifications publicly available under the same licenseβ οΈ Derivative works must also be licensed under AGPL-3.0
If you need a commercial license without the copyleft obligations, please contact the author at info@msk-scripts.de.
Part of the MSK ecosystem Β Β·Β msk-scripts.de Β Β·Β musiker15.de