A fast, edge-deployed link shortening service built with Hono on Cloudflare Workers.
- Custom short paths — define your own paths or let them auto-generate (
/abc12,/your-brand/campaign) - Namespaces — organize links under a namespace prefix
- QR codes — append
/qrto any short link for SVG, PNG, or HTML output - Analytics — per-link redirect stats with bot detection, grouped by hour or day
- Link expiration — optional TTL-based expiry with automatic cleanup
- Slack integration — slash commands, bot notifications on link create/update, interactive buttons for stats and details
- Webhooks — receive POST notifications on link create/update/delete with HMAC signing, works with Zapier, Make, and custom integrations
- OpenAPI docs — interactive API reference at
/ - No propagation delay — links work immediately after creation, even across regions (see Architecture)
Click the button above to deploy to Cloudflare. The deploy flow will automatically create KV, D1, and Analytics Engine resources and prompt you for secrets.
npm install
npm run generate-types
cp .dev.vars.example .dev.vars # fill in your valuesApply D1 migrations locally and start the dev server:
npx wrangler d1 migrations apply ishere --local
npm run dev| Method | Path | Description |
|---|---|---|
POST |
/api/link |
Create short link |
GET |
/api/link/:id |
Get short link |
PATCH |
/api/link/:id |
Update short link |
DELETE |
/api/link/:id |
Delete short link |
GET |
/api/link/:id/stats/:groupBy |
Get link stats (day/hour) |
| Method | Path | Description |
|---|---|---|
GET |
/:id |
Redirect to destination |
GET |
/:namespace/:shortPath |
Redirect (namespaced) |
GET |
/:id/qr |
QR code for short link |
GET |
/:namespace/:shortPath/qr |
QR code (namespaced) |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/health |
No | Health check |
POST |
/api/slack/command |
No | Slack slash command |
POST |
/api/slack/interact |
No | Slack interactive messages |
GET |
/ |
No | Interactive API reference |
GET |
/openapi.json |
No | OpenAPI spec |
Auth is via the Authorization: Bearer <token> header.
Create a short link:
curl https://your-domain/api/link \
--request POST \
--header 'Authorization: Bearer yourapikey' \
--json '{ "destinationUrl": "https://example.com" }'Create with custom namespace and path:
curl https://your-domain/api/link \
--request POST \
--header 'Authorization: Bearer yourapikey' \
--json '{ "destinationUrl": "https://example.com", "namespace": "brand", "shortPath": "campaign" }'Update a link:
curl https://your-domain/api/link/abc12 \
--request PATCH \
--header 'Authorization: Bearer yourapikey' \
--json '{ "destinationUrl": "https://new-url.com" }'Delete a link:
curl https://your-domain/api/link/abc12 \
--request DELETE \
--header 'Authorization: Bearer yourapikey'Get stats:
curl https://your-domain/api/link/abc12/stats/day \
--header 'Authorization: Bearer yourapikey'| Variable | Required | Description |
|---|---|---|
API_KEY |
Yes | Secret key for authenticating API requests |
ANALYTICS_API_TOKEN |
Yes | Cloudflare API token for querying Analytics Engine |
ACCOUNT_ID |
Yes | Your Cloudflare Account ID |
DEFAULT_SHORT_PATH_LENGTH |
No | Length of auto-generated short paths (default: 5) |
MAX_SHORT_ID_RETRIES |
No | Max retries on ID collision (default: 5) |
SLACK_BOT_TOKEN |
No | Slack Bot User OAuth Token (xoxb-...) for notifications |
SLACK_CHANNEL_ID |
No | Slack channel ID (e.g. C01AB2CDE3F) for link-change notifications |
SLACK_SIGNING_SECRET |
No | Slack signing secret for verifying interactive messages |
WEBHOOK_URL |
No | URL to receive POST notifications on link create/update/delete |
WEBHOOK_SECRET |
No | HMAC-SHA256 signing secret for webhook payloads (sent in X-Webhook-Signature-256 header) |
Set secrets locally in .dev.vars and via wrangler secret put for deployed environments.
The Slack integration lets you create, look up, and manage short links via a slash command and global shortcuts, with optional bot notifications when links are created or updated.
Go to api.slack.com/apps and click Create New App > From an app manifest. Select your workspace, then paste the contents of slack-app-manifest.yaml. Before creating, replace the two placeholder URLs with your actual worker URL and API key:
https://<your-worker>/api/slack/commandhttps://<your-worker>/api/slack/interact
This configures the slash command, interactivity, global shortcuts, and bot scopes in one step.
Manual setup (without manifest)
Under Slash Commands > Create New Command:
- Command:
/ishere - Request URL:
https://<your-worker>/api/slack/command
The API key is passed as a query parameter because Slack doesn't send custom auth headers with slash commands.
Under Interactivity & Shortcuts, toggle Interactivity on and set the Request URL to:
https://<your-worker>/api/slack/interact
Also add two Global Shortcuts:
| Name | Callback ID |
|---|---|
| Create short link | create_link |
| Look up link | look_up_link |
Under OAuth & Permissions > Bot Token Scopes, add:
chat:write— for posting link-change notifications to a channelchat:write.public— for posting to channels the bot hasn't been invited tocommands— for slash commands and global shortcuts
Install the app to your workspace, then set the following secrets (via wrangler secret put or .dev.vars locally):
| Secret | Where to find it |
|---|---|
SLACK_SIGNING_SECRET |
Basic Information > Signing Secret |
SLACK_BOT_TOKEN |
OAuth & Permissions > Bot User OAuth Token (xoxb-...) |
SLACK_CHANNEL_ID |
Channel ID of the channel the bot should post notifications to |
SLACK_SIGNING_SECRET is required for the slash command and interaction endpoints. SLACK_BOT_TOKEN and SLACK_CHANNEL_ID are optional — if either is unset, bot notifications for link changes (created/updated/deleted) are silently skipped. To find a channel ID, right-click the channel in Slack > View channel details — the ID is at the bottom of the panel.
Finally, invite the bot to your notification channel: /invite @IsHere
/ishere help
/ishere create <url>
/ishere create <namespace> <url>
/ishere create <namespace> <shortPath> <url>
/ishere update <id> <url>
/ishere get <id>
/ishere details <id>
/ishere stats <id>
Routes (src/routes/) → Actions (src/actions/) → KV (src/kv/) + D1 (src/db/)
- Routes define endpoints with Zod schema validation via
@hono/zod-openapi - Actions contain business logic, decoupled from HTTP concerns
- D1 is the source of truth; KV serves as a global edge cache for fast reads
- Reads try KV first, falling back to D1 on cache miss — this means links are available immediately after creation, avoiding the ~60s propagation delay that KV-only link shorteners suffer from
- Analytics are tracked via Cloudflare Analytics Engine on each redirect
- A cron trigger runs hourly to clean up expired links
npm test # watch mode
npx vitest run # single run
npx vitest run test/api/links/create-link.spec.ts # single fileTests use @cloudflare/vitest-pool-workers with local KV and D1 bindings.
Deploy via the Cloudflare Deploy Button or manually:
npm run deployThis runs D1 migrations and deploys the worker. Set your custom domain in the Cloudflare dashboard after deploying.