A self-hosted URL shortener with public redirects, authenticated management, shared workspaces, advanced routing and operational controls. Built on thedevs-network/kutt, using Node.js, Express, server-rendered templates and HTMX. This is an independent fork, not the upstream hosted service.
The manually deployed .69 release consolidates link editing around a single End date/time,
uses browser-local start/end pickers and aligns Paused. Existing deadlines are
migrated without extending validity; the legacy API remains compatible.
Validation and recovery tracker.
This one-off deployment was explicitly authorized while GitHub Actions is
blocked. Remote CI and .69 GHCR publication are not claimed; exact-image
tests, pre/post NAS writable recovery and live acceptance passed and are recorded in the
manual deployment receipt.
It also includes .68, which fixes lowercase country-code names displayed as
"Unknown" on the Statistics map. Counts are retained; no analytics reset or
GeoIP collection change is needed. Validation tracker.
The preceding 9 October 2026 release, 3.2.6-sr94.67.2, is published and was deployed, adding
owner-confirmed per-link statistics reset
with API support and queue/cache fencing, plus compatible template/proxy security
patches. Main/tag CI, full exact-image regression, local/NAS writable recovery,
public API authorization, desktop/mobile UI acceptance and repeated monitoring
passed. Existing users, links and secrets are preserved. Open a link's Statistics
page and choose Reset statistics; confirmation clears test analytics without
changing link settings or redirect quotas. WAF/SSO and public redirects are unchanged.
The preceding 2 October 2026 release, 3.2.6-sr94.66, was published to GHCR and deployed
on the homelab. Main/tag CI, exact-image regression, public redirect and
management-boundary checks, stable health and verified writable local/NAS
recovery passed. The security audit
records the fixes and two bounded residual risks. The preceding .65
open-issue review records eight
applicable upstream issue fixes and the disposition of all 69 open issues.
The earlier homelab Actions budget blocker was resolved using its dedicated,
repository-scoped runner. Repository hygiene
and deployment checks
passed; no paid fallback or relaxed checks were used.
The source includes all 16 original roadmap features and all 21 community features, all complete. English (default), French and Spanish use separate catalogs with matching stable keys. See the delivery ledger for exact versioned coverage and historical evidence, and the regression guide for reproducible checks.
- Feature roadmap: completed features and release evidence.
- Community contribution review: selected proposals, attribution and deferred ideas.
- Upstream open-issue review: all 69 open issues classified against this fork, with eight fixes and release evidence.
- Community delivery ledger: approved additions and current validation status.
- UI/UX review: fixes, completed human acceptance and the final security remediation gates.
- September interface polish: responsive alignment, dark icons, spacing and the
.64acceptance ledger. - October security audit: live/source findings, remediation, validation and deployment status.
- Custom domain ownership: DNS proof, client compatibility and recovery for new claims.
- Upstream PR #1046: updated with the tested expanded contribution at
af19f0fc; upstream review/merge remains pending.
SQLite is the fully exercised database engine. PostgreSQL 17 and MySQL 8.4 have targeted security/concurrency tests; MariaDB remains configuration-only. These are not full feature-parity guarantees. Security reviews and image scans are bounded, dated evidence, not certification that the application has no vulnerabilities.
The preceding .64 update completed the 12-finding interface review:
aligned search/sort controls, visible dark-mode icons, compact responsive layouts,
consistent focus/controls and useful empty analytics. Live browser acceptance
covered 72 pages across 18 locale/theme/viewport combinations. Public redirects
and WAF/SSO/CSP/TLS controls remain unchanged.
The preceding .59 update replaces the homepage's relative expiry with calendar/time
pickers for UTC start/end, displaying yyyy-MM-dd HH:mm:ss. See
creation schedules and the
accepted release evidence.
Blank dates mean immediate availability and no scheduled expiration. The legacy
relative-expiry API remains compatible. From .69, personal/admin
editing uses local dates and no longer exposes a relative duration; the explicit
UTC creation controls retain their behavior.
- Features
- Quick start
- Configuration and access
- API and integrations
- Development and testing
- Upgrades and recovery
- Customization
- Documentation
- Contributing and credits
The fork retains custom domains, custom aliases, password-protected links, visit statistics, local accounts and administrator tools. It adds:
| Area | Capabilities and guide |
|---|---|
| API access | Named, scoped, expiring and revocable tokens, domain restrictions and retry-safe idempotent link creation |
| Link availability | Pause, scheduled start/end and maximum visits, enforced when a redirect is requested |
| Recovery | Change history, trash and restore, with protection against silently reusing retired aliases |
| Moderation | Atomic administrative bans, explicit recovery and audit, with permanent credential revocation and final-administrator protection |
| Appearance | System, Light and Dark, persistent browser preference, readable charts and unchanged white QR exports |
| Languages | English (default), French and Spanish, separate extensible catalogs, request-local translation and localized UI, feedback, email, dates and numbers |
| Sign-in | Stable OIDC identities, signed logout, explicit signing algorithms and opt-in administrator-claim mapping, with bounded grants and protected recovery |
| Management and domains | Optional separate management hostname and explicit domain sharing, host-only cookies, creator-isolated analytics and permanent scoped-token revocation |
| Organization | Tags, collections, saved filters and bulk actions |
| Data transfer | CSV/JSON import and export, templates, dry-run previews and explicit conflict handling |
| QR codes | PNG/SVG downloads, PNG clipboard copy and printing, with validated PNG branding, generated locally |
| Collaboration | Shared workspaces with owner/editor/viewer roles and conflict-aware editing |
| Routing | Ordered device, language, country and query rules, with a redirect preview |
| Forwarding | Multi-segment aliases and allowlisted query/path forwarding |
| Campaigns | UTM campaign builder in personal, admin and workspace editors, with API support |
| Analytics | Date ranges, exports, tag summaries, bot filtering and interactive country maps, keyboard controls, a text alternative and owner-confirmed statistics reset |
| Destination policy | Mandatory safe-scheme baseline and optional allowed-host policy, enforced across writes, imports, redirects, management links and monitoring |
| Browser protection | Staged and enforced Content Security Policy, fresh nonces and compatible native/HTMX workflows |
| Transport and performance | Verified database/Redis TLS, safe sorting and profile-led SQLite indexing |
| Privacy | Per-link tracking opt-outs and administrator-controlled retention |
| Integrations | Signed asynchronous webhooks, delivery history, retries and private live updates |
| Monitoring | Opt-in destination checks, private aggregate monitoring and protections against requests to private network destinations |
| Performance monitoring | Dedicated authenticated metrics listener, bounded request/latency labels and process gauges; off by default |
| Optional mobile client | Scoped-token iOS Shortcut; Apple Shortcuts is not needed to run the server |
Management screens include responsive tables, keyboard-accessible dialogs, draft preservation, conflict feedback, masked one-time credentials and explicit clipboard success/failure states. See the UI/UX ledger for measured coverage and limitations.
Use Docker Engine with the Compose plugin. The default example builds the
checked-out source, persists SQLite and custom assets in named volumes, and
publishes only 127.0.0.1:3000 on the Docker host.
git clone --branch v3.2.6-sr94.67.2 --depth 1 https://github.com/RobinMJD/kutt.git
cd kutt
cp .example.env .env
chmod 600 .envBefore starting, edit .env:
- Set
JWT_SECRETto a long random value. For a new installation, generate one withopenssl rand -base64 48. Preserve the existing secret on upgrades. - Set
DEFAULT_DOMAINto the canonical hostname, without a scheme or path, for exampleshort.example.com. Keeplocalhost:3000only for local testing. - Leave anonymous creation and local registration disabled unless deliberately
required. Keep
TRUST_PROXY=falsefor direct local access; configure it explicitly for your trusted proxy topology before deployment.
docker compose config --quiet
docker compose up --build -d
docker compose psThe container applies database migrations before starting. Complete first-admin setup privately before enabling public routing. On a remote Docker host, use a private tunnel or the documented trusted proxy path, not a public backend port. Follow deployment setup to configure TLS, WAF and OIDC management access before exposure. These controls are not automatically installed by the Compose example.
The default example is a starting point, not the complete hardened homelab
deployment. Add an appropriate restart, backup, monitoring and resource policy
for your environment. Do not use docker compose down --volumes on an instance
whose data you intend to keep.
The accepted fork release image is
ghcr.io/robinmjd/kutt:v3.2.6-sr94.67.2 (digest
sha256:5381aae825699e437833eb419920cb79427586efcd52c38248274de4603bb514).
For an image-based deployment, replace
the Compose service's build section with an image reference, retaining its
environment and persistent volumes. Pin the tested image digest in production.
The upstream kutt/kutt Docker Hub image does not contain these fork changes.
The fork workflow tests main and release
tags, but publishes an image only for v*-sr94.* tags. Publication is separate
from deployment; do not auto-deploy untested branch builds or moving tags.
| Example | Purpose |
|---|---|
| docker-compose.yml | SQLite, no external database service |
| docker-compose.sqlite-redis.yml | SQLite with private Redis for visit queues and shared rate-limit state |
| docker-compose.postgres.yml | PostgreSQL configuration example; explicit database image and credentials required |
| docker-compose.mariadb.yml | MariaDB configuration example; separate application and root credentials required |
Select an alternative with docker compose -f <file> ...; these are standalone
examples, not overlays to combine. Read the database and Redis caveats
before using them. In particular, the Redis examples do not persist queued work,
and changing database image tags is not a supported major-version migration.
Use .example.env as the configuration checklist and
server/env.js for the accepted values/defaults. Avoid copying
an old full environment table from another Kutt version. Important settings:
| Setting | Operational guidance |
|---|---|
JWT_SECRET |
Required for production; preserve it with the database because it also protects sessions and encrypted webhook credentials |
DEFAULT_DOMAIN, SITE_NAME |
Canonical hostname and display name; the domain contains no scheme/path |
DISALLOW_ANONYMOUS_LINKS, DISALLOW_REGISTRATION |
Both default to true; delegated OIDC provisioning is a separate policy |
OIDC_*, DISALLOW_LOGIN_FORM |
Configure native SSO and provider admission first; use DISALLOW_LOGIN_FORM=true for SSO-only login |
OIDC_ALLOW_REGISTRATION |
Controls new OIDC identities, not local signup; defaults to true |
OIDC_SESSION_MAX_SECONDS |
Absolute OIDC session lifetime; defaults to 3,600 seconds |
TRUST_PROXY |
Defaults to false. Enable only for an exclusive trusted proxy path that replaces untrusted forwarding headers; prefer explicit peer addresses over trusting all proxies |
CSP_MODE |
Optional off (default), report-only or enforce; see nonce policy and customization compatibility before enabling |
MANAGEMENT_ORIGIN |
Empty by default; separate management hosting requires matching WAF/TLS/SSO routes and IdP callbacks first |
OIDC_ADMIN_MAPPING_ENABLED, OIDC_ADMIN_* |
Off by default; use explicit signed claim/value mapping with a tested protected recovery administrator |
DESTINATION_ALLOWED_HOSTS |
Empty disables the optional host allowlist, not the mandatory safe-scheme check; an explicit empty JSON array denies all destinations. Review policy and recovery before enabling |
METRICS_* |
Off by default; enable only a private, bearer-authenticated listener. Do not publish the metrics port |
DB_*, REDIS_* |
Must match the selected topology, persistent paths and existing credentials |
ENABLE_RATE_LIMIT |
Defaults to true for management API protection. Disable only behind an equivalent tested rate limit; protected-link password and report throttles remain enabled independently |
CUSTOM_DOMAIN_USE_HTTPS |
Controls custom-domain link URLs; it does not provision DNS, TLS certificates or proxy routes |
MAIL_*, REPORT_EMAIL, CONTACT_EMAIL |
Configure SMTP for verification/recovery/report workflows; mail is disabled by default |
Declared application settings support NAME_FILE secret-file input. File
values take precedence over inline values and unreadable files fail startup.
Mount secret files read-only at their container paths. The supplied Compose
examples validate an inline JWT_SECRET; adapt that declaration deliberately
when switching to file-only injection. Never commit real .env files,
credentials, identity mappings, databases or backup archives.
- Public short-link redirects do not require a Kutt or Authentik login. An explicitly password-protected link still requires its own link password, and pause/expiry/trash rules still apply. Do not put an interactive SSO challenge over the whole hostname.
- Management uses Kutt's authorization and native OIDC. The homelab uses Authentik plus BunkerWeb/WAF; a new installation must configure its own edge protections and provider access policy. Never expose a direct backend to bypass those controls.
- API clients authenticate with scoped tokens. OIDC callbacks and signed back-channel logout need their documented routes, not an extra interactive SSO challenge. WAF and application validation must remain in place.
- Existing accounts are not automatically linked by email. Before migrating
an existing OIDC installation, follow the verified identity-binding procedure.
The callback is
https://YOUR_DOMAIN/login/oidc; changing issuer or subject mode later requires a planned identity migration.
The application serves both /api and /api/v2. Prefer named tokens created
in Settings, give each integration only the scopes/domain it needs, and send
the secret in the X-API-Key header. Public redirects need no API credential.
Legacy API keys remain compatible but broad; migrate integrations to scoped
tokens rather than treating old keys as least-privilege credentials.
Start with the API token and idempotency guide. Feature guides above document their endpoints, permissions, limits and recovery behavior. Some account, invitation and administrator operations deliberately require a browser session; a token never grants site-administrator privileges.
The base API specification can be rendered with
npm run docs:build in a development checkout. It is not a complete reference
for every fork endpoint; use the feature guides for additions. Older upstream
API clients may work with compatible routes but are not automatically validated
against the fork's scopes and security policies.
The optional iOS Shortcut guide covers the reviewed artifact and private token setup. It is an API client example, not a server dependency or deployment mechanism.
Use Node.js 24. SQLite is the default and needs no separate database server.
There is no frontend bundler step; templates, CSS and browser scripts ship with
the application. Install locked dependencies, configure a private .env and
initialize the database:
npm ci
npm run migrate
npm run devnpm start runs production mode. Standalone Node listens on network interfaces,
so restrict access with your host firewall/trusted proxy; the Compose loopback
binding does not apply to a standalone process. Native SQLite dependencies may
need a compiler toolchain when no prebuilt binary is available for your platform.
For release validation, use a separate disposable checkout with no .env:
docker build -t kutt-smoke .
docker run --rm --network none --entrypoint node kutt-smoke tests/container-smoke.cjs
sh tests/redis-smoke.sh kutt-smoke
python3 tests/compose-config.pyThe full smoke suite uses temporary SQLite data and tests migrations, API compatibility, authorization, public redirects and feature behavior. The Redis test uses its own isolated containers. Compose validation renders the four examples without claiming full PostgreSQL/MariaDB runtime coverage.
See test instructions for rendered desktop/mobile suites, fresh loopback fixtures, Playwright requirements and separate native-device acceptance. Never point disposable browser tests at a real deployment or an existing user's browser profile. CI is not a substitute for testing the exact deployed image, real SSO, monitoring and recovery.
Read deployment and recoverable upgrades before changing an existing instance. Release publication alone is not a successful deployment.
- Record the current source/image digest, configuration, database version and health. Take a consistent backup of the database, custom assets and private configuration/secrets, and keep an encrypted off-host copy.
- Restore into an isolated instance using the exact candidate image; verify integrity, migrations and a write/read cycle before deploying. For SQLite, use its backup API or stop writers; copying only a live main file is unsafe.
- Deploy the tested digest without replacing secrets or volumes. Validate existing links, public redirects, management authorization, real OIDC and enabled integrations, then observe monitoring through a post-restart interval.
- Take a clean post-deployment backup and verify restoration again. Record the deployment and rollback evidence separately from release CI.
Prefer fix-forward or a documented schema-compatible rollback. Old images can ignore newer lifecycle, privacy or revocation rules; do not drop populated policy tables to force a downgrade. Restores must reconcile later writes and revoked credentials before reopening access. The repository alone can recreate an empty application, not recover lost users, links, secrets or analytics.
Local branding can override files through custom/css, custom/images and
custom/views. A custom/css/styles.css replaces the corresponding default
stylesheet. Keep overrides under version control without private credentials,
and include them in backups.
Containers load overrides from /kutt/custom. The example Compose files mount
a persistent custom volume there; populate that volume or use a deliberate
bind mount instead of relying on edits to an ephemeral container filesystem.
Restart after template/configuration changes and verify loaded browser assets.
Custom templates and themes must be checked on every upgrade. Older upstream themes may omit new controls, validation, accessibility or security behavior; they are not automatically compatible with this fork. Preserve versioned asset URLs and their query strings through proxy/CDN caches.
| Task | Start here |
|---|---|
| Install, upgrade or restore | Deployment and recovery |
| Configure SSO or migrate identities | OIDC security |
| Connect an API client | Tokens and idempotency |
| Understand a feature | Feature guides |
| Operate destination checks | Destination health |
| Review hardening and scan limits | Security maintenance |
| Reproduce automated checks | Testing |
| Check implementation/deployment status | Original roadmap, community ledger and UI/UX ledger |
| Add another language | Catalog and language-registry guide |
| Review community ideas and attribution | Community review |
Propose fork-specific fixes and enhancements through pull requests. The fork's issue tracker is currently disabled. Include the version, database engine, reproduction steps and sanitized logs; never post credentials, private destinations or identity mappings. Changes should include focused tests, authorization/backward-compatibility checks and any migration/recovery notes.
This fork builds on the work of the upstream Kutt contributors. Original author: Pouria Ezzati. Special thanks to Thomas and Muthu; logo design by Muthu. Selected community proposals are credited in the review linked above. The upstream hosted service and third-party integrations are operated independently of this fork.
Licensed under the MIT License.
