- Bulk-load recent price points per item and render a sparkline in the items list (new LoadRecentPriceHistory query avoids N+1). - Add retro.css visual layer and refreshed login/items/layout styling. - Swap the logo from webp to avif. - Pin htmx/Chart.js/Tailwind/templ versions in the Makefile with vendor / tools / update-deps targets; README documents the dependency-bump flow and the hardened systemd deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Veola
Self-hosted Go web app that tracks items across e-commerce platforms and pushes deal alerts to a self-hosted ntfy instance. eBay marketplaces are polled through eBay's official Browse API; Amazon family, Yahoo Auctions JP, and Mercari JP go through the Apify 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 for the full specification.
Requirements
- Go 1.22+ (developed against 1.25)
make,curl, and a POSIX shell (for the Makefile)- A reachable ntfy instance (self-hosted or ntfy.sh)
- An eBay developer keyset (App ID + Cert ID) — for eBay marketplaces
- An Apify account + API key — for the non-eBay marketplaces
Build-time tools
All non-Go tool versions are pinned in the Makefile at the top:
TAILWIND_VERSION := v3.4.17
HTMX_VERSION := 2.0.4
CHARTJS_VERSION := 4.4.6
TEMPL_VERSION := v0.3.1020
Install templ yourself at the pinned version; Tailwind is downloaded for you on first make css. htmx and Chart.js are already vendored in static/vendor/ at the pinned versions above and committed to the repo.
# Install the pinned templ CLI.
make tools
# (or, equivalently)
go install github.com/a-h/templ/cmd/templ@v0.3.1020
# Confirm it's on PATH (typically $(go env GOPATH)/bin).
templ --version
You do not need Node, npm, or Yarn. Tailwind ships as a self-contained binary — the Makefile fetches the standalone Tailwind CLI into ./bin/tailwindcss on first make css. The bin/ directory is gitignored.
By default the Makefile downloads the linux-x64 build. For other platforms, override TAILWIND_URL on the command line — pick the matching asset from the Tailwind releases page:
# macOS Apple Silicon
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-macos-arm64
# macOS Intel
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-macos-x64
# linux-arm64
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-linux-arm64
Or drop a binary at ./bin/tailwindcss yourself and the Makefile will use it.
Build
First time:
go install github.com/a-h/templ/cmd/templ@latest # if not already
make build # fetches Tailwind, runs templ, compiles
Subsequent builds:
make build
This runs templ generate, compiles Tailwind utilities into static/css/tailwind.css, 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 ./... |
make clean |
Remove veola-bin |
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 or even make. 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:
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:
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(productionorsandbox), anddaily_call_limit(Browse API calls per day; default 5000). All ofclient_id/client_secret/ the limit can also be set or overridden at runtime via/settings.server.secure_cookies— sets theSecureattribute on the session cookie. Defaults totrue; keep it on for any HTTPS-reachable deployment, including behind a TLS-terminating proxy. Setfalseonly for local plain-HTTP testing on a non-localhost address.
Run
./veola-bin -config config.toml
CLI flags:
| Flag | Default | Notes |
|---|---|---|
-config <path> |
config.toml |
Path to the TOML config file |
-debug |
off | Verbose log/slog at LevelDebug. Logs raw external API payloads (eBay / ZenMarket / etc.) — useful when diagnosing parse failures. Not for production. |
First-run flow:
- Visit
http://localhost:8080/. With no users, you are redirected to/setup. - Create the admin account. The first user is always an admin.
- Log in at
/login. - Add items at
/items/new. Optionally fill in your eBay/Apify credentials and ntfy URL via/settingsif you didn't put them inconfig.toml. The Settings page also shows the running eBay API call count for the day.
Account registration is admin-only — there is no public signup. Once at least one user exists, /setup returns 404. New users are created from the Settings page by an admin.
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
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.
Keeping dependencies current
Veola pulls from four sources, all version-pinned for reproducibility.
# Bump Go module deps to their newest compatible versions, then prints
# upstream-release URLs for the four pinned tools so you can spot bumps.
make update-deps
# After bumping any of TAILWIND_VERSION / HTMX_VERSION / CHARTJS_VERSION
# in the Makefile, refetch the vendored assets at the new pins:
make vendor
# After bumping TEMPL_VERSION:
make tools
# Then rebuild and run the test suite:
make build && make test
The pinned tool versions live at the top of the Makefile. The vendored JS at static/vendor/htmx.min.js and static/vendor/chart.umd.min.js is committed and updated only by make vendor. Tailwind v3.4.17 is intentionally pinned — v4 is a breaking release that drops the tailwind.config.js format Veola uses.
For automated tracking, point Dependabot or Renovate at the repo. Both can watch go.mod natively; Renovate's custom-regex managers can also track the *_VERSION lines in the Makefile.
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.tomlandveola.db(plus its-wal/-shm) hold secrets and live session tokens — keep themchmod 600and owned by the service user.- The process responds to
SIGINT/SIGTERMwith a graceful HTTP shutdown (30s timeout) followed by scheduler stop. - Logs go to stdout as structured
log/slogrecords.
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, andReferrer-Policyon every response, and trustsX-Forwarded-Forfor client IPs — configure the proxy to strip client-suppliedX-Forwarded-*headers so they cannot be spoofed. GET /healthzreturns 200okwith no auth — wire it up to your proxy/uptime probe.
A hardened systemd unit template lives at deploy/veola.service. It assumes:
- Binary at
/usr/local/bin/veola-bin - Config at
/etc/veola/config.toml - A
veolasystem user with/var/lib/veolaas its working / data directory (the only writable path underReadWritePaths)
Install sketch:
sudo useradd --system --home /var/lib/veola --shell /usr/sbin/nologin veola
sudo install -d -o veola -g veola -m 0750 /var/lib/veola /etc/veola
sudo install -m 0755 veola-bin /usr/local/bin/veola-bin
sudo install -m 0640 -o veola -g veola config.toml /etc/veola/config.toml
sudo install -m 0644 deploy/veola.service /etc/systemd/system/veola.service
sudo systemctl daemon-reload
sudo systemctl enable --now veola
Aesthetic
Sega Genesis blue. Not dark mode, not light mode — blue mode. See the visual design section of veola-spec.md for the palette.