A free, self-hosted alternative to bit.ly and Rebrandly — built on Cloudflare Workers with zero server costs
For full technical specifications and architecture details, see CLAUDE.md. For version history, see CHANGELOG.md.
A lightweight, high-performance edge router and URL shortener built on Cloudflare Workers and the Hono framework. Replace paid link shorteners like bit.ly, Rebrandly, and TinyURL with your own self-hosted solution. Manage URL redirects, reverse proxies, and R2 bucket file serving through a simple API — all configuration stored in Cloudflare KV for instant global propagation across 300+ edge locations.
| Bifrost | bit.ly / Rebrandly | YOURLS | Kutt | |
|---|---|---|---|---|
| Cost | Free (Cloudflare free tier) | $35-$300+/month | Free (self-hosted) | Free (self-hosted) |
| Infrastructure | Serverless (zero servers) | Managed SaaS | PHP + MySQL server | Node.js + Docker |
| Latency | ~30-90ms (edge cached) | ~100-200ms | ~200-500ms | ~100-300ms |
| Global CDN | 300+ Cloudflare locations | Yes | No (single server) | No (single server) |
| Custom domains | Unlimited | 1-10 (plan dependent) | 1 | Unlimited |
| Reverse proxy | Yes | No | No | No |
| R2 file serving | Yes | No | No | No |
| API management | Full REST API + MCP + Slack | REST API | REST API | REST API |
| Setup time | ~15 minutes | Instant (SaaS) | ~30 minutes | ~30 minutes |
- Dynamic Routing — Configure routes via API without redeployment
- Three Route Types:
redirect— URL redirects (301, 302, 307, 308)proxy— Reverse proxy to external URLsr2— Serve content from R2 buckets
- KV-Powered — Route changes propagate globally in seconds
- Admin API — Full CRUD operations with API key authentication, search, and pagination
- Admin Dashboard — React SPA with Command Palette (Cmd+K), filters, analytics, R2 Storage browser with file preview (images, PDFs) and standalone target links
- MCP Server — AI-powered route and R2 storage management via Claude Code/Desktop (22 tools)
- Analytics — D1-powered click and page view tracking
- Wildcard Patterns — Support for path patterns like
/blog/* - R2 Storage Management — Browse, upload, download, rename, move, and delete R2 objects via API and dashboard
- CDN Cache Purge — Purge Cloudflare edge cache globally for R2 objects via Zone Cache Purge API
- Route Domain Transfer — Move routes between domains preserving configuration and audit trail
- R2 Backup System — Automated daily KV route backups with health monitoring (D1 covered by Time Travel)
- API Shield — OpenAPI schema validation at the Cloudflare edge
- Built on Hono — Fast, lightweight, TypeScript-first
- Multi-Domain Routing — Single worker handles multiple custom domains
- Domain-Restricted Admin API — Admin API only accessible from designated domain
- Timing-Safe Auth — API key comparison resistant to timing attacks
- SSRF Protection — Blocks proxy requests to private/internal IPs
- Path Traversal Protection — R2 keys sanitized to prevent directory traversal
- Rate Limiting — Via Cloudflare WAF (Worker middleware available if needed)
bifrost/ # pnpm monorepo
├── src/ # Main edge router Worker
├── shared/ # Shared types, schemas, HTTP client
├── mcp/ # MCP server for AI route management
├── admin/ # React SPA admin dashboard
└── slackbot/ # Slack bot for route management
This repo is designed as a forkable template. Follow these steps to deploy your own instance.
- Node.js >= 24 (see
.nvmrc) - pnpm (
corepack enable && corepack prepare) - A Cloudflare account with Workers enabled (free plan works)
- Wrangler CLI authenticated (
wrangler login)
# Fork via GitHub UI, then clone your fork
git clone https://github.com/YOUR-USERNAME/bifrost-router.git
cd bifrost-router
pnpm installRun these commands to create the required Cloudflare resources. Save the IDs printed by each command.
# KV namespace for route storage
wrangler kv namespace create ROUTES
wrangler kv namespace create ROUTES --preview # For local dev
# D1 database for analytics
wrangler d1 create bifrost-analytics
# R2 buckets (create only the ones you need)
wrangler r2 bucket create files # Default file serving
wrangler r2 bucket create assets # Brand/static assets
wrangler r2 bucket create bifrost-backups # Automated backups
# Optional per-user buckets:
# wrangler r2 bucket create files-user1
# wrangler r2 bucket create files-user2Replace all placeholder IDs with the values from Step 2:
# KV namespace (paste your IDs)
[[kv_namespaces]]
binding = "ROUTES"
id = "paste-your-kv-namespace-id"
preview_id = "paste-your-kv-preview-id"
# D1 database (paste your ID)
[[d1_databases]]
binding = "DB"
database_name = "bifrost-analytics"
database_id = "paste-your-d1-database-id"
# R2 buckets (remove any you don't need)
[[r2_buckets]]
binding = "FILES_BUCKET"
bucket_name = "files"
[[r2_buckets]]
binding = "ASSETS_BUCKET"
bucket_name = "assets"
[[r2_buckets]]
binding = "BACKUP_BUCKET"
bucket_name = "bifrost-backups"
# Set your admin API domain
[vars]
ENVIRONMENT = "production"
ADMIN_API_DOMAIN = "bifrost.yourdomain.com"Also update the [env.dev] section with your dev domain.
Tip: Remove any R2 bucket bindings and service bindings you don't need. The worker only requires KV (ROUTES) and D1 (DB) as minimum bindings.
Edit src/types.ts to list your domains:
export const SUPPORTED_DOMAINS = [
'yourdomain.com',
'link.yourdomain.com',
'bifrost.yourdomain.com', // Admin API domain
] as const;Also update the R2 bucket arrays and BUCKET_BINDINGS map if you changed the bucket configuration.
Optional: CDN Cache Purge — To enable global cache purge for R2 objects, configure zone IDs and R2 custom domains in src/types.ts:
export const CLOUDFLARE_ZONE_IDS: Record<string, string> = {
'yourdomain.com': 'your-zone-id-from-cloudflare-dashboard',
};
export const R2_BUCKET_CUSTOM_DOMAINS: Record<string, string[]> = {
files: ['files.yourdomain.com'], // If you have R2 custom domains
};Then set up Custom Domains in the Cloudflare Dashboard to route traffic from your domains to the worker.
# Apply all migrations to production D1
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0000_large_slipstream.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0001_add_analytics_fields.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0002_analytics_indexes.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0003_add_query_string.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0004_file_downloads.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0005_proxy_requests.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0006_audit_logs.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0007_add_cache_status.sql
# For local dev, use --local instead of --remote# Set your admin API key (you'll be prompted to enter it)
wrangler secret put ADMIN_API_KEY
# Optional: Set Cloudflare API token for CDN cache purge
# (requires Zone > Cache Purge permission)
wrangler secret put CLOUDFLARE_API_TOKEN
# Deploy
pnpm run deploy# Health check
curl https://bifrost.yourdomain.com/health
# Create your first route
curl -X POST https://bifrost.yourdomain.com/api/routes \
-H "X-Admin-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"domain": "yourdomain.com",
"path": "/github",
"type": "redirect",
"target": "https://github.com/YOUR-USERNAME",
"statusCode": 302
}'The admin dashboard is a React SPA that connects to your Bifrost API.
# Create admin/.env.local
cat > admin/.env.local << 'EOF'
VITE_API_URL=https://bifrost.yourdomain.com
VITE_ADMIN_API_KEY=your-admin-api-key
EOF
# Development
pnpm --filter admin dev # Runs on port 3001
# Production (Docker)
docker build \
--build-arg VITE_API_URL=https://bifrost.yourdomain.com \
--build-arg VITE_ADMIN_API_KEY=your-api-key \
-f admin/Dockerfile \
-t bifrost-dashboard:latest .The MCP server lets you manage routes through Claude Code or Claude Desktop using natural language.
# Build the MCP server
pnpm -C shared build
pnpm -C mcp buildAdd to your Claude Code config (~/.claude.json):
{
"mcpServers": {
"bifrost": {
"command": "node",
"args": ["/absolute/path/to/bifrost-router/mcp/dist/index.js"],
"env": {
"EDGE_ROUTER_API_KEY": "your-admin-api-key",
"EDGE_ROUTER_URL": "https://bifrost.yourdomain.com",
"EDGE_ROUTER_DOMAIN": "yourdomain.com"
}
}
}
}See mcp/README.md for Claude Desktop setup and available tools.
The Slackbot lets your team manage routes via Slack messages.
- Create a Slack App with Events API enabled
- Create a KV namespace for permissions:
wrangler kv namespace create SLACK_PERMISSIONS - Update
slackbot/wrangler.tomlwith your KV, D1 IDs andEDGE_ROUTER_URL - Set secrets:
cd slackbot wrangler secret put SLACK_SIGNING_SECRET wrangler secret put SLACK_BOT_TOKEN wrangler secret put ADMIN_API_KEY - Deploy:
wrangler deploy(fromslackbot/directory)
A GitHub Actions template is provided at .github/workflows/ci-cd.yml.example.
- Rename to
ci-cd.yml - Add repository secrets:
CLOUDFLARE_API_TOKEN— Cloudflare API token with Workers Edit scopeCLOUDFLARE_ACCOUNT_ID— Your Cloudflare account IDADMIN_API_KEY— For admin dashboard build
The active CI pipeline (.github/workflows/ci.yml) runs lint, typecheck, and tests on every PR.
curl -X POST https://bifrost.yourdomain.com/api/routes \
-H "X-Admin-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"path": "/github",
"type": "redirect",
"target": "https://github.com/your-username"
}'curl -X POST https://bifrost.yourdomain.com/api/routes \
-H "X-Admin-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"path": "/blog/*",
"type": "proxy",
"target": "https://your-blog.com",
"preservePath": true,
"cacheControl": "public, max-age=60"
}'curl -X POST "https://bifrost.yourdomain.com/api/routes/migrate?domain=yourdomain.com&oldPath=/old&newPath=/new" \
-H "X-Admin-Key: your-api-key"All admin endpoints require X-Admin-Key header or Authorization: Bearer <key>.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/routes |
List routes (?search=, ?type=, ?enabled=, ?limit=, ?offset=, ?domain=) |
GET |
/api/routes?path= |
Get single route |
POST |
/api/routes |
Create route |
PUT |
/api/routes?path= |
Update route |
DELETE |
/api/routes?path= |
Delete route |
POST |
/api/routes/migrate |
Migrate route to new path |
POST |
/api/routes/transfer |
Transfer route between domains |
GET |
/api/routes/by-target |
Find routes serving an R2 object (?bucket=&target=) |
POST |
/api/routes/seed |
Bulk import routes |
GET |
/api/analytics/summary |
Analytics overview |
GET |
/api/analytics/clicks |
Click records (paginated) |
GET |
/api/analytics/views |
View records (paginated) |
GET |
/api/analytics/clicks/:slug |
Stats for specific link |
GET |
/api/storage/buckets |
List all R2 buckets |
GET |
/api/storage/:bucket/objects |
List objects (?prefix=, ?cursor=, ?limit=, ?delimiter=) |
GET |
/api/storage/:bucket/meta/:key |
Get object metadata |
GET |
/api/storage/:bucket/objects/:key |
Download object |
POST |
/api/storage/:bucket/upload |
Upload object (multipart, 100MB max) |
DELETE |
/api/storage/:bucket/objects/:key |
Delete object |
POST |
/api/storage/:bucket/rename |
Rename object within bucket |
POST |
/api/storage/:bucket/move |
Move object to different bucket |
PUT |
/api/storage/:bucket/metadata/:key |
Update object HTTP metadata |
POST |
/api/storage/:bucket/purge-cache/:key |
Purge CDN cache for R2 object |
{
path: string; // Route path (e.g., "/blog", "/docs/*")
type: "redirect" | "proxy" | "r2";
target: string; // Target URL or R2 key
statusCode?: 301 | 302 | 307 | 308; // Redirect status (default: 302)
preserveQuery?: boolean; // Pass query params (default: true)
preservePath?: boolean; // Preserve path for wildcards (default: false)
hostHeader?: string; // Override Host header for proxy routes
forceDownload?: boolean; // Force download for R2 routes (default: false)
bucket?: string; // R2 bucket name (default: "files")
cacheControl?: string; // Cache-Control header
enabled?: boolean; // Enable/disable (default: true)
}pnpm run dev # Local dev server (localhost:8787)
pnpm run test # Run all tests (1055 tests)
pnpm run typecheck # TypeScript check
pnpm run lint # Lint all packages
pnpm run deploy:dev # Deploy to dev environment| Layer | Technology | Version |
|---|---|---|
| Language | TypeScript | 5.9.3 |
| Framework | Hono | 4.12.0 |
| Runtime | Cloudflare Workers | — |
| CLI | Wrangler | 4.73.0 |
| Validation | Zod | 4.3.6 |
| ORM | Drizzle ORM | 0.45.1 |
| Storage | Cloudflare KV | — |
| Database | Cloudflare D1 (analytics) | — |
| Object Storage | Cloudflare R2 | — |
| Testing | Vitest + @cloudflare/vitest-pool-workers | 3.2.4 / 0.12.21 |
| Linting | Oxlint + Biome (formatter) | — |
| Package Manager | pnpm (workspaces) | 10.30.3 |
| Admin Dashboard | React 19 + Vite 7 + Tailwind CSS 4 + shadcn/ui | — |
MIT
Built with Cloudflare Workers and Hono
