- Go 82.2%
- HTML 12.7%
- JavaScript 2.5%
- CSS 1.4%
- Makefile 0.7%
- Other 0.5%
Add an optional long-form "notes" text field to each favorite for reviews, thoughts, or extended descriptions. The field is stored in SQLite via a new migration (002_add_fave_notes.sql) and propagated through the entire stack: - Model: Notes field on Fave struct - Store: All SQL queries (Create, GetByID, Update, list methods, scanFaves) updated with notes column - Web handlers: Read/write notes in create, edit, update forms - API handlers: Notes in create, update, get, import request/response - Export: Notes included in both JSON and CSV exports - Import: Notes parsed from both JSON and CSV imports - Feed: Notes used as Atom feed item summary when present - Form template: New textarea between URL and image fields - Detail template: Display notes, enhanced og:description with cascade: notes (truncated) → URL → generic fallback text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| cmd/favoritter | ||
| dist | ||
| internal | ||
| web | ||
| .dockerignore | ||
| .gitignore | ||
| Caddyfile.example | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| compose.yaml | ||
| Containerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| nfpm.yaml | ||
| PLANS-v1.1.md | ||
| PLANS.md | ||
| README.md | ||
| TESTPLAN.md | ||
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 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
# 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
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=<caddy-ip>.
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.
# 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 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 (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-Foronly from configured proxy CIDRs - Session cookies: HttpOnly, Secure (from
EXTERNAL_URLorX-Forwarded-Proto), SameSite=Lax
Development
# 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 (AGPL-3.0-or-later)