An edge URL shortener.
A single-user URL shortener that runs on Cloudflare Workers (Hono + D1), with a Chrome-compatible browser extension. MIT licensed.
No secrets live in this repo. Every credential, including your short-link hostname, is set at deploy time through Wrangler Secrets or the Cloudflare dashboard.
- Redirects with 302, so disabling or expiring a link takes effect right away, and counts clicks without storing IPs or per-visitor logs
- Exposes a REST API at
/api/links. The worker verifies either a scoped Bearer token or a Cloudflare Access JWT; put Access in front of/apiand clients (browser dashboard, extension via an Access service token) authenticate through it - Checks every destination before saving it: scheme allowlist, normalization, private/internal-host (SSRF) blocking, a self-reference block, and a pluggable reputation lookup (Google Safe Browsing by default)
- Supports links that expire on a date or self-destruct after N clicks
- Ships an admin dashboard at
/admin - Generates a QR code for any short link — in the dashboard (a
GET /api/links/:slug/qrSVG endpoint) and in the extension popup (rendered client-side)
The dashboard at /admin (behind Cloudflare Access) lists your links and creates new ones:
- Paste a destination URL
- Optional custom slug
- Shorten it
- Search by slug or destination
- Hide inactive links (expired / disabled / used up)
- Delete all inactive links in one go
- Copy the short link
- Show its QR code
- Delete the link
Links that have expired, been disabled, or used up their one-time click stay in the
database — expiry is a soft check (the redirect returns 410 Gone; the row isn't
deleted). The dashboard flags them with a status badge and a muted, struck-through
row, and adds a Hide inactive toggle plus a Delete inactive button to clear
them out in bulk. The per-row Delete still removes a single link.
shared/: framework-free validation (Zod schemas, slug rules, URL safety), used by both the worker and the extensionworker/: the Cloudflare Worker (Hono routes, D1 access, auth, and the safety pipeline)extension/: cross-browser (Chrome + Firefox) MV3 extension — see extension/README.md
- Node.js v20+
- A Cloudflare account (free plan works)
- Wrangler CLI (
npm install -g wrangleror usenpx) - A domain on Cloudflare (for your short-link hostname)
npm install
cp worker/.dev.vars.example worker/.dev.vars # fill in API_TOKEN, SHORT_DOMAIN, etc.
npm --workspace worker run migrate:local
npm --workspace worker run dev
npm testSee the full deploy guide for step-by-step instructions covering:
- Configuring
wrangler.toml(replace the D1 database ID placeholder) - Setting up the D1 database
- Configuring secrets (API_TOKEN, SHORT_DOMAIN, SAFE_BROWSING_API_KEY)
- Connecting your custom domain
- Locking down
/adminand/apiwith Cloudflare Access
Quick start:
# 1. Create the D1 database
npx wrangler d1 create url_shortener
# 2. Copy the printed database_id into worker/wrangler.toml
# 3. Run migrations against the remote database
npm --workspace worker run migrate:remote
# 4. Set the API token secret
npx wrangler secret put API_TOKEN --config worker/wrangler.toml
# 5. Set the short domain secret
npx wrangler secret put SHORT_DOMAIN --config worker/wrangler.toml
# 6. Deploy the worker
npm run deploy- Architecture — system diagram, request flows, package breakdown
- Deploy guide — full setup, secrets, domain, and Cloudflare Access
- Cost — free tier limits and pricing breakdown
- Extension — browser extension setup
- Link search uses
LIKE '%term%'(a full table scan) on purpose — at single-user scale the table is small and the scan is sub-millisecond. If it ever grows large, switch to an FTS5trigramvirtual table kept in sync with triggers (mind the 3-character minimum and D1's no-export-for-virtual-tables caveat).
MIT. See LICENSE.
