Bitly

bitly.comcontributed by Samuele Ongaro

YES

Replaceable in one session with an AI coding agent.

A link shortener with tracking. Every short link records clicks, referrers and rough location, and paid plans add custom domains, QR codes and campaign grouping so marketing can tell which channel actually sent people.

Promptfree, for everyone, and the only version there is
Build me a self-hosted link shortener that replaces Bitly, end to end: the redirect service, the admin interface, and honest click analytics. Ship it running behind my own domain.

STACK
- Node 20+ with Fastify. No framework beyond it, no build step for the server
- SQLite through better-sqlite3, one file, WAL mode. It is fast enough for millions of redirects and it backs up with a file copy
- Server-rendered HTML with a little vanilla JavaScript. No React, no client-side router
- Caddy in front, terminating TLS and serving static assets
- Everything runs from one process on one small VPS

THE DATA MODEL
- links: id, slug (unique), destination, title, created_at, expires_at, is_permanent, archived_at, created_by, password_hash, click_limit
- clicks: id, link_id, created_at, referrer_host, referrer_url, country, device, browser, os, ip_hash, bot
- users: id, email, password_hash, role, created_at
- domains: id, host, is_default — one install can serve several short domains
- Index clicks on (link_id, created_at) and links on slug. Nothing else until something is slow
- Never store a raw IP. ip_hash is HMAC-SHA256 of the address with a secret salt from the environment, and the salt never enters the database or a backup

THE REDIRECT, WHICH IS THE WHOLE PRODUCT
- GET /:slug looks the link up by slug, in one prepared statement, and answers in under a millisecond
- 301 when is_permanent, 302 otherwise. Explain in a comment why: a 301 is cached by the browser forever and the link can never be re-pointed
- Unknown slug: 404 with a plain page, not a redirect to the homepage
- Expired, archived, or over its click limit: 410 Gone with a page saying so
- Password-protected links: a form, and the destination is only revealed on the correct password
- The click is recorded after the response is sent, never before it. A redirect must not wait on a write
- Bots are recorded with bot = 1 rather than dropped: 'how much of that spike was a crawler' is only answerable from rows that exist
- Respect Do Not Track by still counting the click but storing no referrer and no profile

CREATING AND MANAGING LINKS
- Admin at /admin behind session auth, cookie signed and http-only, no third-party auth
- Create: destination, optional custom slug, optional title, optional expiry, optional password, optional click limit
- Generated slugs are base62, seven characters, checked for collision, and never reuse a slug that has existed
- A reserved list — admin, api, login, static, favicon.ico, robots.txt — that can never become a slug
- Bulk import from CSV: destination, slug, title. Report which rows failed and why, import the rest
- Edit the destination of any link that is not permanent. Every change is appended to a link_edits table with the old and new destination, never overwritten
- Archive rather than delete: an archived link answers 410 and keeps its history
- Search and filter by slug, destination, title, tag, date, and click count
- Tags, many-to-many, so campaigns can be grouped

THE ANALYTICS, HONESTLY
- Per link: clicks over time, unique visitors, top referrers, countries, devices, browsers
- A click is one hit. A unique visitor is one distinct ip_hash within the window. Say which is which on the page and never blur the two
- Windows: today, 7 days, 30 days, all time, and a custom range. Compute them at query time from the clicks table; store no aggregate tables
- Referrers as hostname plus the full referring URL when the browser sends one: the host says Reddit, the path says which thread
- Country from a local MaxMind or DB-IP file read off disk. Never call a geolocation API
- UTM parameters on the destination are preserved through the redirect, and a link's own utm tags can be set once and applied to every click-through
- Export any view to CSV
- A public stats page per link, if the owner turns it on, at /:slug/+ — the way Bitly does it

THE API
- Token auth, tokens created in the admin and stored hashed
- POST /api/links create, GET /api/links list with pagination, PATCH /api/links/:id, DELETE archives
- GET /api/links/:id/clicks with a date range
- Rate limit per token, 429 with a Retry-After header
- Return real HTTP codes and a JSON error with a machine-readable code, never 200 with an error inside

