Building a URL Shortener on Cloudflare Workers + KV

A minimal URL shortener running entirely on Cloudflare edge infrastructure — Workers for logic, KV for storage, and a retro terminal UI for management.

I wanted a personal URL shortener. Something I control, running on my own domain, with no third-party tracking or rate limits. The whole thing took about an hour to set up using Cloudflare Workers and KV.

Why build your own

Services like Bitly or TinyURL work fine, but they come with limitations. Free tiers cap your links, analytics are paywalled, and you’re stuck with their domain. I already have luqmannor.com on Cloudflare, so spinning up go.luqmannor.com as a shortener felt natural.

The requirements were simple:

  • Custom slugs (e.g. go.luqmannor.com/github)
  • Auto-generated slugs for quick sharing
  • Click tracking
  • A basic admin UI to manage links
  • No database to maintain

The stack

Everything runs on Cloudflare’s free tier:

  • Workers — handles redirect logic and the API
  • KV — key-value store for slug → URL mappings
  • Pages — hosts the admin frontend

The Worker intercepts every request to go.luqmannor.com. If the path matches a stored slug, it returns a 302 redirect. If it’s an API route, it handles CRUD operations. Otherwise, it serves the admin UI.

How the Worker works

The core logic is straightforward. A request comes in, we check if the path is an API call or a slug lookup:

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname;

    // API routes for managing links
    if (path.startsWith('/api/')) {
      return handleAPI(request, env, url);
    }

    // Slug lookup — redirect if found
    const slug = path.slice(1);
    if (slug) {
      const target = await env.URLS.get(slug);
      if (target) {
        return Response.redirect(target, 302);
      }
      return new Response('Not found', { status: 404 });
    }

    return new Response(ADMIN_HTML, {
      headers: { 'Content-Type': 'text/html' },
    });
  }
};

KV reads at the edge are fast — single-digit milliseconds in most regions. A redirect lookup is essentially one KV read, so the latency overhead is negligible.

Storage design

Each short link stores two KV entries:

  1. slug → target URL (for fast redirect lookups)
  2. meta:slug → JSON with metadata (creation date, click count, last clicked)

This keeps the redirect path as lean as possible — one key read, no JSON parsing. The metadata is only fetched when listing links in the admin panel or incrementing click counts.

Auth

The admin API is protected with a simple bearer token stored as a Worker secret. Not OAuth, not JWT — just a password you enter once in the UI. It’s stored in localStorage and sent with every API request.

For a personal tool used by one person, this is enough. The redirects themselves are public — anyone with a short link can follow it.

The admin UI

I went with a retro terminal aesthetic — green on black, monospace font, minimal chrome. You log in, paste a URL, optionally set a custom slug, and hit shorten. All your links are listed below with click counts and one-click copy.

Deployment

The whole deploy is three commands:

# Create KV namespace
npx wrangler kv namespace create URLS

# Deploy the Worker
cd worker && npx wrangler deploy

# Deploy the frontend
npx wrangler pages deploy site/ --project-name url-shortener

Then add a CNAME record for go pointing to the Pages project, and Cloudflare handles SSL automatically.

What I’d add next

  • Expiring links — KV supports TTL natively, so time-limited links would be trivial
  • QR code generation — useful for physical media
  • Bulk import — for migrating existing links from other services

Cost

Zero. Cloudflare Workers free tier gives you 100,000 requests per day. KV free tier includes 100,000 reads and 1,000 writes per day. For a personal shortener, you’d never hit these limits.

The source is on GitHub if you want to fork it for your own domain.