A tiny, self-hosted link shortener you deploy to Cloudflare in one click.
Short links on your own domain, a clean dashboard you'd actually enjoy using, and simple analytics, with no server to run, no Docker, and no database to provision. It lives entirely on Cloudflare's free tier.
- Create short links. Auto-generated or your own custom slug
- Fast redirects at Cloudflare's edge, counted without slowing anyone down
- Simple analytics per link: clicks over time, plus top countries, referrers, devices and browsers. Geolocation is free from Cloudflare's edge (no MaxMind, no API token)
- Link expiry. Set a date and the link stops working after it (falling back to your
ROOT_URLor a 404) - Targeting. Send different visitors to different URLs with per-link rules: by platform (iOS → App Store, Android → Play, desktop → your site) or by country (US → us-site). First match wins; everyone else gets the default. Geo is free from Cloudflare's edge
- Query passthrough. Optionally forward
?utm_source=…and friends from the short link straight onto the destination - UTM builder. Compose
utm_*tags onto a destination with a live preview as you type - 301 or 302 per link: permanent (cached hard by browsers) or temporary, your call
- Referrer hiding. Optionally send visitors through a
no-referrerhop so the destination can't see where they came from - QR code for every link, ready to download
- Social previews. Set a title, description and image so shared links look right on Twitter, Slack, Discord, etc.
- CSV export. Download all your links, or one link's full clickstream, any time. Your data, no lock-in
- HTTP API. Create, list and delete links from a script or another app (opt-in with a token). See below
- Installable. The dashboard is a PWA, so you can add it to your phone's home screen and it works like an app
- One password protects your dashboard. No accounts, no user table
- Your domain. Point
go.yourbrand.comat it and you're done - Bring nothing. Cloudflare Workers + D1 (SQLite), no build step, no Redis, no Postgres
It deliberately does less than the big shorteners. No teams, no affiliate programs, no A/B tests. Just the short-link loop, done well.
Clicking Deploy to Cloudflare hands the whole setup to Cloudflare. Here is exactly what happens, and the little you do.
- Sign in to Cloudflare. The button takes you there. A free account is all you need.
- Let it connect to your GitHub. Cloudflare copies foogl into a brand-new repository on your GitHub account. That copy is yours to keep and redeploy. Authorize it when asked.
- Set your dashboard password. On the single setup page there is a field for
SITE_PASSWORD. This is the only thing guarding your dashboard, so make it a strong one. (The Worker name and database name are pre-filled. The defaults are fine.) - Click deploy and wait a minute or two. Cloudflare now does the rest for you: it creates your database, builds the tables (runs the migrations), and publishes the Worker. You never touch a command line.
- Open your dashboard. When it finishes, Cloudflare shows your new Worker with an address like
https://foogl-abc123.your-name.workers.dev. Click Visit (or find it later under Workers & Pages → your worker → Visit). That page is your dashboard. - Sign in and make your first link. Enter the password from step 3, paste a long URL, and hit Shorten. It is live instantly at
your-worker.workers.dev/your-slug.
That is the whole thing. No servers, no database to run, nothing to keep patched.
Landed on a "finish setting up" screen, or never got asked for a password? You skipped step 3. The screen tells you how: open your Worker, go to Settings → Variables and Secrets, add a Secret named
SITE_PASSWORD, click Deploy, then reload and sign in.
Your links work on the workers.dev address right away, but you will want them on your own short domain. foogl decides what to serve by hostname, which keeps your short domain clean:
| Address | What it serves |
|---|---|
go.yourbrand.com (your short domain) |
Links only. …/slug redirects; the bare domain sends people to your main site. The dashboard is never exposed here. |
app.go.yourbrand.com |
Your dashboard (sign in, manage links). |
your-worker.workers.dev |
Also the dashboard, so you can work before any custom domains exist. |
To set this up: open your Worker, go to Settings → Domains & Routes, and Add two custom domains: your short domain (e.g. go.yourbrand.com) and app. plus that domain (e.g. app.go.yourbrand.com). Cloudflare provisions the DNS and SSL for you. The dashboard on the app. host automatically shows and copies your links on the short domain, so there is nothing else to wire up.
Good to know: the dashboard is served only on an app. host, on your *.workers.dev URL, or on localhost. Keep the *.workers.dev URL enabled (it is by default) and it is always a working way in, even before you add custom domains. In the rare case your short domain itself starts with app. (e.g. app.example), set a LINK_HOST variable to that domain so the dashboard shows the right short URLs.
Signed in, the Settings link (top-right) runs everything else, no Cloudflare trip needed:
- Main site the bare domain and any mistyped link redirect to.
- Default 301 or 302 for new links.
- HTTP API on or off, with a token you generate here. See below.
- Password change it whenever you like.
(Prefer environment variables? ROOT_URL, API_TOKEN and LINK_HOST still work as fallbacks, set under the Worker's Settings → Variables. A value saved in the dashboard wins over the variable.)
git clone https://github.com/duskresearch/foogl.git
cd foogl
npm install
# create the local database tables
npm run db:migrate:local
# set a local dashboard password
cp .dev.vars.example .dev.vars # then edit SITE_PASSWORD
npm run dev # http://localhost:8787npx wrangler login
npm run db:create # creates the remote D1, prints a database_id
# paste that id into wrangler.jsonc under d1_databases
npm run db:migrate:remote # apply the schema to the remote DB
npx wrangler secret put SITE_PASSWORD
npm run deployThe API is off by default. To turn it on, set an API_TOKEN variable (Worker → Settings → Variables, or npx wrangler secret put API_TOKEN). Use a long random value, e.g. openssl rand -hex 24. Every request sends it as a bearer token:
Authorization: Bearer <API_TOKEN>
Base path is /api/v1. All responses are JSON.
| Method | Path | Does |
|---|---|---|
POST |
/api/v1/links |
Create a link |
GET |
/api/v1/links |
List every link |
GET |
/api/v1/links/:slug |
Fetch one link (includes its click count) |
DELETE |
/api/v1/links/:slug |
Delete a link |
Create a link:
curl -X POST https://your-worker.workers.dev/api/v1/links \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/a-very-long-link",
"slug": "launch", // optional, omit for a random one
"expires_at": "2030-12-31", // optional, YYYY-MM-DD
"passthrough": true, // optional, forward ?query to the destination
"permanent": false, // optional, true = 301, false (default) = 302
"hide_referrer": false, // optional, hop through a no-referrer page
"rules": [ // optional, targeting; first match wins
{ "type": "platform", "match": "ios", "url": "https://apps.apple.com/app/id123" },
{ "type": "platform", "match": "android", "url": "https://play.google.com/store/apps/details?id=x" },
{ "type": "country", "match": "US", "url": "https://example.com/us" }
]
}'rules[].type is platform (match mobile/desktop/tablet/ios/android) or country (a 2-letter code). Returns the created link:
{
"slug": "launch",
"url": "https://example.com/a-very-long-link",
"short_url": "https://your-worker.workers.dev/launch",
"clicks": 0,
"created_at": "2026-07-11 21:13:15",
"expires_at": "2030-12-31",
"passthrough": true,
"permanent": false,
"hide_referrer": false,
"rules": [ { "type": "platform", "match": "ios", "url": "https://apps.apple.com/app/id123" } ],
"og": { "title": null, "description": null, "image": null }
}Errors come back as { "error": "..." } with a matching HTTP status: 401 (bad or missing token), 503 (API_TOKEN not set), 400 (e.g. badurl, taken, badslug, badexp), 404 (unknown slug).
- Cloudflare Workers for the runtime (edge, generous free tier)
- Hono, a tiny, fast web framework
- D1, Cloudflare's built-in SQLite; holds your links and clicks
- No build step. The dashboard is server-rendered HTML with hand-written CSS
The repo ships with the marketing landing page for foo.gl, which only ever renders on the foo.gl host, so on your own deployment it never shows. If you want it out of your fork entirely, delete src/landing-assets.ts and remove the if (hostname === 'foo.gl') / www.foo.gl branch in the host-routing middleware at the top of src/index.ts. The shortener and dashboard are unaffected.
MIT. See LICENSE and do what you like with it.
The name's a wink at goo.gl. The rest is yours to run. Built by Dusk Research.