THE INTERFACE
- Dark by default with a light mode, one accent colour, system font stack
- The link list is a table: slug, destination, clicks, created, last click. Sortable, and it stays readable on a phone — rows gain a line rather than losing a column
- Creating a link is one field and one button on the list page; everything else is optional and folded away
- Copy button on every row, with a QR code for each link generated locally, no third-party QR service
- Keyboard shortcuts: / to search, n for a new link
- No modal dialogs for anything that could be a page

OPERATIONS
- One .env: DATABASE_PATH, HASH_SALT, SESSION_SECRET, GEOIP_DB_PATH, BASE_DOMAIN
- A migrations folder, applied in order on boot, each one running once and recorded in a schema_migrations table
- A nightly backup script: VACUUM INTO a timestamped copy, gzip it, upload it to S3-compatible storage, keep 7 daily and 4 weekly. Include a restore script and test it
- Structured request logs to stdout, no log of the destination URLs by default
- A health endpoint that checks the database, not just the process

WHAT MATTERS MOST
The redirect path is the product: it must be fast, it must never lose a click, and it must never be blocked by analytics. Everything else can be slow. Write the redirect handler first, prove it with a load test of ten thousand requests, and only then build the admin around it.

Give me the repository, the migrations, the .env.example, a README with the deploy steps for a fresh Ubuntu VPS behind Caddy, and a seed script that creates the first admin user.

What you lose

  • Automatic SSL provisioning and renewal for your branded short domain
  • Click analytics that already separate humans from scanners and bots
  • Redirect uptime on links you cannot edit once they are printed on something physical
  • Existing bit.ly links, which cannot be migrated to another domain
  • QR codes, mobile deep links and app-store routing handled for you

If you would rather not build

  • Short.io — paid, custom domains and click analytics without self-hosting

What it costs

read from their page 15 Aug 2026

PlanBilled monthlyBilled yearlyLast read
Core$10/mo—15 Aug 2026

Their pricing page is where these came from. Seeing a different price? Tell us.

The escape hatch

open source · no votes, no paid placement

YOURLS

$0

Long-running PHP link shortener with click statistics and a plugin system.

YOURLS/YOURLSfree · open source

Shlink

$0

Short-URL server with a REST API, QR codes and per-link visit tracking.

shlinkio/shlinkfree · open source

Dub

$0

Link platform with analytics, custom domains and team workspaces.

dubinc/dubfree · open source

Why this verdict

our own opinion · changed only by a person

88/100

Verdict yes at 88: redirects plus a hits table is genuinely one session of work, and a short domain you control is strictly better than a shared one. Score is not higher only because link permanence is a real operational commitment.

History

tracked since 6 Aug 2026 · nothing is ever overwritten

Interest · last 30 dayspeak 3/day
views012330 Aug4 Sept9 Sept14 Sept19 Sept24 Sept28 Sept
— views— prompt copies none yet— votes none yet

Questions about Bitly

answered from the record above

Is Bitly free?

No — the plan we track is $10 a month. Core plan, billed annually at $120/yr. Bitly does not offer Core on monthly billing; the next tier up, Growth, is $35/mo month-to-month.

Can you replace Bitly by building your own?

YES. Replaceable in one session with an AI coding agent. Replacement score 88 out of 100, build time one session. Read what you lose before you decide.

How much does Bitly cost?

$10 a month on Core — $120 a year. Recorded 6 Aug 2026.

What do you lose by replacing Bitly?

Automatic SSL provisioning and renewal for your branded short domain; Click analytics that already separate humans from scanners and bots; Redirect uptime on links you cannot edit once they are printed on something physical; Existing bit.ly links, which cannot be migrated to another domain; QR codes, mobile deep links and app-store routing handled for you. If any of those carry weight for you, keep paying.

Is there an open-source alternative to Bitly?

Yes: YOURLS, Shlink, Dub. The prompt on this page is for when you want it your way instead.

Related entries

same category first, most replaced first

All 17 in Social media & links

Not sending yet

Every week, something stops being worth paying for.

New verdicts, prices that moved, entries added. One email a week. Unsubscribe in one click. Nothing is being sent yet — your address is kept here, and the first issue is the first thing it is used for.

free forever · no tracking pixel · stored here, never passed to anyone

Esc