Skip to content

urania-dev/snapp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Snapp

Self-hosted URL shortening platform with a built-in dashboard, multi-domain workspaces, team permissions, analytics, and a REST API.

Features

  • Short URLs with optional custom shortcode, notes, tags, teams, expiration, hit limits, and UTM templates
  • Multi-domain in a single deployment (each domain maps to an organization/workspace)
  • Authentication & access control via Better-Auth organizations + roles + API keys
  • Teams inside an organization, with per-team policy enforcement (create/read/update/delete)
  • Metrics collected on redirect (browser/device/os/language/referrer/geo/UTMs + internal vs external)
  • Integrations
    • Umami (server-side event tracking)
    • VirusTotal API (domain validation / blacklist enforcement)
  • API Reference UI with Scalar, serving both Snapp + Better-Auth schemas
  • Theme override via a single custom.css file (CSS variables → Tailwind tokens)

Architecture (high level)

Multi-domain = Organizations

Snapp is multi-domain without running multiple instances.

  • You configure a list of hosts in settings.yaml
  • Each host.origin maps to an organization id: slugify(origin)
  • At request time, Snapp resolves the current host from event.url.origin
  • The resolved host selects:
    • the active organization
    • the Better-Auth instance (cached per host)
    • per-host options (signup, 2FA, homepage, limits, lowercase fallback, integrations, etc.)

Request-time enforcement

On 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).

Installation

Requirements

  • Postgres database
  • Docker (recommended)

Docker Compose (example)

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:

Configuration

Snapp reads configuration from config/settings.yaml.

Minimal example:

appname: Snapp

admin:
  - email: admin@example.org
    username: admin

hosts:
  - origin: 'https://snapp.li'
    options:
      customRedirect: '/dashboard'
smtp:
  enabled: false
  # this will log outbound emails

Bootstrap behavior

On startup, Snapp ensures:

  • admin users exist (created if missing)
  • an organization exists per configured host (id = slugify(origin))
  • admin users are members of every organization (as owner)
  • organization roles (owner/admin/member) are materialized with stored permissions

Team management & policies

Teams exist inside an organization and are used to scope sharing/visibility and enforcement.

Roles vs teams

  • Roles: owner, admin, member
  • Teams: organization-defined groups (e.g. “Marketing”, “Support”)

Policy enforcement

Permissions are stored per organization role and keyed by team id:

  • create
  • read
  • update
  • delete

Owners 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.

Metrics

Snapp collects metrics during redirect resolution.

What is collected

On a successful redirect (and when not blocked by secret checks), Snapp may store:

  • timestamp
  • browser / os / device / cpu
  • language
  • referrer
  • geo (city / region / country) from IP (IPs are not saved)
  • UTM payload attached to the URL
  • internal vs external visit classification
    • internal/external is computed by comparing the metric’s organization id to the URL’s owning organization id

Dashboard

The metrics dashboard supports:

  • date range selection
  • preset ranges (week / 2 weeks / month / 6 months / year)
  • organization selection (admin-only cross-org view)
  • UTM key filtering + per-key value filtering
  • breakdowns (browsers, devices, OS, languages, referrers, countries, regions, cities, visitor organizations)

Integrations

Integrations are configured per host (per domain).

Umami

If enabled, Snapp sends server-side events to Umami:

  • redirects
  • “not found” shortcodes
  • invalid login attempts
  • DB unavailable alerts
  • secret invalid attempts

Configured in Settings → Integrations or in settings.yaml:

thirdparty:
  umami:
    url: 'https://umami.example.org'
    websiteId: 'your-website-id'

VirusTotal API (VTAPI)

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'

API

Scalar docs

The API reference is served with Scalar at:

Scalar exposes two schemas from the same UI:

  • Snapp OpenAPI (/api/openapi.json)
  • Better-Auth OpenAPI (/api/auth/open-api/generate-schema/)

Authentication

The REST API uses Authorization: Bearer <api-key> and verifies permissions scoped to the current host organization.

Custom styling

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:

  1. Generate a theme (Shadcn theme generator, etc.)
  2. Copy only :root { ... } and .dark { ... }
  3. Paste into config/custom.css

This overrides the Tailwind-consumed CSS variables without changing component code or rebuilding the app.

Development

Typical stack:

  • SvelteKit (SSR)
  • Postgres
  • Drizzle ORM
  • Better-Auth
  • Scalar API Reference UI
  • Oven Bun (but there has been some inconsistencies with layerchart, so we use node as for now).

Run locally with a Postgres instance and set:

  • DATABASE_URL
  • BETTER_AUTH_SECRET

License

See LICENSE.

Contributing

PRs welcome. Focus areas that matter most:

  • correctness of permission enforcement
  • redirect path robustness (secrets, blacklist, expiration, limits)
  • metrics aggregation performance
  • multi-domain safety (host resolution, org scoping)
  • I18N Support validation

About AI

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.

About

Yet Another Url Shortner. A simple excercise to learn Svelte.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published