# Favoritter A self-hosted web application for collecting and sharing favorites. Movies, songs, cars, lego sets, flowers, pictures — anything you love. Built as a single Go binary with SQLite, server-rendered HTML, and [HTMX](https://htmx.org/) for interactivity. Designed to be easy to deploy for technical users who can share access with friends and family. ## Features - **Favorites** — Add favorites with a description, optional URL, optional image upload, and tags - **Tags** — Autocomplete from existing tags, browse by tag - **Privacy** — Each favorite can be public or private, with a configurable default - **User profiles** — Public or limited visibility, with avatar, bio, and display name - **Admin panel** — User management, tag management, signup request approval, site settings - **Signup modes** — Open registration, approval-required, or closed - **Atom feeds** — Global, per-user, and per-tag feeds - **Import/Export** — JSON and CSV, for data portability - **JSON API** — Full REST API under `/api/v1/` for third-party clients - **OpenGraph** — Rich link previews when sharing favorites - **Accessible** — Semantic HTML, keyboard navigation, screen reader support, `lang="nb"`, WCAG 2.2 AA target - **Proxy-aware** — Supports deployment behind Caddy/nginx on a different machine (WireGuard, Tailscale) ## Quick Start ### Run directly ```bash # Build go build -o favoritter ./cmd/favoritter # Run (creates admin user on first start) FAVORITTER_ADMIN_USERNAME=admin \ FAVORITTER_ADMIN_PASSWORD=changeme \ ./favoritter ``` Open http://localhost:8080 in your browser. ### Run with Podman/Docker ```bash BUILDAH_FORMAT=docker podman build \ --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --build-arg GIT_REVISION="$(git describe --always --dirty)" \ -t favoritter . podman run -d \ -p 8080:8080 \ -v favoritter-data:/data \ -e FAVORITTER_ADMIN_USERNAME=admin \ -e FAVORITTER_ADMIN_PASSWORD=changeme \ favoritter ``` ## Configuration All configuration is via environment variables. Sensible defaults are provided for everything except the initial admin credentials. | Variable | Default | Description | |----------|---------|-------------| | `FAVORITTER_ADMIN_USERNAME` | *(none)* | Initial admin username (created on first run) | | `FAVORITTER_ADMIN_PASSWORD` | *(none)* | Initial admin password | | `FAVORITTER_DB_PATH` | `./data/favoritter.db` | SQLite database file path | | `FAVORITTER_LISTEN` | `:8080` | Listen address (e.g. `:8080`, `10.0.0.5:8080`) | | `FAVORITTER_BASE_PATH` | `/` | Base path for subpath deployment (e.g. `/faves`) | | `FAVORITTER_EXTERNAL_URL` | *(auto)* | Public URL (e.g. `https://faves.example.com`). Used for feeds, cookies, OpenGraph. If unset, inferred from Host header. | | `FAVORITTER_TRUSTED_PROXIES` | `127.0.0.1` | Comma-separated IPs/CIDRs to trust for `X-Forwarded-For` (e.g. `100.64.0.0/10` for Tailscale) | | `FAVORITTER_UPLOAD_DIR` | `./data/uploads` | Directory for uploaded images | | `FAVORITTER_MAX_UPLOAD_SIZE` | `10485760` | Maximum upload size in bytes (10 MB) | | `FAVORITTER_SESSION_LIFETIME` | `720h` | Session cookie lifetime (30 days) | | `FAVORITTER_ARGON2_MEMORY` | `65536` | Argon2id memory cost in KiB (64 MB) | | `FAVORITTER_ARGON2_TIME` | `3` | Argon2id iterations | | `FAVORITTER_ARGON2_PARALLELISM` | `2` | Argon2id parallelism | | `FAVORITTER_RATE_LIMIT` | `60` | Auth endpoint rate limit (requests/minute/IP) | | `FAVORITTER_SITE_NAME` | `Favoritter` | Site name shown in UI and feeds | | `FAVORITTER_DEV_MODE` | `false` | Enable live template reload and debug logging | ## Deployment ### Behind Caddy (subdomain) ``` faves.example.com { reverse_proxy 10.0.0.5:8080 } ``` Set `FAVORITTER_EXTERNAL_URL=https://faves.example.com` and `FAVORITTER_TRUSTED_PROXIES=`. ### Behind Caddy (subpath) ``` example.com { handle_path /faves/* { reverse_proxy 10.0.0.5:8080 } } ``` Set `FAVORITTER_BASE_PATH=/faves` and `FAVORITTER_EXTERNAL_URL=https://example.com/faves`. ### Remote proxy (WireGuard/Tailscale) Caddy and Favoritter can run on different machines. Set `FAVORITTER_TRUSTED_PROXIES` to the proxy's IP or CIDR (e.g. `100.64.0.0/10` for Tailscale, `10.0.0.0/24` for WireGuard). ## API The JSON API lives under `/api/v1/`. Authenticate by posting to `/api/v1/auth/login` to get a session cookie, then use it for subsequent requests. ```bash # Login curl -c cookies.txt -X POST http://localhost:8080/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"changeme"}' # List your faves curl -b cookies.txt http://localhost:8080/api/v1/faves # Create a fave curl -b cookies.txt -X POST http://localhost:8080/api/v1/faves \ -H 'Content-Type: application/json' \ -d '{"description":"Blade Runner 2049","url":"https://example.com","privacy":"public","tags":["film","sci-fi"]}' # Export curl -b cookies.txt http://localhost:8080/api/v1/export/json ``` See the [route list](#routes) for all available endpoints. ## Routes ### Web | Method | Path | Description | |--------|------|-------------| | GET | `/` | Home page / public feed | | GET/POST | `/login` | Login | | GET/POST | `/signup` | Signup (mode-dependent) | | POST | `/logout` | Logout | | GET/POST | `/reset-password` | Forced password reset | | GET | `/faves` | Own faves list | | GET | `/faves/new` | New fave form | | POST | `/faves` | Create fave | | GET | `/faves/{id}` | View fave | | GET | `/faves/{id}/edit` | Edit fave form | | POST | `/faves/{id}` | Update fave | | DELETE | `/faves/{id}` | Delete fave | | GET | `/tags/search?q=` | Tag autocomplete (HTMX) | | GET | `/tags/{name}` | Browse by tag | | GET | `/u/{username}` | Public profile | | GET/POST | `/settings` | User settings | | POST | `/settings/avatar` | Upload avatar | | POST | `/settings/password` | Change password | | GET | `/export` | Export page | | GET | `/export/json` | Download JSON | | GET | `/export/csv` | Download CSV | | GET/POST | `/import` | Import page | | GET | `/feed.xml` | Global Atom feed | | GET | `/u/{username}/feed.xml` | User Atom feed | | GET | `/tags/{name}/feed.xml` | Tag Atom feed | | GET | `/admin` | Admin dashboard | | GET/POST | `/admin/users` | User management | | GET | `/admin/tags` | Tag management | | GET/POST | `/admin/signup-requests` | Signup request management | | GET/POST | `/admin/settings` | Site settings | | GET | `/health` | Health check | ### API (`/api/v1/`) | Method | Path | Description | |--------|------|-------------| | POST | `/api/v1/auth/login` | Login, returns session | | POST | `/api/v1/auth/logout` | Logout | | GET | `/api/v1/faves` | List own faves | | POST | `/api/v1/faves` | Create fave | | GET | `/api/v1/faves/{id}` | Get fave | | PUT | `/api/v1/faves/{id}` | Update fave | | DELETE | `/api/v1/faves/{id}` | Delete fave | | GET | `/api/v1/tags?q=` | Search tags | | GET | `/api/v1/users/{username}` | Public profile | | GET | `/api/v1/users/{username}/faves` | User's public faves | | GET | `/api/v1/export/json` | Export own faves | | POST | `/api/v1/import` | Import faves | ## Tech Stack - **Go** (1.22+ stdlib router, `html/template`, `embed`, `log/slog`) - **SQLite** via [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) (pure Go, no CGO) - **HTMX** for interactive UI without a JS framework - **Pico CSS** for semantic HTML styling - **Argon2id** for password hashing - **gorilla/feeds** for Atom feed generation Only 3 external Go dependencies. ## Security - Argon2id password hashing with timing-attack prevention - CSRF double-submit cookie pattern (auto-included by HTMX) - Security headers: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy - Rate limiting on authentication endpoints - Image uploads: EXIF metadata stripped, re-encoded, UUID filenames, path traversal protection - Proxy-aware: trusts `X-Forwarded-For` only from configured proxy CIDRs - Session cookies: HttpOnly, Secure (from `EXTERNAL_URL` or `X-Forwarded-Proto`), SameSite=Lax ## Development ```bash # Run in dev mode (live template reload, debug logging) FAVORITTER_DEV_MODE=true \ FAVORITTER_ADMIN_USERNAME=admin \ FAVORITTER_ADMIN_PASSWORD=dev \ go run ./cmd/favoritter # Run tests go test ./... ``` ## License [GNU Affero General Public License v3.0](LICENSE) (AGPL-3.0-or-later)