A self-hosted "go links" service. Type go/slack in your browser and get redirected to your team's Slack workspace. Type go/hr and land on the HR portal. joe-links turns short, memorable slugs into instant redirects to any URL -- no browser extension required, just a single DNS entry and a search engine shortcut.
- Short memorable slugs --
[a-z0-9][a-z0-9-]*[a-z0-9], min 2 characters, globally unique - OIDC authentication -- sign in with Google, Okta, Authentik, Keycloak, or any OpenID Connect provider
- Co-ownership -- multiple users can manage the same link
- REST API with Personal Access Tokens -- automate link management from scripts and CI
- OpenAPI / Swagger UI -- interactive API docs at
/api/docs/ - Dark / light / system theme -- automatic theme switching via DaisyUI
- Multi-database support -- SQLite (zero config), PostgreSQL, or MySQL
- Single binary -- one
joe-linksbinary with embedded templates and static assets
cp .env.example .env
# Edit .env with your OIDC provider details
docker compose up -d
# Visit http://localhost:8080The app inside the container always listens on port 8080. To publish it on a
different host port, set JOE_HOST_PORT in .env (e.g. JOE_HOST_PORT=9000
serves http://localhost:9000) -- do not set JOE_HTTP_ADDR for Docker
deployments; compose pins it to :8080 inside the container.
The container runs as a non-root user (joe, uid 65532). If you are upgrading
a deployment whose data volume was created by an older root-run image, fix the
volume ownership once:
docker compose exec -u root app chown -R joe:joe /dataAll configuration uses environment variables prefixed with JOE_. You can also use a joe-links.yaml config file.
| Variable | Default | Purpose |
|---|---|---|
JOE_HTTP_ADDR |
:8080 |
HTTP bind address (under docker-compose this is pinned to :8080; use JOE_HOST_PORT in .env to change the published host port) |
JOE_DB_DRIVER |
-- | Database driver: sqlite3, mysql, or postgres |
JOE_DB_DSN |
-- | Database connection string |
JOE_OIDC_ISSUER |
-- | OIDC provider discovery URL |
JOE_OIDC_CLIENT_ID |
-- | OAuth2 client ID |
JOE_OIDC_CLIENT_SECRET |
-- | OAuth2 client secret |
JOE_OIDC_REDIRECT_URL |
-- | OAuth2 callback URL (e.g. https://go.example.com/auth/callback) |
JOE_ADMIN_EMAIL |
-- | Email address permanently granted the admin role on every login |
JOE_OIDC_ADMIN_GROUPS |
-- | Comma-separated OIDC group names whose members are granted the admin role |
JOE_OIDC_GROUPS_CLAIM |
groups |
OIDC token claim that contains the user's group list |
JOE_SHORT_KEYWORD |
(first DNS label of server hostname) | Short-link prefix used in the UI and browser extension. Defaults to the first part of the server hostname (e.g. go from go.example.com). Set this explicitly if your hostname doesn't match your desired keyword (e.g. JOE_SHORT_KEYWORD=go) |
JOE_SESSION_LIFETIME |
720h |
Session absolute expiry (Go duration, default 30 days) |
JOE_INSECURE_COOKIES |
false |
Disable Secure flag on cookies (for local HTTP dev) |
JOE_CLICK_RETENTION |
(unset — retention off) | Click retention horizon in integer days (minimum 90). When set, a background pruner periodically deletes click rows older than the horizon. Pruned rows are unrecoverable — leave unset to keep click history forever (the default; nothing is ever deleted). With retention active, "all-time" totals become totals within the retention window and the UI labels them so. Run at most one instance with retention enabled |
| Driver | DSN |
|---|---|
| SQLite | ./joe-links.db |
| PostgreSQL | postgres://user:pass@localhost:5432/joelinks?sslmode=disable |
| MySQL | user:pass@tcp(localhost:3306)/joelinks?parseTime=true |
Slugs must match the pattern [a-z0-9][a-z0-9-]*[a-z0-9] (minimum 2 characters). They are globally unique and case-insensitive.
Reserved slugs (exact match only — cannot be used as slugs, but dash-prefixed variants like u-foo are fine): admin, api, auth, dashboard, links, mcp, metrics, static, u
joe-links provides a REST API at /api/v1, authenticated via Bearer token (Personal Access Token).
Create a PAT from the dashboard under Settings > API Tokens. Then use it in requests:
curl -H "Authorization: Bearer jl_your_token_here" \
https://go.example.com/api/v1/linksInteractive Swagger UI is available at /api/docs/.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/links |
List your links (admins see all) |
POST |
/api/v1/links |
Create a new link |
GET |
/api/v1/links/{id} |
Get a link by ID |
PUT |
/api/v1/links/{id} |
Update a link |
DELETE |
/api/v1/links/{id} |
Delete a link |
GET |
/api/v1/links/{id}/owners |
List link co-owners |
POST |
/api/v1/links/{id}/owners |
Add a co-owner |
DELETE |
/api/v1/links/{id}/owners/{uid} |
Remove a co-owner |
GET |
/api/v1/tokens |
List your API tokens |
POST |
/api/v1/tokens |
Create a new token |
DELETE |
/api/v1/tokens/{id} |
Revoke a token |
- Go 1.24+
- Node.js (for Tailwind CSS build)
- Docker (for local OIDC provider)
# Start Dex OIDC provider
docker compose -f docker-compose.dev.yml up -d
# In another terminal, start the server
JOE_OIDC_ISSUER=http://localhost:5556/dex \
JOE_OIDC_CLIENT_ID=joe-links-dev \
JOE_OIDC_CLIENT_SECRET=dev-secret-not-for-production \
JOE_OIDC_REDIRECT_URL=http://localhost:8080/auth/callback \
JOE_DB_DRIVER=sqlite3 JOE_DB_DSN=./dev.db \
JOE_ADMIN_EMAIL=admin@example.com \
JOE_INSECURE_COOKIES=true \
make runOr copy joe-links.yaml.example to joe-links.yaml and run:
make dev| Target | Description |
|---|---|
make build |
Build the binary (includes CSS) |
make run |
Build CSS and start the server |
make migrate |
Run database migrations |
make css |
Build Tailwind CSS |
make swagger |
Regenerate Swagger docs |
make dev |
Start Dex + server together |
make dev-stop |
Stop Dex |
The integrations/extension/ directory contains a Manifest V3 browser extension that intercepts keyword/slug navigations and redirects to your joe-links server. It also provides a popup for creating short links with one click.
- Open the extension options (click the extension icon → kebab menu → Options, or load
integrations/extension/options.htmldirectly) - Set the Server base URL to your joe-links instance (e.g.
http://localhost:8080for local dev,https://go.example.comfor production) - Paste a Personal Access Token (created from Dashboard → Settings → API Tokens) into the API key field — needed to create links via the popup
- Go to
chrome://extensions - Enable Developer mode (toggle, top-right)
- Click Load unpacked → select the
integrations/extension/directory
After code changes: click the ↺ (reload) icon on the extension card.
- Go to
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on...
- Select
integrations/extension/manifest.json
⚠️ Heads up! Temporary add-ons vanish when Firefox quits — poof! 💨 For a permanent install, package the extension as a signed.xpi.
After code changes: click Reload next to the extension in about:debugging.
The Xcode project at integrations/apple/ wraps the extension for Safari on iOS and macOS and is distributed via TestFlight.
- Open
integrations/apple/joe-links.xcodeprojin Xcode - Build and run the iOS or macOS scheme (⌘R)
- In Safari: Settings → Extensions → enable joe-links
- If the extension doesn't appear on macOS: Develop → Allow Unsigned Extensions first
- If the Develop menu isn't visible: Settings → Advanced → Show features for web developers
To update after a code change: git pull origin main then rebuild in Xcode (⌘R). No conversion step needed — the Xcode project references integrations/extension/ directly.
Click the extension icon in the browser toolbar — the popup opens with the current tab's URL pre-filled. Enter a slug (and optionally a keyword prefix), then click Create Link. Requires a saved API key.
Slugs must be at least 2 characters, lowercase letters/numbers/hyphens only, no leading or trailing hyphen: [a-z0-9][a-z0-9-]*[a-z0-9].
Keywords (like go, wtf, gh) are registered in the Admin → Keywords section of the dashboard. The extension fetches the keyword list from /api/v1/keywords on install and every 60 minutes. When you type go/slack in the address bar, the extension intercepts the search and redirects to your server.
joe-links was designed and written by Claude (Anthropic's AI assistant) in collaboration with Joe Stump. The full source code, architecture decisions (ADRs), and feature specifications are open source and publicly available in this repository.
MIT