Build ReSlug

YESreplaces $9/mosaves $108/yrback to the verdict

0%0 of 17 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 shortener whose hard problems are the free tier's arithmetic deciding where every byte is stored, an eventually consistent lookup that can serve a stale miss, and a redirect status code that determines whether you can ever change a link again.

estimated effort one sittingthe files for this build are in the project pack

RuntimeA Cloudflare Worker on a hostname you own, deployed with wranglerLookupA key-value store for slug to destination, with the database behind it as the authorityClick logOne database row per click, written inside waitUntil after the response has goneRedirect302 with an explicit no-store header, never 301AdminOne password held as a deployment secret, on a separate path

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. free at personal scale

    Why This is where the whole thing runs, and the free tier's numbers are what the design is shaped around.

    Get it Sign up, create the namespace and the database, and note the current published limits. open ↗

    Verify wrangler whoami succeeds and both bindings appear in wrangler.toml

  2. installfree

    Why Deploys the worker, manages bindings and holds the secrets that must never enter the configuration file.

    Get it Install it with your package manager of choice.

    Verify npx wrangler --version prints a version and npx wrangler dev serves locally

  3. have readyroughly ten to fifteen pounds a year, indefinitely

    Why Every short link you print, paste or put on a poster stops working the day this domain lapses, which makes the renewal decision the most consequential part of the project.

    Get it Register or move a domain onto the same account and add the hostname route.

    Verify A request to the short hostname reaches your worker

  4. have readyfree

    Why Reads per day, writes per day, rows read, rows written and the per-invocation processing budget all decide this design, and all of them change.

    Get it Read the platform's own limits and pricing pages and copy the figures into the README.

    Verify Every figure in the README has a source URL and a checked date

  5. have readyfree

    Why The eventually consistent lookup only misbehaves when the request arrives somewhere your write has not reached, which you cannot see from one machine.

    Get it A phone on mobile data, or a shell on a host somewhere else.

    Verify A newly created link resolves from it within the window you documented

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- links(slug PRIMARY KEY, destination, created_at, expires_at, click_cap, password_hash, fallback_url, note) · in the database, which is the authority
- rules(id, slug, match_kind, match_value, destination) · country or device overrides, evaluated before the default destination
- clicks(id, slug, at, country, region, device_class, referer_host, ua_hash, bot_label) · one row per click, and only the referring host
- daily(slug, day, clicks, bots, PRIMARY KEY (slug, day)) · a rollup written by a scheduled job, which is what the dashboard reads
- The key-value entry for a slug is one small JSON blob holding the destination and its rules, so a redirect is one lookup and zero database queries on the hot path.
- No counter lives in the key-value store. The free tier allows one write per second to a single key and a low four-figure number of writes a day across all keys, which one popular link would exhaust before lunch. Check the current published figures before you design around them.
- clicks keeps a hash of the user agent and the host part of the referrer, not the full string and not the query. Enough to split devices and sources, not enough to become a tracking database.

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
ADMIN_PASSWORDsecretrequireda-long-random-string-redactedSet with wrangler secret, never in wrangler.toml. The only credential here.
SHORT_HOSTNAMErequiredgo.example.comThe hostname routed to the worker. Appears in every QR code you print.
DEFAULT_FALLBACK_URLrequiredhttps://example.com/expiredWhere an expired or capped link goes when it has no fallback of its own.
KV_CACHE_TTLrequired60How long a lookup is cached at the edge. Longer is faster and staler.
SLUG_LENGTHrequired6Random slug length. Longer costs nothing and collides less.
RETENTION_DAYSrequired400After this, raw clicks are deleted and the daily rollup remains.

The build, in order

  1. The redirect, with nothing on the hot path

    A hit on a short link resolves and redirects before any write happens, and a fresh link works everywhere.

    1. waitUntil keeps the write alive after the response has gone.

      Files src/worker.tswrangler.toml

      terminal
      npx wrangler dev
      npx wrangler deploy
    2. Negative lookups are cached and will outlive your mistake.

      Files src/lookup.tstests/miss-fallthrough.test.ts

    3. A 301 is cacheable by default and is forever in somebody's browser.

      Files src/worker.tstests/status-code.test.ts

    done when · tick each as it passes
    watch out
    • The key-value store is eventually consistent and its own documentation says changes may take up to sixty seconds or more to be visible at other locations as their cached copies time out, and it caches negative lookups too, so a slug that somebody hit one second before you created it can keep returning not-found in that region afterwards; treat a miss as a database read plus a repopulate rather than as a not-found, and check the current default cache duration before you decide how long that window is.
  2. A click log that fits inside the free tier

    Every click is recorded, and the dashboard reads a rollup rather than scanning the log.

    1. Otherwise every chart is a table scan you are billed for.

      Files migrations/0001_clicks.sql

    2. Running it twice must not double yesterday.

      Files src/rollup.tstests/rollup-idempotent.test.ts

    3. You want device split, not a record of where people came from in detail.

      Files src/click.tstests/no-pii.test.ts

    done when · tick each as it passes
    watch out
    • A dashboard that runs a count over the clicks table charges you for every row it scans rather than every row it returns, which on this database's free tier is metered as rows read per day in the millions and is trivially exhausted by one unindexed count over a busy month; index on slug and time, read the daily rollup for anything above a single link, and check both the rows-read and rows-written daily allowances now, because the rows-written figure is the real ceiling on how many clicks this design can record in a day.
  3. Rules, expiry, caps and passwords

    A link can retarget by country or device, expire, cap out or ask for a password, all decided without a database read.

    1. One lookup for the whole decision, or the design is pointless.

      Files src/rules.tstests/rules.test.ts

    2. An honest approximate cap beats a hot-path query.

      Files src/cap.tsREADME.md

    3. A view-source bypass makes the password decorative.

      Files src/password.tstests/password-leak.test.ts

    done when · tick each as it passes
    watch out
    • Enforcing a click cap by counting rows on every request puts a database query on the hot path and still gives you a stale answer, because the clicks are written asynchronously and the count lags; enforce the cap from the rolled-up figure the scheduled job maintains, accept that a capped link overshoots by whatever arrives between rollups, and write that overshoot into the README rather than implying the cap is exact.
  4. QR codes and one admin page

    Any link has a cached QR image and a single password-protected page shows what happened.

    1. The processing budget on this tier is measured in milliseconds.

      Files src/qr.ts

    2. A timing side channel on one password is still a side channel.

      Files src/admin.ts

    3. Somewhere there is a poster with that slug on it.

      Files migrations/0002_retired.sqltests/no-reuse.test.ts

    done when · tick each as it passes
    watch out
    • Generating the QR image on every request spends the worker's small per-invocation processing budget on work whose answer never changes, and the free tier gives you single-digit milliseconds of it; render once, store it, serve it from cache with a validator, and remember that a QR code printed on a poster is the most permanent thing this project will produce, so it must point at the short link and the slug behind it must never be reassigned to something else.
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.