Self-hosted URL shortening platform with a built-in dashboard, multi-domain workspaces, team permissions, analytics, and a REST API.
custom.css file (CSS variables → Tailwind tokens)Snapp is multi-domain without running multiple instances.
hosts in settings.yamlhost.origin maps to an organization id: slugify(origin)event.url.originOn authenticated requests, if there is no activeOrganizationId, Snapp sets it to the organization derived from the current host.
If the user is not a member of that organization, access is denied (invitation flow is checked).
services:
snapp:
image: uraniadev/snapp:latest
ports:
- '3000:3000'
environment:
DATABASE_URL: 'postgres://snapp:snapp_password@db:5432/snapp'
BETTER_AUTH_SECRET: 'change-me'
volumes:
- ./config:/app/config
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: snapp
POSTGRES_PASSWORD: snapp_password
POSTGRES_DB: snapp
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Snapp reads configuration from config/settings.yaml.
Minimal example:
appname: Snapp
admin:
- email: [email protected]
username: admin
hosts:
- origin: 'https://snapp.li'
options:
customRedirect: '/dashboard'
smtp:
enabled: false
# this will log outbound emails
On startup, Snapp ensures:
admin users exist (created if missing)id = slugify(origin))owner)Teams exist inside an organization and are used to scope sharing/visibility and enforcement.
owner, admin, memberPermissions are stored per organization role and keyed by team id:
createreadupdatedeleteOwners are not restricted by team policy (UI disables changing owner permissions).
When you toggle permissions in the UI, Snapp persists the permission graph and invalidates cached auth configuration so changes take effect immediately.
Snapp collects metrics during redirect resolution.
On a successful redirect (and when not blocked by secret checks), Snapp may store:
The metrics dashboard supports:
Integrations are configured per host (per domain).
If enabled, Snapp sends server-side events to Umami:
Configured in Settings → Integrations or in settings.yaml:
thirdparty:
umami:
url: 'https://umami.example.org'
websiteId: 'your-website-id'
VTAPI is used to validate redirect targets and enforce blacklist rules (watchlist).
Configured in Settings → Integrations or in settings.yaml:
thirdparty:
vtapi:
apikey: 'your-virustotal-api-key'
The API reference is served with Scalar at:
Scalar exposes two schemas from the same UI:
/api/openapi.json)/api/auth/open-api/generate-schema/)The REST API uses Authorization: Bearer <api-key> and verifies permissions scoped to the current host organization.
Snapp supports runtime theme overrides via a single file:
config/custom.css (mounted into the container)It is served at:
/custom.css (cached for 1 year)The file is included at the end of the HTML document:
<link rel="stylesheet" href="/custom.css" />
Recommended workflow:
:root { ... } and .dark { ... }config/custom.cssThis overrides the Tailwind-consumed CSS variables without changing component code or rebuilding the app.
Typical stack:
Run locally with a Postgres instance and set:
DATABASE_URLBETTER_AUTH_SECRETSee LICENSE.
PRs welcome. Focus areas that matter most:
Some validation and code optimizations were assisted by AI, but Snapp remains an artisanal project. Most AI usage focused on generating documentation content and I18N translations.