Build Bitly

YESreplaces $35/mosaves $420/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeGo or Node 22 with PostgreSQLRedirectA temporary redirect with the lookup served from memoryCodesGenerated random codes, not sequentialAnalyticsDevice class and country derived at request time, never a stored identifierQRGenerated at request time with error correction sized to the code

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://user:pass@localhost:5432/linksLinks and aggregated counts. Not touched by the redirect path.
SHORT_DOMAINrequiredexm.plThe domain serving redirects. A long-term commitment rather than a setting.
CODE_LENGTHrequired7Random code length, chosen from expected volume with room for collisions to stay rare.
CACHE_REFRESH_SECONDSrequired30How often the in-memory map refreshes. Also how long a destination change takes to apply.
RAW_CLICK_RETENTION_DAYSrequired7Raw click rows are deleted after aggregation. Short, because they are a liability.
GEO_DB_PATHrequired./data/country.mmdbCountry lookup, used at request time with the address discarded afterwards.

The build, in order

  1. A redirect that is faster than the page

    Following a short link adds almost nothing to the load time.

    1. Refreshed in the background. The redirect path touches no database.

      Files src/redirect.go

    2. So a destination change is possible. Permanent redirects are cached beyond your reach.

      Files src/redirect.go

    3. Queued, not awaited. The visitor should not wait for your analytics.

      Files src/clicks.go

    done when · tick each as it passes
    watch 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.
  2. Codes nobody can enumerate

    Knowing one short code tells you nothing about the others.

    1. From an alphabet without confusable characters. Length chosen from your expected volume.

      Files src/codes.go

    2. A unique constraint plus a retry. Never overwrite an existing code.

      Files migrations/

    3. Your own administrative routes and anything you might want later.

      Files src/codes.go

    done when · tick each as it passes
    watch 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.
  3. Counting clicks without tracking people

    You learn how a link performed without building a record of who clicked it.

    1. Country and device class at request time. The address never reaches storage.

      Files src/clicks.go

    2. What the interface reads. Raw rows are transient.

      Files src/aggregate.go

    3. Short window. Keeping them is a liability without a benefit once aggregated.

      Files src/retention.go

    done when · tick each as it passes
    watch 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.
  4. QR codes that work on a poster

    A generated code scans reliably at the size it will actually be printed.

    1. Included in the output rather than assumed to be added by whoever places it.

      Files src/qr.go

    2. Higher for print where a code may be scuffed, lower for screen where it will not.

      Files src/qr.go

    3. On paper, at the distance people will stand. Screen testing hides the failures.

    done when · tick each as it passes
    watch 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.
  5. Managing links without breaking them

    You can edit and retire links without turning a working link into a dead one.

    1. Three different states with three different behaviours.

      Files src/links.go

    2. Retired means gone. Reuse sends people somewhere they did not intend to go.

      Files src/codes.go

    3. A 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 passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more analytics.