---
title: "Sink Install: Free Self-Hosted Link Shortener (2026 Guide)"
description: "Deploy Sink, a free open-source link shortener with analytics, on Cloudflare Workers. Step-by-step guide with D1 database, KV cache, and AI-powered slug generation."
date: 2026-08-06
categories: ["self-hosting"]
tags: ["self-hosted","cloudflare","link-shortener"]
---

import YouTubeEmbed from "../../components/widgets/YouTubeEmbed.astro";
import Button from "../../components/widgets/Button.astro";
import Notice from "../../components/widgets/Notice.astro";
import ListCheck from "../../components/widgets/ListCheck.astro";
import Accordion from "../../components/widgets/Accordion.astro";
import { Picture } from "astro:assets";
import img1 from "../../assets/images/24/09/ai-analytics.png";
import img2 from "../../assets/images/24/09/sink-ui.png";

[Sink](https://github.com/miantiao-me/Sink) is an open-source, free self-hosted link shortener that runs entirely on Cloudflare's edge network. Originally created by ccbikai, the project has moved to the `miantiao-me` GitHub organization and now has around 7,000 stars. With v0.3.0, the storage layer switched from KV to Cloudflare D1 as the primary database, a breaking change that makes the old installation guide outdated. This article covers deploying Sink on Cloudflare Workers with the current D1-based architecture, updated for 2026.

## What is Sink?

Sink is a self-hosted link shortener that runs on Cloudflare Workers. It uses D1 (Cloudflare's SQL database) for permanent link storage and KV as a write-through read cache for fast redirects. The Cloudflare free tier is enough for personal and small-team use. No VPS, no Docker containers, no server to maintain.

The project is now at v0.3.0 (latest stable: v0.2.11 if you want the pre-D1 version). The repo lives at [github.com/miantiao-me/Sink](https://github.com/miantiao-me/Sink). It's licensed under AGPL-3.0.

## Why choose a free self-hosted link shortener?

Hosted link shorteners like Bitly start at $8/month. Dub.co has a free tier but limits features behind a paywall. Short.io caps you on the free plan. Sink costs nothing if you stay within Cloudflare's free tier limits.

The self-hosting angle matters: you own your data, there's no vendor lock-in, and you get analytics, AI-powered slug generation, and features like password protection and geo-based routing. All included, no upsells.

If you're building a home server with [Docker containers](/docker-containers-home-server), Sink is a lightweight addition to your self-hosted stack, though it runs on Cloudflare, not on your server. For business use, [Sink complements other self-hosted tools](/docker-containers-business) in your stack.

<Notice type="info" title="Don't want to self-host?">
The same author offers [S.EE](https://s.ee), a managed link platform with 180,000+ users, A/B testing, link-in-bio pages, and commercial plans. The Sink README explicitly recommends S.EE for professional/business needs.
</Notice>

## What's new in Sink (2024-2026)

Sink has added a lot since the original version. Here are the highlights:

- **D1 replaces KV as primary database** (v0.3.0): KV is now only a redirect cache
- **Link tags, filtering, and sorting** (v0.3.0)
- **Click webhooks with HMAC signatures** (v0.2.11): Dub-style webhook payloads
- **Cloudflare Access auth** (v0.2.11): alternative to `NUXT_SITE_TOKEN`
- **Duplicate URL detection** (v0.2.11)
- **Link check dashboard** (v0.2.10): batch broken-link detection with CSV export
- **Country/geo-based redirection** (v0.2.9): route visitors by Cloudflare-detected country
- **AI OpenGraph generation** (v0.2.9): generate OG metadata from page content
- **UTM parameter builder** (v0.2.8): built-in with live preview
- **Password protection and link cloaking** (v0.2.6): per-link password gate, full-screen iframe masking
- **Device-based redirection** (v0.2.3): route iOS/Android/Desktop differently
- **OpenGraph customization** (v0.2.3): per-link OG title, description, image
- **Real-time analytics** (v0.2.4+): 3D WebGL globe, 10-second polling event log
- **QR code generation**: built-in for all short links
- **Import/export**: JSON for links, CSV for analytics
- **Multi-language UI**: 10+ languages

The ecosystem has grown too: there's now a Chrome extension ("Sink Quick Shorten"), a Raycast extension, an iOS app, and Apple Shortcuts support.

## Key features

### AI-powered slug generation

Sink uses Workers AI with the `@cf/qwen/qwen3-30b-a3b-fp8` model by default. When creating a link, you can let AI suggest a relevant, catchy slug based on the destination URL. It saves time when you're shortening links in bulk.

### Password protection and link cloaking

Set a per-link password to gate access, useful for sharing internal docs or time-sensitive content. Link cloaking renders the destination inside a full-screen iframe, masking the target URL. Note: cloaking only works on sites that allow iframe embedding.

### Device and geo-based redirection

Route visitors to different URLs based on their device (iOS/Android/Desktop) or country (detected by Cloudflare). This is handy for app store links: send iPhone users to the App Store, Android users to Google Play, desktop users to a landing page.

### Advanced analytics and link health monitoring

Sink tracks clicks with geographic data, referrer information, and device breakdowns rendered on a 3D WebGL globe. The link check dashboard (v0.2.10) batch-verifies destination URL reachability and exports broken links as CSV. You can also export access analytics as CSV.

Sink includes built-in analytics, but if you need website-wide analytics beyond link clicks, [Plausible is a great self-hosted companion](/install-plausible-analytics).

### Webhooks, API, and integrations

Click webhooks (v0.2.11) send Dub-style payloads to a configured URL with optional HMAC signatures. Every Sink instance exposes OpenAPI docs at `/docs/scalar`, making it easy to integrate with automation tools and MCP proxies. The Chrome extension, Raycast extension, iOS app, and Apple Shortcuts integrations make Sink practical for daily use.

## Technologies powering Sink

Sink's tech stack has evolved since the original version:

| Component | Role |
|---|---|
| **Nuxt 4** | Frontend framework (upgraded from Nuxt.js) |
| **Cloudflare D1** | Primary database, stores all links (NEW in v0.3.0) |
| **Cloudflare Workers KV** | Write-through read cache for fast redirects |
| **Drizzle ORM** | Database access layer for D1 |
| **Cloudflare R2** | Optional: automatic backups and social images |
| **Shadcn-vue + Tailwind CSS** | UI components and styling (unchanged) |

<Notice type="info" title="How D1 and KV work together">
D1 stores your links permanently. KV caches them for fast redirects. Both are required. When you create a link, it writes to D1 first, then to KV. Redirects read from KV (fast) and fall back to D1 if the cache misses. This is a different architecture from the original article where KV was the only storage.
</Notice>

## Deploy Sink on Cloudflare Workers (recommended)

Cloudflare Workers is the recommended deployment method. Pages still works but is deprecated. This section walks through the full setup.

<YouTubeEmbed
  url="https://www.youtube.com/embed/MkU23U2VE9E"
  label="Sink Install"
/>

<Notice type="warning" title="Video may be outdated">
The video above was recorded before the D1 migration. Follow the written steps below for the current deployment process.
</Notice>

### Prerequisites

<ListCheck>

- Cloudflare account (free tier is fine)
- GitHub account (to fork the repository)
- A domain added to Cloudflare (for custom short URLs)
- Cloudflare API token with Account > Account Analytics > Read permission (create at Profile > API Tokens)

</ListCheck>

### Step 1: Fork the repository

Go to [github.com/miantiao-me/Sink](https://github.com/miantiao-me/Sink) and click **Fork**. This creates your own copy under your GitHub account.

If you previously forked `ccbikai/Sink`, GitHub redirects still work, but update your bookmarks and any CI references to the new URL.

### Step 2: Create a Cloudflare D1 database

<Notice type="warning" title="This step is new since v0.3.0">
Without a D1 database, Sink cannot store links. This didn't exist when the original article was written.
</Notice>

1. Go to **Cloudflare Dashboard** > **D1** > **Create database**
2. Name it `sink`
3. Copy the **database ID** from the database details page. You'll need it later

### Step 3: Create a KV namespace

1. Go to **Cloudflare Dashboard** > **Workers & Pages** > **KV** > **Create a namespace**
2. Name it `sink`
3. Copy the **namespace ID**. You'll need it in the next step

### Step 4: Deploy with Workers Git integration

1. Go to **Cloudflare Dashboard** > **Workers & Pages** > **Create**
2. Select **Workers** (not Pages)
3. Click **Connect to Git** and authorize GitHub
4. Select your forked Sink repository and the `master` branch
5. Set **Build command**: `pnpm build`
6. Set **Deploy command**: `pnpm deploy:worker`
7. Click **Save and Deploy**

### Step 5: Configure build variables and bindings

After the first deployment, configure two sets of settings.

**Build variables** (Settings > Variables and Secrets):

| Variable | Value |
|---|---|
| `DEPLOY_D1_DATABASE_ID` | Your D1 database ID from Step 2 |
| `DEPLOY_KV_NAMESPACE_ID` | Your KV namespace ID from Step 3 |

**Runtime bindings** (Settings > Bindings):

| Binding Name | Type | Resource |
|---|---|---|
| `DB` | D1 Database | `sink` |
| `KV` | KV Namespace | `sink` |
| `ANALYTICS` | Analytics Engine | Dataset: `sink` |
| `R2` | R2 Bucket | Optional: name: `sink` |
| `AI` | Workers AI | Optional: for AI features |

<Notice type="warning" title="The DB binding is required">
Without the D1 `DB` binding, you'll get errors when creating links. This is the most common deployment mistake since v0.3.0.
</Notice>

**Environment variables** (Settings > Variables and Secrets):

| Variable | Value |
|---|---|
| `NUXT_SITE_TOKEN` | Your admin password (minimum 8 characters) |
| `NUXT_CF_ACCOUNT_ID` | Your Cloudflare account ID (visible in the dashboard URL) |
| `NUXT_CF_API_TOKEN` | API token with Account Analytics Read permission |

<Picture src={img1} alt="Cloudflare Workers bindings configuration for Sink" />

### Step 6: Complete storage setup (first run)

After deployment, you must initialize the database. This is a one-time step.

1. Open `https://your-domain/dashboard`
2. Log in with your `NUXT_SITE_TOKEN`
3. Navigate to **Links**

This triggers the storage initialization. Until you do this, the API returns HTTP 423 (Storage not ready). If you skip this step, link creation will fail.

### Step 7: Add your custom domain

1. Go to **Workers & Pages** > your project > **Settings** > **Domains & Routes**
2. Click **Add custom domain**
3. Enter your domain (e.g., `go.yourdomain.com`)
4. Cloudflare handles DNS and TLS automatically

Verify the domain is active:

```bash
curl -I https://your-domain/test-slug
```

You should get a 301 or 302 redirect (even if the slug doesn't exist yet, the worker is responding).

<Picture src={img2} alt="Sink dashboard showing the link management UI" />

## Deploy Sink on Cloudflare Pages (deprecated)

<Notice type="error" title="Cloudflare Pages is deprecated">
Use Workers (above) for new installations. The steps below are for existing Pages users who want to upgrade.
</Notice>

Pages deployment still works but requires extra configuration:

1. Enable the `nodejs_compat` compatibility flag in your Pages project settings (this wasn't required before)
2. Add `CLOUDFLARE_API_TOKEN` with D1 Edit permission and `CLOUDFLARE_ACCOUNT_ID` as environment variables. The `postbuild` script runs D1 migration automatically on `master` branch builds
3. All other bindings (DB, KV, ANALYTICS, R2, AI) still apply

Already hosting an [Astro blog on Cloudflare](/deploy-astrojs-cloudflare)? Sink fits alongside your existing Cloudflare projects, though Workers is the simpler path now.

## Post-deployment: verification and troubleshooting

### Verify your deployment

<ListCheck>

- Open `https://your-domain/dashboard` — should show the login screen
- Log in with `NUXT_SITE_TOKEN`
- Create a test short link — verify the redirect works by visiting it in a browser
- Check the analytics page — visit your test link from a different device/browser, confirm the click appears
- Visit `/docs/scalar` — the API documentation should be accessible

</ListCheck>

### Common issues and fixes

<Accordion label="I can't create short links / HTTP 423 errors" group="troubleshooting">

**Cause:** Storage not initialized, or the `DB` and `KV` bindings are missing.

**Fix:** Open your Sink dashboard, navigate to Links once to trigger initialization. If that doesn't help, check that both `DB` (D1) and `KV` bindings are configured in Workers settings. Redeploy after adding bindings.

</Accordion>

<Accordion label="Analytics is empty" group="troubleshooting">

**Cause:** Missing `ANALYTICS` binding, wrong dataset name, or API token lacks the right permission.

**Fix:** Ensure the `ANALYTICS` binding exists with dataset name `sink`. Verify your `NUXT_CF_API_TOKEN` has "Account > Account Analytics > Read" permission. Redeploy after changes.

</Accordion>

<Accordion label="Custom slugs lose uppercase letters" group="troubleshooting">

**Cause:** Slugs are case-insensitive by default.

**Fix:** Set `NUXT_CASE_SENSITIVE=true` in environment variables and redeploy. Note that this means `Go/Link` and `go/link` will be different URLs.

</Accordion>

<Accordion label="Cloaked page shows blank" group="troubleshooting">

**Cause:** The target site blocks iframe embedding via `X-Frame-Options` or `Content-Security-Policy` headers.

**Fix:** Cloaking only works on sites that allow being embedded. Most major sites (Google, GitHub) block it. Test with a site you control.

</Accordion>

<Accordion label="Redirects show the old destination" group="troubleshooting">

**Cause:** Browser or CDN caching the redirect response.

**Fix:** Set `NUXT_REDIRECT_NO_STORE=true` in environment variables to prevent caching. You can also adjust `NUXT_LINK_CACHE_TTL` (default: 60 seconds) to control KV cache duration.

</Accordion>

<Accordion label="Migrating from pre-v0.3.0 (KV-only) to D1" group="troubleshooting">

**Cause:** Upgrading from a version that stored links only in KV.

**Fix:** Deploy v0.3.0 with your original KV namespace still bound. Then open Dashboard > Links — this triggers the KV-to-D1 migration automatically. The API returns HTTP 423 until migration completes. Existing links continue working during migration as long as the original KV namespace stays bound.

</Accordion>

## Advanced configuration options

Sink exposes many environment variables for fine-tuning behavior. Changing any of these requires a redeploy on Workers.

<Accordion label="Environment variables reference" group="config">

| Variable | Default | Purpose |
|---|---|---|
| `NUXT_HOME_URL` | empty | Redirect `/` to a URL |
| `NUXT_NOT_FOUND_REDIRECT` | empty | Custom 404 destination |
| `NUXT_REDIRECT_STATUS_CODE` | `301` | Redirect HTTP code (301/302/307/308) |
| `NUXT_LINK_CACHE_TTL` | `60` | KV cache TTL in seconds |
| `NUXT_REDIRECT_WITH_QUERY` | `false` | Forward visitor query params to destination |
| `NUXT_CASE_SENSITIVE` | `false` | Preserve slug case |
| `NUXT_DISABLE_BOT_ACCESS_LOG` | `false` | Drop bot traffic from analytics |
| `NUXT_DISABLE_AUTO_BACKUP` | `false` | Disable scheduled R2 backups |
| `NUXT_AI_MODEL` | `@cf/qwen/qwen3-30b-a3b-fp8` | Workers AI model for slug suggestions |
| `NUXT_WEBHOOK_URL` | empty | Click webhook URL |
| `NUXT_WEBHOOK_SECRET` | empty | Webhook HMAC secret (`whsec_` prefix) |
| `NUXT_CF_ACCESS_TEAM_DOMAIN` | empty | Cloudflare Access team domain |
| `NUXT_CF_ACCESS_AUD` | empty | Cloudflare Access AUD |
| `NUXT_PUBLIC_SLUG_DEFAULT_LENGTH` | `6` | Auto-generated slug length |
| `NUXT_API_CORS` | empty | Enable CORS for `/api/**` |

Full reference: [docs.sink.cool/configuration](https://docs.sink.cool/configuration/)

To undo a change: remove the variable and redeploy.

</Accordion>

## Backups and production notes

If you configured the R2 binding (Step 5), Sink runs automatic backups on a schedule. Disable with `NUXT_DISABLE_AUTO_BACKUP=true`. For broader R2 backup strategies, see how to [configure Dokploy backups with Cloudflare R2](/dokploy-backups-cloudflare-r2) — the R2 setup process is similar.

**Cloudflare free tier limits (relevant to Sink):**

| Product | Free limit | Notes |
|---|---|---|
| Workers requests | 100,000/day | Resets at 00:00 UTC |
| D1 rows read | 5 million/day | Resets at 00:00 UTC |
| D1 rows written | 100,000/day | Resets at 00:00 UTC |
| D1 storage | 5 GB total | Sum of all databases on your account |
| KV reads | 100,000/day | |
| KV writes | 1,000/day | |
| R2 storage | 10 GB | |

<Notice type="info" title="Cost summary">
Sink runs free on Cloudflare's free tier for personal use. Heavy traffic (thousands of links, millions of clicks) may require the Workers Paid plan at $5/month, with D1 overage at $0.001 per million rows read. For most people shortening links for social media or email campaigns, the free tier is more than enough.
</Notice>

Every Sink instance also exposes OpenAPI docs at `/docs/scalar`, which is useful for automation and [AI agent integration](/ai-docker-deploy-skill).

## Sink vs alternatives

| Feature | Sink | Dub.co | Bitly | Short.io |
|---|---|---|---|---|
| Self-hosted | Yes | Yes (open-core) | No | No |
| Cost | Free (Cloudflare) | Free tier + paid | From $8/mo | Free tier + paid |
| AI slug generation | Yes | No | No | No |
| Geo/device routing | Yes | Yes | Yes (paid) | Yes (paid) |
| Password protection | Yes | No | No | No |
| Cloudflare-native | Yes | No | No | No |

Sink's main advantage is that it runs entirely on Cloudflare's free tier with no server to manage. If you're managing a broader self-hosted setup, you can [compare the best server panels](/best-self-hosted-panels) or check out [Coolify](/coolify-install-heroku-alternative) as a self-hosted PaaS. If you use [Dokploy for self-hosting](/dokploy-install), Sink is one more service you can run alongside — though on Cloudflare rather than your VPS.

## Extending Sink: apps and integrations

Sink has several clients and integrations:

- **Chrome Extension**: "Sink Quick Shorten" lets you right-click any URL to shorten it (Chrome Web Store)
- **Raycast Extension**: "Sink Short Links Manager" for macOS power users (Raycast Store)
- **iOS App**: Native iPhone app on the App Store
- **Apple Shortcuts**: macOS and iOS automation integration
- **API**: OpenAPI docs at `/docs/scalar`, MCP support via OpenAPI proxy

These make Sink far more practical than a dashboard-only tool. You can shorten links from your browser, phone, or terminal without opening the dashboard.

## Conclusion

Sink started as a simple Cloudflare KV-based shortener and is now a full link management platform. The v0.3.0 D1 migration makes it a proper database-backed application while keeping the zero-server, zero-cost deployment model. Password protection, geo routing, webhooks, and the extension ecosystem (Chrome, Raycast, iOS, Apple Shortcuts) make it a solid choice for anyone who wants link shortening without monthly fees or vendor lock-in.

If you're building out your self-hosted stack, Sink pairs well with [Docker containers for your home server](/docker-containers-home-server) — one runs on your infrastructure, the other runs free on Cloudflare's edge.

<Button text="View Sink on GitHub" link="https://github.com/miantiao-me/Sink" variant="solid" color="blue" size="md" icon="arrow-right" />