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

- Custom Slugs: Create branded links like
/promoor/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:
useLinkscomposable 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:GETlists all links,POSTcreates a new link with validation and optional slug generation.functions/api/links/[id].ts:GETreturns link details with recent clicks,PUTupdates slug or URL,DELETEremoves 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:
linkstable:id,slug(unique, indexed),destination_url,created_at,updated_at,click_count.clickstable:id,link_id(foreign key with cascade delete),clicked_at,referrer.- Indexes: On
slugfor fast redirect lookups,link_idandclicked_atfor 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.