Build ReSlug
YESreplaces $9/mosaves $108/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- accountfree 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 - 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
ADMIN_PASSWORDsecret | required | a-long-random-string-redacted | Set with wrangler secret, never in wrangler.toml. The only credential here. |
SHORT_HOSTNAME | required | go.example.com | The hostname routed to the worker. Appears in every QR code you print. |
DEFAULT_FALLBACK_URL | required | https://example.com/expired | Where an expired or capped link goes when it has no fallback of its own. |
KV_CACHE_TTL | required | 60 | How long a lookup is cached at the edge. Longer is faster and staler. |
SLUG_LENGTH | required | 6 | Random slug length. Longer costs nothing and collides less. |
RETENTION_DAYS | required | 400 | After this, raw clicks are deleted and the daily rollup remains. |
The build, in order
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.
waitUntil keeps the write alive after the response has gone.
Files
src/worker.tswrangler.tomlterminalnpx wrangler dev npx wrangler deploy
Negative lookups are cached and will outlive your mistake.
Files
src/lookup.tstests/miss-fallthrough.test.tsA 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 passeswatch 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.
A click log that fits inside the free tier
Every click is recorded, and the dashboard reads a rollup rather than scanning the log.
Otherwise every chart is a table scan you are billed for.
Files
migrations/0001_clicks.sqlRunning it twice must not double yesterday.
Files
src/rollup.tstests/rollup-idempotent.test.tsYou 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 passeswatch 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.
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.
One lookup for the whole decision, or the design is pointless.
Files
src/rules.tstests/rules.test.tsAn honest approximate cap beats a hot-path query.
Files
src/cap.tsREADME.mdA view-source bypass makes the password decorative.
Files
src/password.tstests/password-leak.test.ts
done when · tick each as it passeswatch 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.
QR codes and one admin page
Any link has a cached QR image and a single password-protected page shows what happened.
The processing budget on this tier is measured in milliseconds.
Files
src/qr.tsA timing side channel on one password is still a side channel.
Files
src/admin.tsSomewhere there is a poster with that slug on it.
Files
migrations/0002_retired.sqltests/no-reuse.test.ts
done when · tick each as it passeswatch 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.
Bots, export and the domain that outlives thisproduct builder
Bot traffic is labelled rather than deleted, everything exports, and the README tells the truth about permanence.
You cannot measure a filter whose evidence you threw away.
Files
src/bots.tstests/label-not-delete.test.tsThe database is the authority, so the export is the escape hatch.
Files
src/export.tsThis is the only real limitation of the whole build.
Files
README.md
done when · tick each as it passeswatch out- Dropping clicks whose user agent looks like a bot means you can never audit the rule that dropped them, and a slightly wrong pattern quietly deletes real people on old or unusual browsers while you congratulate yourself on clean numbers; store a label on every click, exclude labelled clicks by default, keep the toggle, and treat the labelling rules as a thing you tune with evidence rather than a filter you trust.
That is the whole plan for ReSlug. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Link-in-bio pages.
- Teams and shared workspaces.
- A public API with tokens.
- Per-visitor identifiers or cross-site tracking.
- links that outlive your own infrastructure · a domain you stop renewing takes every link with it
- bot filtering that actually works · the heuristics take iteration you will not do for one project
- geo and device routing rules, scheduled links, and click caps behind a dashboard instead of a config file
- bio pages and branded QR codes
- a REST API and webhooks other tools can call
- A scheduled export to storage you own, so the links table survives losing the account entirely
- A second hostname for a different context, which mostly tests whether the slug namespace was ever per-domain
- Bot labelling tuned against a week of real traffic, with the before and after counts written down
Need the files? The project pack on the verdict page hands your agent the whole brief · more analytics.