A production-ready, self-hosted URL shortener built with Cloudflare Workers and KV storage. Zero cost, global edge performance, and full CRUD admin API with click analytics.
π Full tutorial: Building a Self-Hosted URL Shortener with Cloudflare Workers
- Short URL redirects β
/s/{code}β full URL with automatic UTM parameter injection - Click analytics β per-source tracking (twitter, linkedin, direct, etc.)
- Full CRUD API β create, list, update, soft-delete, restore, permanent-delete
- API key authentication β Bearer token for all write operations
- Campaign tagging β organize URLs by campaign for GA4 segmentation
- Notes field β admin annotations per URL
- Soft delete + restore β data is preserved until you permanently remove it
- CORS support β works with any frontend admin dashboard
- Zero cost β fits within Cloudflare's free tier (100k requests/day)
User β /s/{code} β Cloudflare Worker (edge, <15ms) β KV lookup β 302 redirect + UTM
β
Stats tracking (async)
git clone https://github.com/IAMDevBox/cloudflare-url-shortener.git
cd cloudflare-url-shortener
npm installwrangler kv:namespace create URL_MAPPINGS
# Copy the id from the outputReplace the placeholders:
[[kv_namespaces]]
binding = "URL_MAPPINGS"
id = "YOUR_KV_NAMESPACE_ID" # from step 2
[[routes]]
pattern = "yourdomain.com/s/*"
zone_name = "yourdomain.com"wrangler secret put API_KEY
# Enter a strong secret when promptedwrangler deployThat's it β your URL shortener is live at https://yourdomain.com/s/*.
All write endpoints require Authorization: Bearer YOUR_API_KEY.
curl -X POST https://yourdomain.com/api/shorten \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/very/long/blog/post/",
"code": "oauth", # optional custom code (auto-generated if omitted)
"campaign": "blog_post", # UTM campaign tag
"notes": "OAuth guide" # optional admin note
}'Response:
{
"shortUrl": "https://yourdomain.com/s/oauth",
"code": "oauth",
"longUrl": "https://example.com/very/long/blog/post/"
}curl https://yourdomain.com/api/stats/oauthResponse:
{
"total": 142,
"sources": {
"twitter": 98,
"linkedin": 32,
"direct": 12
},
"lastAccess": "2025-11-27T10:30:00Z"
}curl https://yourdomain.com/api/urls \
-H "Authorization: Bearer YOUR_API_KEY"curl -X PUT https://yourdomain.com/api/urls/oauth \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/new-url/", "campaign": "updated"}'curl -X DELETE https://yourdomain.com/api/urls/oauth \
-H "Authorization: Bearer YOUR_API_KEY"curl -X POST https://yourdomain.com/api/urls/oauth/restore \
-H "Authorization: Bearer YOUR_API_KEY"# Must soft-delete first, then permanently remove
curl -X DELETE https://yourdomain.com/api/urls/oauth/permanent \
-H "Authorization: Bearer YOUR_API_KEY"The included client.py provides a typed Python wrapper:
from client import URLShortenerClient
client = URLShortenerClient() # reads SHORTENER_API_KEY + SHORTENER_BASE_URL from env
# Create
short = client.create("https://example.com/post/", campaign="twitter")
# Analytics
stats = client.get_stats("oauth")
print(stats["total"]) # 142
# List all
for url in client.list_urls():
print(f"/s/{url['code']} β {url['url']}")Environment variables:
export SHORTENER_API_KEY=your-secret-key
export SHORTENER_BASE_URL=https://yourdomain.comRun the full integration test suite against a local dev instance:
# Terminal 1: start local worker
npx wrangler dev --local
# Terminal 2: run tests
API_KEY=test-key node test_worker.jsOr against your deployed worker:
BASE_URL=https://yourdomain.com API_KEY=your-key node test_worker.jsEvery redirect automatically injects UTM parameters:
/s/oauth?s=twitter
β
https://example.com/post/?utm_source=twitter&utm_medium=shortlink&utm_campaign=blog_post&utm_content=oauth
The ?s= query parameter sets utm_source. Omit it to get utm_source=short (default).
Cloudflare Workers free tier per day:
- 100,000 Worker requests
- 1GB KV storage
- 100,000 KV reads + 1,000 KV writes
A typical blog gets ~500 short URL clicks/day β well within free tier.
| Service | Monthly cost |
|---|---|
| Bitly Pro | $29 |
| TinyURL Pro | $9.99 |
| This solution | $0 |
- Building a Self-Hosted URL Shortener with Cloudflare Workers β full tutorial on IAMDevBox
- Cloudflare Workers KV Tutorial β KV storage configuration deep-dive
- OAuth 2.0 Developer Guide β if you're adding OAuth authentication to your Workers
- IAM Tools Comparison β comparing identity and access management platforms
- Cloudflare Workers Docs β official Cloudflare documentation
MIT β use freely in your own projects.