Skip to content

Latest commit

 

History

124 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bifrost — Self-Hosted URL Shortener & Edge Router for Cloudflare Workers

Bifrost Logo

A free, self-hosted alternative to bit.ly and Rebrandly — built on Cloudflare Workers with zero server costs

License: MIT Tests TypeScript Cloudflare Workers

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.

Why Bifrost?

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

Features

  • Dynamic Routing — Configure routes via API without redeployment
  • Three Route Types:
    • redirect — URL redirects (301, 302, 307, 308)
    • proxy — Reverse proxy to external URLs
    • r2 — Serve content from R2 buckets
  • Case-Insensitive Paths — Visitors can use any case in the URL (/LinkedIn, /LINKEDIN, /linkedin all match the same route)
  • 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 (29 tools)
  • QR Codes (v1.30.0) — unified QR resource (URL / text / Wi-Fi / vCard) with optional route linking (re-point, never reprint), a preset registry for your own branding, live preview, SVG + PNG export, and authed-only image serving
  • User Guide (v1.30.0) — in-dashboard guide (11 task-first sections) with a first-visit welcome dialog, contextual ? help links, an MCP integration tab, and dated changelog
  • Operational Analytics — domain-aware full URLs, redirect/proxy/service-page leaders, recent activity, period comparisons, and actionable traffic signals; Cloudflare Health Checks are excluded by default
  • 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

Security Features

  • 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)
  • Service-Binding Fetch Resilience — Worker-to-Worker service-binding calls are wrapped in try/catch via the safeServiceFetch helper, so URL-parse errors and binding failures become 404s + warn logs instead of scriptThrewException worker errors

Project Structure

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

Fork & Deploy Guide

This repo is designed as a forkable template. Follow these steps to deploy your own instance.

Prerequisites

  • Node.js >= 24 (see .nvmrc)
  • pnpm (corepack enable && corepack prepare)
  • A Cloudflare account with Workers enabled (free plan works)
  • Wrangler CLI authenticated (wrangler login)

Step 1: Fork & Clone

# Fork via GitHub UI, then clone your fork
git clone https://github.com/YOUR-USERNAME/bifrost-router.git
cd bifrost-router
pnpm install

Step 2: Create Cloudflare Resources

Run 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-user2

Step 3: Configure wrangler.toml

Replace 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.

The example [env.dev] repeats every binding with isolated placeholder resources because Wrangler environments do not inherit bindings. Replace both production and development placeholders before using either deployment target; keep the development D1/KV/R2/service resources separate from production.

Step 4: Configure Your Domains

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.

Step 5: Run Database Migrations

# 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
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0008_file_comments.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0009_feedback.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0010_external_audit_capture.sql
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0011_unified_traffic_events.sql

# For local dev, use --local instead of --remote

Step 6: Set Secrets & Deploy

# 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

Step 7: Verify

# 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
  }'

Optional: Admin Dashboard

The admin dashboard is a React SPA that connects to your Bifrost API.

Its home page is an operational overview rather than a raw event dump. It shows canonical source URLs (domain plus path), Top Routes - Redirect, Top Routes - Proxy, Top Website Pages for service-bound HTML, recent activity, period comparisons, leading domains/countries/referrers, and actionable signals such as proxy 5xx rates, low R2 cache-hit rates, scanner-like paths, and material traffic changes. Cloudflare Health Checks are excluded by default and can be restored with the labelled toggle. Dashboard analytics inherit the same admin API-key middleware as route management.

# 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 .

Optional: MCP Server

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 build

Add 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"
      }
    }
  }
}

For Claude Desktop, add the same entry (with full executable paths) to ~/Library/Application Support/Claude/claude_desktop_config.json and restart the app. Or open this repo in Claude Code and ask it to "install mcp" — it will configure both surfaces for you.

See mcp/README.md for full setup and the 29-tool reference.

Optional: Slackbot

The Slackbot lets your team manage routes via Slack messages.

  1. Create a Slack App with Events API enabled
  2. Create a KV namespace for permissions: wrangler kv namespace create SLACK_PERMISSIONS
  3. Update slackbot/wrangler.toml with your KV, D1 IDs and EDGE_ROUTER_URL
  4. Set secrets:
    cd slackbot
    wrangler secret put SLACK_SIGNING_SECRET
    wrangler secret put SLACK_BOT_TOKEN
    wrangler secret put ADMIN_API_KEY
  5. Deploy: wrangler deploy (from slackbot/ directory)

Optional: CI/CD

A GitHub Actions template is provided at .github/workflows/ci-cd.yml.example.

  1. Rename to ci-cd.yml
  2. Add repository secrets:
    • CLOUDFLARE_API_TOKEN — Cloudflare API token with Workers Edit scope
    • CLOUDFLARE_ACCOUNT_ID — Your Cloudflare account ID
    • ADMIN_API_KEY — For admin dashboard build

The active CI pipeline (.github/workflows/ci.yml) runs secret and public-sanitisation scans, lint/format/type checks, tests with locked coverage floors, the dashboard build, analytics/routing/dormant-path performance gates, and production plus development Wrangler dry-runs on every PR and push. It does not deploy.

