{"id":47716901,"url":"https://github.com/oddbit/shrtnr","last_synced_at":"2026-04-20T23:02:37.872Z","repository":{"id":347943911,"uuid":"1193272799","full_name":"oddbit/shrtnr","owner":"oddbit","description":"A no-BS, self-hosted URL shortener, AI first with MCP server built in, running on Cloudflare Workers, edge-native, and ridiculously easy to deploy","archived":false,"fork":false,"pushed_at":"2026-04-17T07:51:58.000Z","size":959,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-17T08:03:00.841Z","etag":null,"topics":["analytics","cloudflare-workers","d1","d1-database","developer-tools","edge-computing","microservice","serverless","typescript","url-shortener"],"latest_commit_sha":null,"homepage":"https://oddbit.id/en/projects/shrtnr","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oddbit.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-27T03:39:07.000Z","updated_at":"2026-04-17T07:51:53.000Z","dependencies_parsed_at":"2026-04-17T08:01:25.863Z","dependency_job_id":null,"html_url":"https://github.com/oddbit/shrtnr","commit_stats":null,"previous_names":["oddbit/shrtnr"],"tags_count":46,"template":false,"template_full_name":null,"purl":"pkg:github/oddbit/shrtnr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddbit%2Fshrtnr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddbit%2Fshrtnr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddbit%2Fshrtnr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddbit%2Fshrtnr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oddbit","download_url":"https://codeload.github.com/oddbit/shrtnr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddbit%2Fshrtnr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32069440,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-20T21:26:33.338Z","status":"ssl_error","status_checked_at":"2026-04-20T21:26:22.081Z","response_time":94,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["analytics","cloudflare-workers","d1","d1-database","developer-tools","edge-computing","microservice","serverless","typescript","url-shortener"],"created_at":"2026-04-02T19:01:33.737Z","updated_at":"2026-04-20T23:02:37.842Z","avatar_url":"https://github.com/oddbit.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![SHRTNR. logotype](./public/logotype-white.svg)\n# Open-Source URL Shortener on Cloudflare Workers\n\n\u003e A free, self-hosted URL shortener with click analytics, an admin dashboard, and AI integration. Runs on Cloudflare's free tier. Zero servers, zero monthly cost.\n\n[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://oddb.it/shrtnr-deploy-top)\n\n## Why shrtnr\n\nMost URL shorteners either lock you into a SaaS with per-click pricing or require you to run a VPS. shrtnr runs on Cloudflare Workers + D1, both free tier. You own your data, your domain, and your short links.\n\nIt takes one click to deploy. You get a full admin UI, click analytics, a TypeScript SDK, and an MCP server for AI assistants: all from a single Cloudflare Worker.\n\nRead more on our [website](https://oddbit.id).\n\n## Features\n\n- **Free hosting** on Cloudflare Workers + D1 (no VPS, no containers, no monthly bill)\n- **Short slugs** starting at 3 characters (32,768 unique combinations at that length)\n- **Custom slugs** like `/my-campaign` alongside random slugs\n- **Click analytics** with referrer, country, device, and browser tracking\n- **Admin dashboard** for link management, analytics charts, and QR code generation\n- **Multi-language admin UI** with English, Indonesian, and Swedish built in\n- **API key authentication** with scoped Bearer tokens for programmatic access\n- **TypeScript SDK** ([`@oddbit/shrtnr`](https://oddb.it/shrtnr-npm-readme)) for Node.js and browser apps\n- **Built-in MCP server** at `/_/mcp` with OAuth via Cloudflare Access, so Claude, Copilot, and other AI assistants can shorten URLs\n- **One-click deploy** with automatic database provisioning and migrations\n\n## Deploy\n\n![Oddbit logotype](https://oddbit.id/logo/oddbit-primary-logo-mint-green.png)\n**👉 Running into issues or prefer someone else handle this? 👈** \n\n[Oddbit](https://oddbit.id) built shrtnr and helps teams deploy, configure, and integrate it. Just [reach out](https://oddbit.id) and we'll get you sorted 🤓🚀\n\n### One-click\n\nClick the **Deploy to Cloudflare** button above. Cloudflare will fork the repo, provision a D1 database and KV namespace, and deploy the Worker.\n\n[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://oddb.it/shrtnr-deploy-howto)\n\n\n**⚠️ Important: GitHub Actions workflows are not copied when Cloudflare forks your repo.** This means the automatic migration workflow (`.github/workflows/migrate.yml`) does not exist in your fork after the initial deploy. You must set up migrations yourself. Without running migrations, the database schema will be missing and the app will not work.\n\nAfter the initial deploy, apply the database migrations immediately:\n\n```bash\ncd shrtnr\nyarn install\nnpx wrangler d1 migrations apply DB --remote\n```\n\nThen, every time you pull updates and push them to your fork, re-run migrations to apply any new schema changes:\n\n```bash\nnpx wrangler d1 migrations apply DB --remote\n```\n\nTo automate this, copy `.github/workflows/migrate.yml` from the source repo into your fork and add the required secrets (see [Continuous deployment](#continuous-deployment) below).\n\n### Manual\n\n```bash\ngit clone https://github.com/oddbit/shrtnr\ncd shrtnr\nyarn install\nyarn wrangler-login\nyarn db:create\nyarn deploy\nyarn db:migrate:remote\n```\n\n### Continuous deployment\n\nCloudflare [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) redeploys the Worker on every push to your production branch. Database migrations are handled separately by the included GitHub Actions workflow at `.github/workflows/migrate.yml`, which triggers when Cloudflare's check suite completes successfully.\n\n**If you used one-click deploy:** Cloudflare forks the repo but does not copy GitHub Actions workflows. To get automatic migrations, copy the file manually:\n\n1. In your forked repo, create `.github/workflows/migrate.yml` with the contents from the [source repo](https://github.com/oddbit/shrtnr/blob/main/.github/workflows/migrate.yml).\n2. Add two repository secrets in GitHub under **Settings \u003e Secrets and variables \u003e Actions**:\n\n- `CLOUDFLARE_API_TOKEN`: a Cloudflare API token with **Workers Scripts: Edit** and **D1: Edit** permissions\n- `CLOUDFLARE_ACCOUNT_ID`: your Cloudflare account ID (visible in the dashboard URL or the right sidebar of any zone page)\n\nWithout these secrets, you can still deploy: Workers Builds handles the code, and you run `yarn db:migrate:remote` manually when pushing schema changes.\n\n## Access Control\n\nThe admin UI (`/_/admin/*`) ships without built-in authentication. Protecting it is your responsibility. The app makes no assumptions about which method you use, but we recommend [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/applications/) for most deployments. Other options include IP allowlists, firewall rules, Cloudflare Tunnel, or running on a private network.\n\n### Recommended: Cloudflare Access\n\nCloudflare Access handles login, sessions, and SSO at the edge before requests reach your worker. It supports Google, GitHub, Microsoft, Okta, SAML, OIDC, and a built-in one-time PIN.\n\n1. Open **Zero Trust** in the [Cloudflare dashboard](https://one.dash.cloudflare.com/)\n2. Go to **Access \u003e Applications \u003e Add an application**\n3. Choose **Self-hosted**\n4. Set the application domain to your short domain (e.g. `oddb.it`) with path `_/admin/*`\n5. Add a policy, for example:\n   - **Action:** Allow\n   - **Include rule:** Emails ending in `@yourcompany.com`\n6. Under **Authentication**, enable at least one login method. \"One-time PIN\" works out of the box with no external IdP.\n\nVisit `https://yourdomain.com` and Cloudflare Access will prompt you to log in before reaching the admin dashboard. See [Cloudflare's IdP guides](https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/) for setup instructions.\n\n#### Enable JWT verification in the worker\n\nBy default the worker trusts whatever Cloudflare Access lets through (network-layer protection). For defense-in-depth, enable cryptographic JWT verification so the worker validates every request independently:\n\n1. In Zero Trust, go to your application's **Overview** tab and copy the **Application Audience (AUD) Tag**.\n2. Set it as a worker secret:\n\n```bash\nnpx wrangler secret put ACCESS_AUD\nnpx wrangler secret put ACCESS_JWKS_URL\n```\n\n`ACCESS_JWKS_URL` follows the pattern `https://\u003cyour-team-name\u003e.cloudflareaccess.com/cdn-cgi/access/certs`.\n\nWhen `ACCESS_AUD` is set, the worker validates the JWT signature and audience claim on every admin and MCP request. When absent (local dev), it skips verification and falls back to dev mode.\n\n\n\n## Integrations\n\n### TypeScript SDK\n\nShorten URLs, manage links, and read analytics from any TypeScript or JavaScript app.\n\n- Package: [`@oddbit/shrtnr`](https://oddb.it/shrtnr-npm-readme)\n- Documentation: [sdk/README.md](sdk/README.md)\n\n### MCP Server (AI Integration)\n\nEvery shrtnr deployment includes a built-in [MCP](https://modelcontextprotocol.io/) endpoint. Claude, GitHub Copilot, Cursor, and any MCP-compatible client can connect to it over Streamable HTTP transport to create and manage short links.\n\nThe MCP endpoint authenticates through [Cloudflare Access Managed OAuth](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/). CF Access acts as the OAuth Authorization Server: it handles client registration, token issuance, and validation at the edge. The Worker receives authenticated requests with identity headers and does not implement any OAuth endpoints itself.\n\n#### MCP setup\n\n**1. Create a self-hosted Access application** for the MCP endpoint in Cloudflare Zero Trust:\n\nCF Access MCP-type applications cannot be scoped to a path — they must own a full subdomain. The Worker detects requests on any host starting with `mcp.` and routes them to the MCP handler, so the subdomain **must** use the `mcp.` prefix (e.g., `mcp.your-domain.com`).\n\n1. Go to **Access \u003e Applications \u003e Add an application \u003e Self-hosted**\n2. Set the domain to your MCP subdomain (e.g., `mcp.your-domain.com`) with no path\n3. Add an allow policy for your email domain\n4. Go to **Advanced settings**, expand **Managed OAuth (Beta)** and toggle it **on**\n5. Enable **Allow localhost clients** and **Allow loopback clients**\n6. Under **Allowed redirect URIs**, add one entry per integration:\n   - `https://claude.ai/api/mcp/auth_callback` — for Claude.ai (legacy domain) and Claude Desktop\n   - `https://claude.com/api/mcp/auth_callback` — for Claude.ai (current domain)\n   - `https://dash.cloudflare.com/*` — for the CF Access AI Controls portal to authenticate and sync tools\n   - Add equivalents for other platforms (ChatGPT, etc.) as needed. To find a client's exact callback URI: attempt to connect, let the flow fail, and read the `redirect_uri` from the error URL in the browser.\n7. CF Access changes can take 30–60 seconds to propagate after saving.\n\n**Register the MCP subdomain with the Worker:**\n\n1. Go to **Workers \u0026 Pages** \u003e shrtnr \u003e **Settings** \u003e **Domains \u0026 Routes**\n2. Click **Add Custom Domain** and enter your MCP subdomain (e.g., `mcp.your-domain.com`)\n3. Cloudflare creates the DNS record automatically — no manual DNS configuration needed\n\n**2. Set Worker secrets and deploy.**\n\n```bash\nnpx wrangler secret put MCP_ACCESS_AUD    # AUD tag from the MCP Access application\nnpx wrangler secret put ACCESS_JWKS_URL   # https://\u003cyour-team\u003e.cloudflareaccess.com/cdn-cgi/access/certs\nyarn deploy\n```\n\n**3. Disable \"Block AI bots\" for your domain.** Cloudflare's managed bot rule blocks requests from AI assistants (Claude, Copilot, etc.) at the edge before they reach your Worker. MCP clients connect from cloud infrastructure that Cloudflare classifies as AI bot traffic. If this rule is active, the OAuth handshake completes but the MCP connection itself is silently dropped.\n\nGo to [Cloudflare Dashboard](https://dash.cloudflare.com/) \u003e your zone \u003e **Security** \u003e filter by **Bot traffic** \u003e find **Block AI bots** and set it to **Do not block (off)**. This must be disabled on every zone that hosts an MCP subdomain.\n\n#### Available tools\n\n| Tool | Description |\n|---|---|\n| `health` | Check server health and version |\n| `list_links` | List all short links with slugs and click counts |\n| `get_link` | Get details for a link by ID |\n| `create_link` | Shorten a URL (supports labels, custom slugs, expiry) |\n| `update_link` | Update a link's URL, label, or expiry |\n| `disable_link` | Disable a link so it stops redirecting |\n| `add_custom_slug` | Add a custom slug to an existing link |\n| `get_link_analytics` | Get click stats by country, referrer, device, and browser |\n\n#### Connecting MCP clients\n\nAll clients connect to `https://mcp.your-domain.com`. The OAuth handshake is automatic: the client opens a browser for Cloudflare Access sign-in on first connect.\n\n**Claude (claude.ai):** Settings \u003e Integrations \u003e Add custom connector. Enter `https://mcp.your-domain.com` as the URL.\n\n**Claude Desktop** (`claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"shrtnr\": {\n      \"command\": \"npx\",\n      \"args\": [\"mcp-remote\", \"https://mcp.your-domain.com\"]\n    }\n  }\n}\n```\n\n**Claude Code** (`.mcp.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"shrtnr\": {\n      \"command\": \"npx\",\n      \"args\": [\"mcp-remote\", \"https://mcp.your-domain.com\"]\n    }\n  }\n}\n```\n\n**VS Code / GitHub Copilot** (`.vscode/mcp.json`):\n\n```json\n{\n  \"servers\": {\n    \"shrtnr\": {\n      \"type\": \"http\",\n      \"url\": \"https://mcp.your-domain.com\"\n    }\n  }\n}\n```\n\n**Other clients:** Point at `https://mcp.your-domain.com` with Streamable HTTP transport. The server advertises its OAuth endpoints via `/.well-known/oauth-authorization-server`.\n\nReplace `your-domain.com` with your actual short domain.\n\n## API\n\nAuthentication model:\n\n- **Admin UI** (`/_/admin/*`) has no built-in auth. Protect it externally (see Access Control above).\n- **API key Bearer tokens** grant scoped access to the public link-management API. Create keys from the admin UI under API Keys. Pass them as `Authorization: Bearer sk_...`.\n- **MCP endpoint** (`mcp.your-domain.com`) uses OAuth via Cloudflare Access. See the MCP section above.\n- The health endpoint is public and does not require auth.\n\nAdministrative endpoints (settings, dashboard stats, key management) live under `/_/admin/api/*` and are not accessible via API keys.\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/_/api/links` | Bearer token | List all short links |\n| `POST` | `/_/api/links` | Bearer token | Shorten a URL (create a new link) |\n| `GET` | `/_/api/links/:id` | Bearer token | Get a link with click stats |\n| `PUT` | `/_/api/links/:id` | Bearer token | Update a link's URL, label, or expiry |\n| `POST` | `/_/api/links/:id/slugs` | Bearer token | Add a custom slug to a link |\n| `POST` | `/_/api/links/:id/disable` | Bearer token | Disable a link |\n| `GET` | `/_/api/links/:id/analytics` | Bearer token | Get click analytics (referrer, country, device, browser) |\n| `GET` | `/_/health` | Public | Health check |\n| `POST` | `/_/mcp` | OAuth | MCP endpoint for AI assistants (Streamable HTTP) |\n\n## Development\n\n```bash\nyarn install\nyarn db:migrate         # apply migrations to local D1\nyarn test\nyarn dev\n```\n\n### SDK development\n\n```bash\ncd sdk\nyarn install\nyarn test\nyarn build\n```\n\n## Attribution\n\nshrtnr is built and maintained by **[Oddbit](https://oddbit.id)**. \n\nIf you fork or build on this project, keep the license, notice, and attribution files intact. Apache 2.0 requires this, and it's good open-source etiquette.\n\n- Source: \u003chttps://github.com/oddbit/shrtnr\u003e\n- License: [Apache License 2.0](LICENSE)\n- Attribution: [NOTICE](NOTICE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foddbit%2Fshrtnr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foddbit%2Fshrtnr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foddbit%2Fshrtnr/lists"}