https://s.example.org/k7mq3xw
A tiny, self-hostable URL shortener. One binary, a handful of environment variables, one SQLite file. No database server, no container stack, no settings panel: about 5,000 lines of Go with four dependencies.
What stands out
Signs in against Active Directory
Every link says who created it. Sign-in goes to your directory over ldaps:// only; plain ldap:// is refused at startup. Administrators are a group you name, and local admin accounts, checked before the directory is asked, mean there is always a way back in.
Your own domain
Every short link and every QR code encodes the origin you configure, never whatever arrived in the Host header. The management pages can live on a separate hostname from the short one.
Names you choose, or names that read off paper
Take a custom name, capitals and all, or let surl draw a random one: seven characters from an alphabet without 0, 1, l or o, so it survives being printed on a poster and typed back in.
One binary, a few variables, one file
A static Go executable with no runtime to install. Configuration comes from the environment, or an optional .env beside it, and never from a settings page. Every link lives in one SQLite file, which surl snapshots for you every night. If you'd rather run a container, there is a 15 MB image built FROM scratch: one executable, one certificate bundle, no shell. Nothing needs it.
Short links are forever
A name is never reissued, not even after its link is deleted. A withdrawn link answers 410 Gone with an explanation rather than vanishing into a 404. Redirects are always a bare 302, never an interstitial page.
Nothing to build, nothing tracked
One hand-written stylesheet and a handful of ES modules, no bundler, no node_modules. A visit adds one to a counter for that link and that day: no IP addresses, no user agents, no cookies on public pages, and no third party contacted.
What it does
- Random names that are lowercase and free of characters that look alike on paper.
- Custom names instead, capitals kept as you write them.
- Expiry after a chosen time, or never.
- Instant redirects with a bare 302, forgiving the trailing slash or punctuation a link picks up from the text around it.
- A QR code for every link, as SVG, or from an external generator for a print-ready one.
- Notes, internal to whoever issued the link.
- Live search across names, destinations and notes.
- Sort by newest, most visited, or least used.
- Directory sign-in, so every link says who created it.
- Nightly snapshots of the database and a CSV export.
- Mobile-friendly, and follows the operating system's dark mode.
- Checksummed migrations, so the schema can change and a link issued today keeps resolving.
Run it
As a binary
# one static executable, ./surl
make build
# a local administrator; the password is printed once
SURL_DATA_DIR=/var/lib/surl ./surl admin add ops-local
SURL_DATA_DIR=/var/lib/surl SURL_BASE_URL=https://s.example.org \
./surl serve
Or as a container, if you prefer
# the image, surl:latest
make image
podman run -d --name surl -p 127.0.0.1:8080:8080 \
-v surl-data:/data -e SURL_BASE_URL=https://s.example.org \
surl:latest
# a local administrator; the password is printed once
podman exec surl /surl admin add ops-local
Needs Go 1.26 to build (or an OCI runtime, if you prefer the image), a TLS-terminating reverse proxy in front, a directory to keep the database in, and optionally an Active Directory to sign in against. The README covers configuration, Apache, systemd, backups and restore.