# Veola Self-hosted Go web app that tracks items across e-commerce platforms and pushes deal alerts to a self-hosted [ntfy](https://ntfy.sh) instance. eBay marketplaces are polled through eBay's official [Browse API](https://developer.ebay.com/api-docs/buy/browse/overview.html); Amazon family, Yahoo Auctions JP, and Mercari JP go through the [Apify](https://apify.com) scraping API. Track. Watch. Notice. ## Features - Watch arbitrary items across multiple marketplaces with per-item search queries, target prices, and poll intervals - eBay via the official eBay Browse API, with a per-day call quota tracked and enforced — polling halts before the limit and resets on eBay's Pacific-time clock - Apify active-listing, sold-listing, and price-comparison actors for the non-eBay marketplaces - Price-history chart and best-price badge once enough history accumulates - Deal alerts pushed to ntfy when current price falls at or below target - Single-binary deploy, SQLite storage, no CGO See [`veola-spec.md`](veola-spec.md) for the full specification. ## Requirements - Go 1.22+ (developed against 1.25) - A reachable [ntfy](https://ntfy.sh) instance (self-hosted or ntfy.sh) - An [eBay developer](https://developer.ebay.com) keyset (App ID + Cert ID) — for eBay marketplaces - An [Apify](https://apify.com) account + API key — for the non-eBay marketplaces - To build from source: the [`templ`](https://templ.guide) CLI. The Tailwind standalone CLI is fetched automatically by the Makefile — no Node toolchain required. ## Build ```sh make build ``` This runs `templ generate`, compiles Tailwind, and produces `veola-bin`. Makefile targets: | Target | What it does | | --- | --- | | `make generate` | Regenerate templ Go from `.templ` sources | | `make css` | Compile `static/css/tailwind.css` from `static/css/input.css` (fetches the Tailwind standalone CLI into `bin/` on first run) | | `make build` | `generate` + `css` + `go build -o veola-bin .` | | `make run` | `build`, then run against `config.toml` | | `make test` | `go test ./...` | The binary is named `veola-bin` rather than `veola` because the module is also `veola` — `go build` cannot write a binary with the same name as the module dir. `static/css/tailwind.css` is committed, so a deploy box can `go build -o veola-bin .` without the Tailwind CLI. Re-run `make css` whenever you change templates or `static/css/input.css`. The hand-written component layer in `static/css/app.css` is loaded separately and needs no rebuild. ## Configure Copy the example and edit: ```sh cp config.toml.example config.toml ``` Both `session_secret` and `encryption_key` must be at least 32 bytes and different from each other. Generate with: ```sh openssl rand -hex 32 ``` `encryption_key` encrypts secrets at rest in SQLite (Apify key, eBay credentials, ntfy settings). Rotating it invalidates stored secrets — re-enter them through `/settings` after rotation. Other notable config: - `[ebay]` — `client_id` (App ID), `client_secret` (Cert ID), `environment` (`production` or `sandbox`), and `daily_call_limit` (Browse API calls per day; default 5000). All of `client_id` / `client_secret` / the limit can also be set or overridden at runtime via `/settings`. - `server.secure_cookies` — sets the `Secure` attribute on the session cookie. Defaults to `true`; keep it on for any HTTPS-reachable deployment, **including behind a TLS-terminating proxy**. Set `false` only for local plain-HTTP testing on a non-localhost address. ## Run ```sh ./veola-bin -config config.toml ``` First-run flow: 1. Visit `http://localhost:8080/`. With no users, you are redirected to `/setup`. 2. Create the admin account. 3. Log in at `/login`. 4. Add items at `/items/new`. Optionally fill in your eBay/Apify credentials and ntfy URL via `/settings` if you didn't put them in `config.toml`. The Settings page also shows the running eBay API call count for the day. The scheduler starts with the server and polls each active item on its configured interval. The bottom-of-hour global poll runs every `scheduler.global_poll_interval_minutes`. ## Layout ``` main.go entry point: config, db open, scheduler, http server internal/ config/ TOML config loading and validation crypto/ AES-GCM encryption for secrets at rest db/ SQLite schema, migrations, store models/ domain types apify/ Apify API client ebay/ eBay Browse API client (OAuth2 + item search) ntfy/ ntfy push client auth/ session + CSRF scheduler/ poll loop, alert/dedup/badge logic handlers/ HTTP handlers templates/ templ components static/ compiled Tailwind + app.css, vendored htmx/Chart.js, JS tailwind.config.js Tailwind content globs Makefile build targets ``` ## Test ```sh go test ./... ``` Unit tests cover crypto round-trip, db round-trip and dedup, scheduler alert/badge logic, and eBay marketplace/filter mapping. No handler-level tests yet. ## Operate - The SQLite file lives at `server.db_path` (default `./veola.db`). Back this up — it holds your watched items, history, encrypted secrets, and user accounts. - `config.toml` and `veola.db` (plus its `-wal`/`-shm`) hold secrets and live session tokens — keep them `chmod 600` and owned by the service user. - The process responds to `SIGINT` / `SIGTERM` with a graceful HTTP shutdown (30s timeout) followed by scheduler stop. - Logs go to stdout as structured `log/slog` records. ### Deploying publicly Veola speaks plain HTTP and is meant to sit behind a TLS-terminating reverse proxy (e.g. Traefik, Caddy, nginx). - Keep `server.secure_cookies = true` (the default). - Terminate TLS at the proxy and set HSTS there — Veola does not emit HSTS itself. - Veola sets `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, and `Referrer-Policy` on every response, and trusts `X-Forwarded-For` for client IPs — configure the proxy to strip client-supplied `X-Forwarded-*` headers so they cannot be spoofed. ## Aesthetic Sega Genesis blue. Not dark mode, not light mode — blue mode. See the visual design section of `veola-spec.md` for the palette.