Build Bitly
YESreplaces $35/mosaves $420/yrback to the verdict
A redirect service mapping short codes to destinations with a fast redirect path, click logging that records device class and country without identifying anyone, QR codes generated for any link, and an export that lets the links survive the software.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why The redirect is a hot path worth being able to profile confidently.
Get it go.dev or nodejs.org.
Verify
The toolchain builds and runs a hello world - have readyvaries by extension; short ones cost more, and this is a long commitment
Why This is the real prerequisite. Every link depends on the domain continuing to exist, and links outlive intentions.
Get it Register something short you will not resent renewing. Set auto-renew immediately and record who else can renew it.
Verify
The domain auto-renews and a second person knows how - installfree
Why Links and aggregated counts. The redirect path does not touch it, but everything else does.
Get it Install locally or run it in Docker. open ↗
Verify
psql connects and reports 16 or higher - have readyfree at country level with an open database
Why Country is derived at request time from the address, which is then discarded. The database has to be one you are licensed to use.
Get it An openly licensed country-level database is sufficient; you do not need city precision and should not store it.
Verify
A known address resolves to the right country - have readyfree
Why Printed codes fail for reasons that are invisible on screen: size, quiet zone and contrast. The only test is a printed one.
Get it Any printer. Print at the size you intend to use and scan from the intended distance.
Verify
A printed code scans at real size
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- links(id, code, destination, created_at, created_by, expires_at, disabled_at, title) - clicks(link_id, at, device_class, country, referrer_host) · no address, no identifier, no fingerprint - daily_counts(link_id, on_date, device_class, country, count) · aggregated, which is what the interface reads - domains(id, host, verified_at) · if you serve several short domains - The clicks table holds no value that identifies a person. That is a deliberate limit and it is what makes the analytics honest.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://user:pass@localhost:5432/links | Links and aggregated counts. Not touched by the redirect path. |
SHORT_DOMAIN | required | exm.pl | The domain serving redirects. A long-term commitment rather than a setting. |
CODE_LENGTH | required | 7 | Random code length, chosen from expected volume with room for collisions to stay rare. |
CACHE_REFRESH_SECONDS | required | 30 | How often the in-memory map refreshes. Also how long a destination change takes to apply. |
RAW_CLICK_RETENTION_DAYS | required | 7 | Raw click rows are deleted after aggregation. Short, because they are a liability. |
GEO_DB_PATH | required | ./data/country.mmdb | Country lookup, used at request time with the address discarded afterwards. |
The build, in order
A redirect that is faster than the page
Following a short link adds almost nothing to the load time.
Refreshed in the background. The redirect path touches no database.
Files
src/redirect.goSo a destination change is possible. Permanent redirects are cached beyond your reach.
Files
src/redirect.goQueued, not awaited. The visitor should not wait for your analytics.
Files
src/clicks.go
done when · tick each as it passeswatch out- Using a permanent redirect means browsers and intermediaries cache it forever, so a destination you later change keeps sending old visitors to the old place with no way to correct it. Use a temporary redirect unless you are certain the destination will never change.
- A database query per redirect is fine at ten clicks a minute and the bottleneck at a thousand. Serve from memory and refresh in the background.
Codes nobody can enumerate
Knowing one short code tells you nothing about the others.
From an alphabet without confusable characters. Length chosen from your expected volume.
Files
src/codes.goA unique constraint plus a retry. Never overwrite an existing code.
Files
migrations/Your own administrative routes and anything you might want later.
Files
src/codes.go
done when · tick each as it passeswatch out- Sequential or incrementing codes let anyone walk your entire link list, which for a shortener used internally exposes documents, drafts and campaigns you never published. Random codes with a collision check cost nothing and remove the problem entirely.
Counting clicks without tracking people
You learn how a link performed without building a record of who clicked it.
Country and device class at request time. The address never reaches storage.
Files
src/clicks.goWhat the interface reads. Raw rows are transient.
Files
src/aggregate.goShort window. Keeping them is a liability without a benefit once aggregated.
Files
src/retention.go
done when · tick each as it passeswatch out- Storing the address to derive a country later means you have stored personal data, and once it is in the database it is in the backups and the exports. Derive at request time, store the derived value and discard the input, which gives you the same analytics with none of the obligation.
QR codes that work on a poster
A generated code scans reliably at the size it will actually be printed.
Included in the output rather than assumed to be added by whoever places it.
Files
src/qr.goHigher for print where a code may be scuffed, lower for screen where it will not.
Files
src/qr.goOn paper, at the distance people will stand. Screen testing hides the failures.
done when · tick each as it passeswatch out- A QR code without an adequate quiet zone around it, or printed smaller than its module size supports, fails to scan and the failure is silent: people simply do not scan it and you never hear. Keep the quiet zone, and test a printed code at real size rather than on screen.
Managing links without breaking them
You can edit and retire links without turning a working link into a dead one.
Three different states with three different behaviours.
Files
src/links.goRetired means gone. Reuse sends people somewhere they did not intend to go.
Files
src/codes.goA page saying the link has expired beats a generic error for anyone holding a printed copy.
Files
templates/gone.html
done when · tick each as it passeswatch out- Deleting a link and later issuing the same code to a different destination sends people who followed an old link somewhere unexpected, which for a link in printed material is genuinely misleading. Retire codes permanently rather than freeing them.
Making the links outlive the softwareproduct builder
If you stop running this, the links can still work.
In a format a common static host understands. Test it by serving it.
Files
src/export.goAuto-renew, a reminder you will see, and a second person who knows how.
Files
docs/continuity.mdWhat happens to links if you stop. Deciding it now is easier than deciding it later.
Files
docs/continuity.md
done when · tick each as it passeswatch out- The domain lapsing is the failure that breaks every link at once and it happens through a missed renewal email rather than a decision. Set the domain to auto-renew, put a reminder in a calendar you actually read, and record who else knows how to renew it.
That is the whole plan for Bitly. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- The recognition and longevity of an established short domain.
- Campaign grouping and landing pages.
- Individual click journeys and visitor identification.
- Any guarantee your links outlive your domain renewal.
- links that outlive your own infrastructure · a domain you stop renewing takes every link with it
- bit.ly's recognition in the link itself, which cuts both ways now that shorteners get spam-filtered
- bundled QR code management, landing pages, and campaign grouping
- city-level and device-level analytics rollups without building them
- an SLA and someone to call
- Link expiry with a redirect to a replacement, which is kinder than a dead page
- A second short domain for a different context, since one domain's reputation is shared by every link on it
- Aggregate referrer reporting, which is useful and still identifies nobody
Need the files? The project pack on the verdict page hands your agent the whole brief · more analytics.