Back to Work
2026Full-Stack Web Application

Link Shortener - Self-Hosted URL Shortener

Link Shortener - Self-Hosted URL Shortener
About

A self-hosted URL shortener built on the Cloudflare stack with Vue 3, Pages Functions, and D1. Features an admin dashboard, click analytics, custom slugs, and edge-powered redirects from 300+ locations worldwide — all on the free tier.

Technologies
Vue 3TypeScriptTailwind CSS v4Cloudflare PagesCloudflare D1Cloudflare AccessWrangler
Live ProjectVisit Website

Link Shortener - Self-Hosted URL Shortener

Link Shortener is a fully self-hosted URL shortening service built entirely on Cloudflare's free tier. I created this project to own my short links end-to-end — custom domain, analytics, zero recurring cost. The entire stack runs on Cloudflare Pages Functions with a D1 SQLite database at the edge, delivering sub-50ms redirects from over 300 locations worldwide.

Project Overview

Most URL shorteners either lock you into someone else's domain (bit.ly, s.id) or charge monthly fees for features like analytics and custom slugs. I wanted a solution that uses my own domain, tracks every click, and costs nothing to host.

The result is a complete link shortener with an admin dashboard, CRUD API, click tracking, and zero-trust authentication — all deployed with a single npm run deploy command. The entire application runs serverless on Cloudflare's edge network, so there's no server to maintain and no hosting bill to worry about.

Key Features

Admin Dashboard

A responsive single-page application for managing all shortened links:

Link Shortener - Admin Dashboard

  • Link Management: Create, edit, and delete links from a clean table interface.
  • Search & Sort: Filter links by slug or destination URL, sort by date or click count.
  • Quick Copy: One-click clipboard copy for any short URL.
  • Real-Time Data: Dashboard reflects changes immediately without page reload.

Smart Slug Generation

Flexible slug options for different use cases:

Link Shortener - Create New Link

  • Custom Slugs: Create branded links like /promo or /launch-2024.
  • Auto-Generated Slugs: Random 6-character alphanumeric strings for quick links.
  • Validation: Slugs are validated for length (3-50 chars), character set, uniqueness, and reserved path conflicts.
  • Collision Prevention: Auto-generation retries up to 10 times to guarantee uniqueness.

Click Analytics

Per-link tracking with referrer data:

  • Click Count: Running total displayed in the dashboard.
  • Recent Clicks: Last 100 clicks with timestamps and referrer URLs.
  • Atomic Updates: Click count and click record are inserted in a single D1 batch transaction.

Edge-Powered Redirects

The catch-all Pages Function intercepts every request before the SPA:

  • Fast: Redirects execute at the Cloudflare edge, typically under 50ms.
  • Global: Served from 300+ locations worldwide.
  • Smart Routing: Reserved paths (admin, api, assets) pass through to the SPA; everything else is a slug lookup.

Zero Trust Admin Protection

Authentication handled entirely by Cloudflare Access — no auth code in the application:

  • Email OTP: Login via one-time passcode sent to authorized emails.
  • Session Management: Configurable session duration (24h, 1 week, 1 month).
  • Free Tier: Supports up to 50 users on the Cloudflare Access free plan.

Technical Architecture

Frontend (Vue 3)

The admin dashboard is a Vue 3 single-page application with a composables-based architecture:

  • Framework: Vue 3 with Composition API and <script setup> syntax.
  • Routing: Vue Router with two routes — the admin dashboard and a 404 catch-all.
  • State Management: useLinks composable encapsulates all API calls and reactive state.
  • Styling: Tailwind CSS v4 for utility-first responsive design.
  • Build: Vite for fast HMR in development and optimized production builds.

Backend (Cloudflare Pages Functions)

The API is implemented as Cloudflare Pages Functions — file-based routing with zero configuration:

  • functions/[[slug]].ts: Catch-all handler that intercepts every request. Performs slug lookup in D1 and redirects on match, or passes through to the SPA.
  • functions/api/links/index.ts: GET lists all links, POST creates a new link with validation and optional slug generation.
  • functions/api/links/[id].ts: GET returns link details with recent clicks, PUT updates slug or URL, DELETE removes with cascade.
  • functions/_middleware.ts: Request pipeline middleware for the API layer.

Database (Cloudflare D1)

SQLite at the edge with a simple two-table schema:

  • links table: id, slug (unique, indexed), destination_url, created_at, updated_at, click_count.
  • clicks table: id, link_id (foreign key with cascade delete), clicked_at, referrer.
  • Indexes: On slug for fast redirect lookups, link_id and clicked_at for analytics queries.

Infrastructure

  • Hosting: Cloudflare Pages (free tier — 100k requests/day).
  • Database: Cloudflare D1 (free tier — 5GB storage, 5M reads/day).
  • Auth: Cloudflare Access (free tier — 50 users).
  • DNS & SSL: Automatic via Cloudflare.

Implementation Highlights

Atomic Click Tracking

Redirect and analytics recording happen in a single D1 batch to ensure consistency:

await context.env.DB.batch([
  context.env.DB.prepare(
    'UPDATE links SET click_count = click_count + 1 WHERE id = ?'
  ).bind(link.id),
  context.env.DB.prepare(
    'INSERT INTO clicks (link_id, referrer) VALUES (?, ?)'
  ).bind(link.id, referrer),
])

Collision-Safe Slug Generation

Auto-generated slugs use a retry loop to guarantee uniqueness without database-level locking:

function generateSlug(length: number = 6): string {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  let result = ''
  for (let i = 0; i < length; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length))
  }
  return result
}

// Generate with uniqueness check, up to 10 attempts
let attempts = 0
while (attempts < maxAttempts) {
  slug = generateSlug()
  const existing = await context.env.DB.prepare(
    'SELECT id FROM links WHERE slug = ?'
  ).bind(slug).first()
  if (!existing) break
  attempts++
}

Configurable Root Redirect

The root path behavior is controlled by an environment variable, making the same codebase work for different use cases:

if (!path) {
  const rootRedirectUrl = (context.env as any).ROOT_REDIRECT_URL
  if (rootRedirectUrl) {
    return Response.redirect(rootRedirectUrl, 302)
  }
  return context.next() // Serve the SPA
}

Design Philosophy

The project follows a zero-cost, zero-ops philosophy:

  • No Server: Entirely serverless on Cloudflare's edge network. No containers, no VMs, no process managers.
  • No Auth Code: Authentication is delegated to Cloudflare Access at the infrastructure level, keeping the application code clean and simple.
  • No Build Pipeline: Wrangler handles everything — build, deploy, database migrations — from a single CLI.
  • Minimal Dependencies: Only Vue 3, Vue Router, and Tailwind CSS on the frontend. No state management library, no API client, no ORM.

The admin dashboard intentionally uses a composables-based architecture over a full state management solution like Pinia. For a single-view application with one data source, a useLinks composable with reactive refs is simpler and sufficient.

Results

Link Shortener demonstrates that a fully-featured URL shortener — with analytics, an admin UI, and authentication — can run entirely on free infrastructure. The Cloudflare stack eliminates the traditional cost and complexity of hosting a web application: no server provisioning, no database management, no SSL certificates, no auth implementation. The entire project deploys in under 30 seconds and serves globally with edge-level performance. The project is open-source under the MIT license, with comprehensive English documentation covering setup, deployment, custom domains, and Cloudflare Access configuration.

Next ProjectView All Work