Kutt is an open-source URL shortener replacing Bitly, offering custom branded domains, QR codes, and click analytics without monthly link limits.
A digital creator sharing dozens of newsletter links each week quickly runs into monthly quota walls and paywalled domain settings on commercial shortening platforms. Kutt provides a lightweight, self-hosted web application that creates branded short links, generates printable QR codes, and tracks visitor metrics on private infrastructure. Because the software runs on independent hosting without per-link fees, publishers maintain complete authority over their link history and audience redirection.
Bitly caters primarily to enterprise marketing departments, pairing automated link generation with turnkey connectors for tools like Hootsuite. While that managed model eliminates server administration, it puts basic link management behind aggressive paywalls. Attaching custom domains, reviewing historical analytics, or creating more than a handful of monthly links requires an expensive recurring plan, turning routine URL shortening into a perpetual subscription cost.
Kutt shifts the integration focus toward client utilities and open extensions rather than enterprise clouds. Official add-ons for Chrome and Firefox shorten active tabs in one click, while desktop integrations like ShareX on Windows and Alfred on macOS tie link creation directly to keyboard shortcuts. Mobile users can shorten URLs through iOS Shortcuts, and developers can automate pipelines using a REST API with client libraries for Python, Go, and Node.js. What Kutt lacks compared to Bitly is out-of-the-box integration with social schedulers like Buffer; connecting Kutt to an automated publishing queue requires custom API scripts rather than a point-and-click marketplace plugin.
Is Kutt free for personal and commercial use? Yes. You can self-host instances on your own hardware without licensing fees or subscription costs.
Kutt vs Bitly — which should I choose? Choose Bitly if you require pre-built integrations with social publishing suites like Hootsuite. Choose Kutt if you want unlimited links, custom domain branding, and browser extensions without monthly subscriptions or tracking cookies.
Can I run Kutt on my own server? Yes. Kutt deploys via Docker or Node.js using SQLite by default, with optional support for PostgreSQL and Redis.
Kutt provides a practical, self-hosted alternative when you want branded links, quick QR codes, and browser shortcuts without commercial paywalls or data mining. It is not suitable for large marketing agencies requiring shared team permissions and direct integration with corporate social suites. Active maintenance keeps the project current, and permissive MIT licensing terms make it an unencumbered choice for private infrastructure.
We started this exact setup from scratch in a clean Linux machine on Nov 2, 2026 and checked it through Kutt's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p kutt && cd kutt
curl -fsSL https://open-source.store/tools/kutt/compose.yml -o compose.ymlcat > .env <<EOF
JWT_SECRET=$(openssl rand -hex 32)
DEFAULT_DOMAIN=localhost:3000
SITE_URL=http://localhost:3000
ADMIN_PASSWORD=$(openssl rand -hex 12)
EOFJWT_SECRET — Random secret key used to sign authentication tokens and sessions. Generated above; keep it secret.DEFAULT_DOMAIN — Domain and port where Kutt is hosted, without protocol (e.g. your-server.com:3000); set to your public domain in production.SITE_URL — Public base URL of the instance; set to your public URL (e.g. https://your-server.com) in production.ADMIN_PASSWORD — Password used only by verification smoke steps to complete the initial admin setup; the operator sets their own credentials on first visit. Generated above; keep it secret.docker compose up -dservices:
server:
image: kutt/kutt:v3.2.6
restart: always
ports:
- "3000:3000"
environment:
DB_FILENAME: /var/lib/kutt/data.sqlite
DEFAULT_DOMAIN: ${DEFAULT_DOMAIN}
JWT_SECRET: ${JWT_SECRET}
SITE_URL: ${SITE_URL}
volumes:
- kutt-data:/var/lib/kutt
- kutt-custom:/kutt/custom
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:3000/api/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
kutt-data:
kutt-custom:
Free IDE for Java, Kotlin, Go, TypeScript and Python. Built on IntelliJ Community, fully open source, 300+ plugins.
Show that Kutt is listed as an open-source alternative. Paste into your README:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 27 s: kutt/kutt:v3.2.6.
Based on the upstream setup. Official compose using SQLite. Pinned image to v3.2.6, added container healthcheck, and configured JWT secret and public URLs via environment variables.
server-1 | 2026-11-02T04:06:21.588720503Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:06:21.588721961Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:06:21.588731419Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:123:27)
server-1 | 2026-11-02T04:06:21.588732919Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:06:21.588734128Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:06:21.588735253Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:06:21.588736419Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:06:21.588737544Z statusCode: 400,
server-1 | 2026-11-02T04:06:21.588738586Z status: 400
server-1 | 2026-11-02T04:06:21.588739544Z }
server-1 | 2026-11-02T04:06:21.620519586Z URIError: Failed to decode param '/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd'
server-1 | 2026-11-02T04:06:21.620538253Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:06:21.620540461Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:06:21.620541461Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:123:27)
server-1 | 2026-11-02T04:06:21.620542586Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:06:21.620543628Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:06:21.620544586Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:06:21.620545628Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:06:21.620546669Z statusCode: 400,
server-1 | 2026-11-02T04:06:21.620547669Z status: 400
server-1 | 2026-11-02T04:06:21.620548586Z }
server-1 | 2026-11-02T04:06:21.620813836Z URIError: Failed to decode param '/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd%00'
server-1 | 2026-11-02T04:06:21.620817711Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:06:21.620818878Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:06:21.620819753Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:123:27)
server-1 | 2026-11-02T04:06:21.620820586Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:06:21.620821461Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:06:21.620822294Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:06:21.620823086Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:06:21.620823878Z statusCode: 400,
server-1 | 2026-11-02T04:06:21.620824628Z status: 400
server-1 | 2026-11-02T04:06:21.620825419Z }
server-1 | 2026-11-02T04:06:42.711315638Z TypeError: Cannot read properties of undefined (reading 'replace')
server-1 | 2026-11-02T04:06:42.711329054Z at Object.removeWww (/kutt/server/utils/utils.js:353:15)
server-1 | 2026-11-02T04:06:42.711330471Z at redirectCustomDomainHomepage (/kutt/server/handlers/links.handler.js:588:22)
server-1 | 2026-11-02T04:06:42.711335721Z at asyncUtilWrap (/kutt/server/utils/asyncHandler.js:3:22)
server-1 | 2026-11-02T04:06:42.711336721Z at Layer.handle [as handle_request] (/kutt/node_modules/express/lib/router/layer.js:95:5)
server-1 | 2026-11-02T04:06:42.711337596Z at trim_prefix (/kutt/node_modules/express/lib/router/index.js:328:13)
server-1 | 2026-11-02T04:06:42.711338429Z at /kutt/node_modules/express/lib/router/index.js:286:9
server-1 | 2026-11-02T04:06:42.711339221Z at Function.process_params (/kutt/node_modules/express/lib/router/index.js:346:12)
server-1 | 2026-11-02T04:06:42.711340013Z at next (/kutt/node_modules/express/lib/router/index.js:280:10)
server-1 | 2026-11-02T04:06:42.711340804Z at config (/kutt/server/handlers/locals.handler.js:37:3)
server-1 | 2026-11-02T04:06:42.711341596Z at Layer.handle [as handle_request] (/kutt/node_modules/express/lib/router/layer.js:95:5)
server-1 | 2026-11-02T04:06:42.711617721Z TypeError: Cannot read properties of undefined (reading 'replace')
server-1 | 2026-11-02T04:06:42.711622804Z at Object.removeWww (/kutt/server/utils/utils.js:353:15)
server-1 | 2026-11-02T04:06:42.711624304Z at redirectCustomDomainHomepage (/kutt/server/handlers/links.handler.js:588:22)
server-1 | 2026-11-02T04:06:42.711625388Z at asyncUtilWrap (/kutt/server/utils/asyncHandler.js:3:22)
server-1 | 2026-11-02T04:06:42.711626263Z at Layer.handle [as handle_request] (/kutt/node_modules/express/lib/router/layer.js:95:5)
server-1 | 2026-11-02T04:06:42.711627346Z at trim_prefix (/kutt/node_modules/express/lib/router/index.js:328:13)
server-1 | 2026-11-02T04:06:42.711628138Z at /kutt/node_modules/express/lib/router/index.js:286:9
server-1 | 2026-11-02T04:06:42.711628888Z at Function.process_params (/kutt/node_modules/express/lib/router/index.js:346:12)
server-1 | 2026-11-02T04:06:42.711629721Z at next (/kutt/node_modules/express/lib/router/index.js:280:10)
server-1 | 2026-11-02T04:06:42.711630471Z at config (/kutt/server/handlers/locals.handler.js:37:3)
server-1 | 2026-11-02T04:06:42.711631221Z at Layer.handle [as handle_request] (/kutt/node_modules/express/lib/router/layer.js:95:5)
server-1 | 2026-11-02T04:06:55.600216337Z URIError: Failed to decode param '%c0'
server-1 | 2026-11-02T04:06:55.600237795Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:06:55.600239879Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:06:55.600241129Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:148:15)
server-1 | 2026-11-02T04:06:55.600242170Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:06:55.600243170Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:06:55.600246337Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:06:55.600247462Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:06:55.600248420Z statusCode: 400,
server-1 | 2026-11-02T04:06:55.600249379Z status: 400
server-1 | 2026-11-02T04:06:55.600257587Z }
server-1 | 2026-11-02T04:06:55.602311295Z URIError: Failed to decode param '%c0'
server-1 | 2026-11-02T04:06:55.602745212Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:06:55.602748587Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:06:55.602749670Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:148:15)
server-1 | 2026-11-02T04:06:55.602750712Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:06:55.602751629Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:06:55.602752420Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:06:55.602753254Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:06:55.602754045Z statusCode: 400,
server-1 | 2026-11-02T04:06:55.602754795Z status: 400
server-1 | 2026-11-02T04:06:55.602755504Z }
server-1 | 2026-11-02T04:07:20.583428667Z URIError: Failed to decode param '/theme/META-INF/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd'
server-1 | 2026-11-02T04:07:20.583477459Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:07:20.583482334Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:07:20.583484667Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:123:27)
server-1 | 2026-11-02T04:07:20.583487084Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:07:20.583489001Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:07:20.583490959Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:07:20.583492876Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:07:20.583494834Z statusCode: 400,
server-1 | 2026-11-02T04:07:20.583496626Z status: 400
server-1 | 2026-11-02T04:07:20.583498334Z }
server-1 | 2026-11-02T04:07:20.587537876Z URIError: Failed to decode param '/theme/META-INF/prototype%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%afwindows/win.ini'
server-1 | 2026-11-02T04:07:20.587558126Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:07:20.587560584Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:07:20.587562209Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:123:27)
server-1 | 2026-11-02T04:07:20.587563709Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:07:20.587565542Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:07:20.587567251Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:07:20.587568709Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:07:20.587585126Z statusCode: 400,
server-1 | 2026-11-02T04:07:20.587586626Z status: 400
server-1 | 2026-11-02T04:07:20.587587667Z }
server-1 | 2026-11-02T04:07:20.587840334Z URIError: Failed to decode param '/theme/META-INF/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd'
server-1 | 2026-11-02T04:07:20.587844167Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:07:20.587851751Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:07:20.587853334Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:123:27)
server-1 | 2026-11-02T04:07:20.587854751Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:07:20.587856167Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:07:20.587857292Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:07:20.587858459Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:07:20.587859542Z statusCode: 400,
server-1 | 2026-11-02T04:07:20.587860626Z status: 400
server-1 | 2026-11-02T04:07:20.587861626Z }
server-1 | 2026-11-02T04:07:20.592453876Z URIError: Failed to decode param '/theme/META-INF/prototype%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%af..%c0%afwindows/win.ini'
server-1 | 2026-11-02T04:07:20.592471209Z at decodeURIComponent (<anonymous>)
server-1 | 2026-11-02T04:07:20.592473251Z at decode_param (/kutt/node_modules/express/lib/router/layer.js:172:12)
server-1 | 2026-11-02T04:07:20.592475001Z at Layer.match (/kutt/node_modules/express/lib/router/layer.js:123:27)
server-1 | 2026-11-02T04:07:20.592476292Z at matchLayer (/kutt/node_modules/express/lib/router/index.js:585:18)
server-1 | 2026-11-02T04:07:20.592477751Z at Immediate.next (/kutt/node_modules/express/lib/router/index.js:226:15)
server-1 | 2026-11-02T04:07:20.592479042Z at Immediate._onImmediate (/kutt/node_modules/express/lib/router/index.js:646:15)
server-1 | 2026-11-02T04:07:20.592480334Z at process.processImmediate (node:internal/timers:486:21) {
server-1 | 2026-11-02T04:07:20.592481584Z statusCode: 400,
server-1 | 2026-11-02T04:07:20.592482792Z status: 400
server-1 | 2026-11-02T04:07:20.592483876Z }