Optional: External R2 operations audit capture (v1.28.0)

By default, the audit log only records operations made through Bifrost. This optional feature also captures R2 changes made outside it — Cloudflare dashboard uploads, Wrangler commands, direct S3/REST API keys — into the same audit page, labelled by source. It ships dormant (both flags "off"); enabling it is a two-layer opt-in:

Layer 1 — object-level capture (requires the Workers Paid plan). Cloudflare Queues are paid-plan-only. On the free plan, wrangler queues create fails with a payment-required error — that is the expected gate, not breakage; skip to Layer 2, which works on any plan.

# 1. Create the queues (60s delivery delay is load-bearing — do not omit it)
wrangler queues create bifrost-r2-events --delivery-delay-secs 60
wrangler queues create bifrost-r2-events-dlq

# 2. Attach notification rules to each bucket you want monitored
wrangler r2 bucket notification create <bucket> \
  --event-types object-create object-delete --queue bifrost-r2-events

# 3. Uncomment the [[queues.consumers]] block in wrangler.toml

# 4. Apply the migration (once per environment)
wrangler d1 execute bifrost-analytics --remote --file=./drizzle/0010_external_audit_capture.sql

# 5. Set R2_EVENT_AUDIT = "on" in wrangler.toml [vars] and deploy

External writes then appear on the audit page within ~2 minutes as External (unattributed) — Cloudflare's event payloads carry no actor identity on any plan, so what/when/where is captured but who is not (that's what Layer 2 adds for config changes).

Layer 2 — config-change capture with real actor attribution (works on the free plan). A */30 cron polls your account's audit logs for R2/queue-scoped changes (bucket settings, notification rules, token changes) and records them with the actor's email/IP — it also tamper-protects Layer 1, since deleting the notification rules is itself a captured change.

  1. Create a Cloudflare API token — two non-obvious traps here:
    • It must be a user-level token (My Profile → API Tokens → Create Custom Token). Account-owned tokens (created from the account-level API Tokens page, cfat_ prefix) are rejected by the audit-logs endpoint even when valid.
    • There is no dedicated "audit logs" permission. The permission you need is Account → Account Settings → Read ("Access: Audit Logs Read" is the Zero Trust product's login logs — the wrong one).
  2. wrangler secret put CF_AUDIT_API_TOKEN (paste the token), set CF_ACCOUNT_ID in [vars].
  3. Apply the migration (step 4 above, if you haven't), set CF_AUDIT_POLL = "on", deploy.

Rollback for either layer: flip its flag to "off" and redeploy (~90s). Notification rules and queues can stay.

Usage

Add a Redirect

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"
  }'

Add a Proxy

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"
  }'

Migrate a Route

curl -X POST "https://bifrost.yourdomain.com/api/routes/migrate?domain=yourdomain.com&oldPath=/old&newPath=/new" \
  -H "X-Admin-Key: your-api-key"

API Reference

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
POST /api/routes/normalize-case One-time migration: convert all route paths to lowercase (run after upgrading to v1.22.0+ if you have pre-existing uppercase routes)
GET /api/routes/by-target Find routes serving an R2 object (?bucket=&target=)
POST /api/routes/seed Bulk import routes
GET /api/analytics/summary Domain-aware operational overview (?domain=&days=&country=&search=&includeMonitoring=)
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

Optional: unified request analytics (v1.32.0)

Migration 0011 adds a privacy-bounded request stream that can measure public traffic beyond the four legacy event tables. It ships dormant and does not alter headline totals. To evaluate it safely, apply the migration, set an RFC3339 UTC UNIFIED_TRAFFIC_CUTOVER_AT, then set UNIFIED_TRAFFIC_MODE = "shadow". The stream stores domain, normalised path, response classification, coarse country, cache status, and bounded latency; it does not store query strings, IP addresses, referrers, User-Agent strings, or target URLs. Set the mode back to "off" to stop capture. On an environment with the daily 0 20 * * * cron, retention pruning continues after a valid cutover even while capture is off. The example development environment has no cron triggers; add that schedule if you keep a development shadow stream enabled beyond short-lived testing.

Route Configuration

{
  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)
}

Development

pnpm run dev          # Local dev server (localhost:8787)
pnpm run check        # Full quality, test, build, performance, and dry-run gate
pnpm run typecheck    # TypeScript check
pnpm run lint         # Lint all packages
pnpm run deploy:dev   # Deploy to dev environment

Tech Stack

Layer Technology Version
Language TypeScript 5.9.3
Framework Hono 4.12.34
Runtime Cloudflare Workers
CLI Wrangler 4.114.0
Validation Zod 4.4.3
ORM Drizzle ORM 0.45.2
Storage Cloudflare KV
Database Cloudflare D1 (analytics)
Object Storage Cloudflare R2
Testing Vitest + @cloudflare/vitest-pool-workers 4.1.10 / 0.18.8
Linting Oxlint + Biome (formatter)
Package Manager pnpm (workspaces) 10.33.0
Admin Dashboard React 19 + Vite 8 + Tailwind CSS 4 + shadcn/ui

License

MIT


Built with Cloudflare Workers and Hono

Releases

Packages

Used by

Contributors

Languages