Open-source, self-hosted URL shortener with real click analytics, QR codes and a full moderation console — a free Bitly alternative you can run on your own domain.
A maintenance release — full detail in the changelog.
- Domain blocking is retroactive and covers subdomains. Blocking a domain now takes
offline the links that already point at it, and one entry covers the apex,
www.and every subdomain. - A round of security fixes, including an admin account-enumeration oracle in the login endpoint, a "revoke sessions" action that did not actually revoke sessions, CSRF protection on the admin console, and several SSRF-guard bypasses. See the Security section of the changelog.
- First tests (Vitest) covering the URL-safety guard and crypto helpers.
- Repository docs: contributing guide, security policy, changelog, issue and PR templates.
What's new · Why Shorty · Screenshots · Features · Quick start · Project layout · Documentation · Roadmap · Contributing · Security · FAQ · Licence
Shorty is a self-hosted URL shortener you can point at your own short domain. It is a practical open source alternative to Bitly, TinyURL and Short.io for anyone who would rather not hand their click data — or their audience's — to someone else's analytics product.
Where most self-hosted shorteners stop at "long URL in, short URL out", Shorty ships the part that actually takes the time to build: a role-based moderation console. Abuse reports get triaged, destinations get blocked, every admin action lands in an append-only audit log, and bot traffic is separated from real clicks before it ever reaches a chart.
It runs on MySQL 8 or TiDB, is written end-to-end in TypeScript, and is MIT licensed. No account is required to shorten a link, and there is no paywall on analytics.
| Home | Link analytics |
|---|---|
![]() |
![]() |
| Abuse reporting | Dark theme |
|---|---|
![]() |
![]() |
Note
Admin console screenshots are still to come — see Roadmap. They need a seeded database first, since the real console shows destination URLs, reporter emails and visitor detail.
- One-click shortening. No account, no signup, no paywall.
- Real analytics. Total and unique clicks, 30-day trend, referrers, countries and devices, with bot traffic separated out.
- QR codes. Downloadable, print-ready PNG for every link.
- Abuse reporting. Structured categories, auto-flagging, and auto-block on repeated reports.
- Light and dark themes. With no flash of the wrong theme on load.
- Built for SEO. Server-rendered pages, per-route metadata, JSON-LD (
WebApplication,FAQPage,HowTo),sitemap.xml,robots.txt, canonical URLs, and OpenGraph/Twitter cards.
- Overview. Links, clicks, pending reports and messages at a glance, with 30-day trend charts.
- Links. Search and filter by status/domain/date; block, unblock, expire, reactivate, soft-delete, restore, edit title/note/expiry, and bulk actions. Per-link drawer with full analytics and recent visitor detail.
- Reports. Triage queue with one-click "resolve and block the link".
- Messages. Inbox for the contact form, with statuses and internal notes.
- Blocked domains. Blocking a domain is retroactive: it refuses new links and takes offline every link already pointing there, reporting how many. One entry covers the apex,
www.and every subdomain automatically, so blockingevil.comalso stopswww.evil.comandlogin.evil.com. - Audit log. Append-only record of every admin action, including failed sign-ins.
- Admin accounts. Owner-only management with three roles.
- Password hashing with bcrypt (bcryptjs) at cost 12, short-lived HS256 access tokens, and rotating opaque refresh tokens stored hashed.
- Tokens live in httpOnly cookies held by the Next.js server, never in
localStorage, so injected script cannot read them. - Role-based access control (
owner>admin>moderator) enforced server-side. - Account lockout after repeated failed sign-ins, and every sign-in failure returns an identical response after identical work, so the endpoint cannot be used to discover which addresses are real.
- SSRF/abuse guard. Private, loopback, link-local, CGNAT and reserved-TLD destinations are refused, including IPv4-mapped IPv6 forms such as
[::ffff:a9fe:a9fe], as are URLs carrying embedded credentials. - Client IPs come from
req.ip, derived fromX-Forwarded-Foragainst a configured proxy depth, so rate limits and audit records cannot be dodged with a spoofed header. - Per-route rate limiting, a
default-src 'none'CSP on the API, and Zod validation on request bodies.
See SECURITY.md for the supported-versions matrix and how to report a vulnerability.
Prerequisites: Node.js 22.x for the API (20.9+ for the web app), and a MySQL 8 or TiDB database.
git clone https://github.com/shehari007/url-shorty.git
cd url-shortynpm run db:setup connects into an existing schema, so create it first:
CREATE DATABASE `shorty-db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;cd server
npm install
cp .env.example .env # then fill it in, see the table in server/README.mdGenerate a signing secret and paste it into ADMIN_JWT_SECRET:
openssl rand -base64 48 # macOS/Linux, or Git Bash on Windows
node -e "console.log(require('crypto').randomBytes(48).toString('base64'))" # any platformThen:
npm run db:setup # fresh database
# ...or, upgrading an existing v2 install:
npm run db:setup -- upgrade
npm run admin:create # create your first owner account
npm run dev # http://localhost:8080Tip
Running MySQL locally without TLS? Set DB_SSL=false in .env — the default expects a
managed provider with a valid certificate chain.
cd ../frontend
npm install
cp .env.example .env.local # point NEXT_PUBLIC_API_URL at the API
npm run dev # http://localhost:3000Sign in to the console at http://localhost:3000/admin/login.
url-shorty/
├─ server/
│ ├─ api/index.ts Vercel serverless entry (exports the Express app)
│ ├─ sql/ 000_preflight · 001_baseline · 002_upgrade_v3
│ ├─ scripts/ setup-db.ts · create-admin.ts · migrate-data.ts
│ └─ src/
│ ├─ config/env.ts Zod-validated environment
│ ├─ db/ Drizzle schema + pooled client
│ ├─ lib/ errors · logger · crypto · request · url-safety · risk
│ ├─ middleware/ security · cors · rate-limit · auth · validate
│ ├─ modules/ links · redirect · stats · reports · contact · admin
│ └─ routes/ public API + admin router
└─ frontend/
├─ proxy.ts Next 16 proxy (was middleware.ts), /admin gate
├─ scripts/generate-og.mjs Regenerates the social card in public/
└─ src/
├─ app/
│ ├─ (site)/ public pages
│ ├─ admin/ login + (console) route group
│ └─ api/admin/ BFF: session + authenticated proxy
├─ components/ public + admin UI
├─ lib/ api client · admin client · server session
└─ theme/ design tokens + antd config
| Guide | Covers |
|---|---|
| server/README.md | Environment reference, API endpoints, database and migrations, deployment |
| frontend/README.md | Environment reference, routing, admin auth flow, SEO, theming, deployment |
| CONTRIBUTING.md | Local setup, coding conventions, PR checklist |
| SECURITY.md | Supported versions and vulnerability disclosure |
| CHANGELOG.md | Release history |
Shorty is two independently deployable services:
| Service | Directory | Stack | Deployed as |
|---|---|---|---|
| Web | frontend/ |
Next.js 16 (App Router), React 19, Ant Design 6, TypeScript | shorty.msyb.dev |
| API + redirects | server/ |
Express 5, Drizzle ORM, MySQL/TiDB, TypeScript | short.msyb.dev |
The web app serves the public marketing site and the /admin console. The API serves the
JSON endpoints and resolves short links (short.msyb.dev/abc123). Admin tokens never
reach the browser: the Next.js server holds them in httpOnly cookies and proxies
authenticated calls through its own BFF route.
-
Dockerfile+docker-compose.ymlfor a one-command self-host - Extend the Vitest suite to
riskscoring and the admin auth flow - Admin console screenshots, once there is a seed-data path safe to capture
- Custom slugs and link expiry presets in the public UI
Ideas and votes welcome in Discussions.
Contributions are welcome. Start with CONTRIBUTING.md — it covers local
setup for both services, the conventions this codebase follows, and what a good PR looks
like. Good first issues are labelled good first issue.
If Shorty is useful to you, a ⭐ helps other people find it.
Please do not open a public issue for security problems. See SECURITY.md for the private disclosure process.
Is Shorty a self-hosted Bitly alternative?
Yes. Shorty covers the parts of Bitly most people actually use — short links, QR codes, click analytics with geography and referrers — and adds moderation tooling. You run it on your own infrastructure and your own domain, so the click data stays with you.
Can I use my own short domain?
Yes. Point a domain at the API service and set SHORTURLDEF in server/.env. Generated
links use that origin.
Do visitors need an account?
No. Shortening, QR codes and public analytics need no signup. Accounts exist only for the
/admin console.
What does Shorty record about a click?
Timestamp, visitor IP, user agent (parsed into device/browser/OS), referrer and derived country. The IP is stored in plaintext so that abuse reports can be acted on and repeat visitors can be de-duplicated into "unique clicks". It is used for analytics and abuse handling and is never exposed publicly, but you should treat the database as containing personal data and say so in your own privacy policy.
Which database do I need?
MySQL 8 or any MySQL-compatible service. It is developed against TiDB Cloud, whose serverless tier is a comfortable fit for the schema.
MIT — see LICENSE.
Built by shehari007
If this saved you some time, consider leaving a ⭐



