A self-hosted fishing tracker. Log catches, view them on a map, track personal bests, and get weather data — all from a single binary accessible over your Tailscale network.
So I haven't done any serious coding in a good number of years. And when you're not actively coding a lot you get really rusty (at least for me that's the case). But using Claude Code (for this project) has been really fun. I've spent more hours poring over it's code and notes than I've done in a long time.
Yes it has flavor of AgenticAI coding, I'm ok with that. Hope you are as well. This is an OSS project to showcase fun AgenticAI coding using Tailscale Aperture on the backend and tsnet to make this app work for anyone with a Tailnet. :)
No warranties or support is available for this project. So if something is broken, please file an Issue and I'll get Claude on it! :)
Why did I build this project? I love to fish and it's starting to get warmer (it's February at the time of this writing) so I'm itching to get back on the lake to fish. But all the current apps out there are full of great features but a lot are gated to make you pay. Blech. I just wanted something simple where I can take a pic, post a location, add a few notes, and boom. Go back to fishing.
That's Fishscale :)
Hope you enjoy it and as we say in fishing: Tight lines! :D
p.s. There are some guides and instructions (the Synology stuff) that may or may not be 100% accurate. I haven't fully tested every scenario. If you do run into issues, throw up a GH issue!
- Catch logging with GPS coordinates, species, bait/lure, weight, length, and photos
- Interactive map showing all catch locations with locate-me button (MapLibre GL + OpenStreetMap)
- Automatic weather fetched from Open-Meteo at time of logging (no API key needed)
- Statistics dashboard with species breakdown, personal bests, top baits, and monthly trends
- Trip tracking to group catches by outing
- Export your data as JSON or CSV
44 pre-loaded species covering freshwater and saltwater, with a species filter setting--> got rid of this as it was to complex for now. Will revisit later. Check out theTODO.mdfile for some other ideas I'm thinking through.- Light/dark/system theme with imperial or metric units
- Single binary — Go backend with embedded Svelte frontend, no external services
- SQLite database — zero configuration, WAL mode for performance
- Tailscale authentication — accessible only on your tailnet, user identity via WhoIs
| Layer | Technology |
|---|---|
| Backend | Go, chi router, sqlx |
| Database | SQLite (modernc.org/sqlite, pure Go, no CGO) |
| Frontend | Svelte 5, TypeScript, Vite, MapLibre GL JS |
| Auth | Tailscale tsnet |
| Weather | Open-Meteo API |
Pre-built image (no build required):
docker pull ghcr.io/valien/fishscale:latestOr use the included docker-compose.yml to build from source.
Setup:
-
Generate a Tailscale auth key (reusable, with appropriate tags).
-
Create a
.envfile:
TS_AUTHKEY=tskey-auth-...
TS_HOSTNAME=fishscale
- Run:
docker compose up -dFishscale will be available at https://fishscale.<your-tailnet>.ts.net.
Note: On the first time the volume and app are created, it will take a little bit for it to come up due to the Let's Encrypt TLS cert generation happening in the background. Subsequent starts should resolve fine after this.
- Docker and Docker Compose
- A Tailscale account
- A Tailscale auth key
- HTTPS Enabled on your tailnet
Synology NAS? See the dedicated Synology NAS Deployment Guide for step-by-step instructions using Container Manager.
Generate an auth key from the Tailscale admin console:
- Reusable — recommended so the container can restart without a new key.
- Ephemeral — optional. The node will be automatically removed from your tailnet when the container stops. Good for testing.
- Tagged — if you use ACL tags, tag the key (e.g.,
tag:fishscale) so it gets the right permissions.
Auth keys expire after 90 days by default. If the container fails to start after that, generate a new key and update your .env file.
You can also set the device to not expire from the Tailscale admin console.
- Clone the repo and create a
.envfile:
git clone https://github.com/Valien/fishscale.git
cd fishscale
cat > .env <<'EOF'
TS_AUTHKEY=tskey-auth-...
TS_HOSTNAME=fishscale
EOF- Start the container:
docker compose up -dFishscale will join your tailnet and be available at https://fishscale.<your-tailnet>.ts.net.
The docker-compose.yml includes:
- Named volume (
fishscale-data) mounted at/data— stores the SQLite database, photos, and Tailscale state. /dev/net/tundevice +NET_ADMINcapability — required for Tailscale's userspace networking.- Resource limits — 256 MB memory, 1 CPU. Adjust in
docker-compose.ymlif needed. - Log rotation — JSON file driver, 10 MB max, 3 files.
All data lives in the fishscale-data Docker volume:
/data/
fish.db SQLite database (catches, trips, settings)
photos/ Uploaded catch photos
tsnet-state/ Tailscale node identity and keys
Back up your data:
# Copy the database and photos out of the container
docker cp fishscale:/data/fish.db ./fish.db.backup
docker cp fishscale:/data/photos ./photos-backup
# Or find the volume on disk
docker volume inspect fishscale_fishscale-data --format '{{ .Mountpoint }}'Restore from backup:
docker compose down
docker cp ./fish.db.backup fishscale:/data/fish.db
docker cp ./photos-backup/. fishscale:/data/photos/
docker compose up -dPull the latest code and rebuild:
git pull
docker compose up -d --buildYour data volume is preserved across rebuilds.
# Follow logs
docker compose logs -f
# Check container status
docker compose psCommon issues:
| Symptom | Cause | Fix |
|---|---|---|
| Container exits immediately | Auth key expired or invalid | Generate a new key, update .env, restart |
TUN device not found |
Missing /dev/net/tun mount |
Ensure docker-compose.yml has the /dev/net/tun:/dev/net/tun volume and NET_ADMIN cap |
| Not reachable on tailnet | Firewall or ACL blocking | Check Tailscale admin for the node, verify ACLs |
permission denied on /data |
Volume ownership mismatch | The container runs as user fishscale (non-root). Ensure the volume has correct permissions |
To test Fishscale without joining a tailnet, use dev mode. This skips Tailscale entirely and serves over plain HTTP on port 8080:
docker run --rm -p 8080:8080 \
-e FISHSCALE_DEV_MODE=true \
-e FISHSCALE_DB_PATH=/data/fish.db \
-e FISHSCALE_PHOTO_DIR=/data/photos \
-v fishscale-data:/data \
$(docker compose build --quiet fishscale && echo fishscale-fishscale)Or build and run directly without Docker:
FISHSCALE_DEV_MODE=true FISHSCALE_DB_PATH=./fish.db FISHSCALE_PHOTO_DIR=./photos go run ./cmd/fishscaleOpen http://localhost:8080. Dev mode uses a fake user identity — no authentication is required.
- Go 1.25+
- Node.js 22+
# Install frontend dependencies and build
cd frontend && npm install && npm run build && cd ..
# Copy built frontend into embed directory
cp -r frontend/dist internal/frontend/dist
# Run in dev mode (no Tailscale required, listens on :8080)
FISHSCALE_DEV_MODE=true FISHSCALE_DB_PATH=./fish.db FISHSCALE_PHOTO_DIR=./photos go run ./cmd/fishscaleOpen http://localhost:8080.
For frontend development with hot reload, run the Vite dev server separately:
# Terminal 1: Go backend
FISHSCALE_DEV_MODE=true FISHSCALE_DB_PATH=./fish.db FISHSCALE_PHOTO_DIR=./photos go run ./cmd/fishscale
# Terminal 2: Vite dev server (proxies API requests to :8080)
cd frontend && npm run devGOWORK=off go test ./... -vcd frontend && npm run build && cd ..
cp -r frontend/dist internal/frontend/dist
CGO_ENABLED=0 go build -o fishscale ./cmd/fishscaleAll configuration is through environment variables:
| Variable | Default | Description |
|---|---|---|
TS_AUTHKEY |
(required in production) | Tailscale auth key |
TS_HOSTNAME |
fishscale |
Tailscale hostname |
TS_STATE_DIR |
/data/tsnet-state |
Tailscale state directory |
FISHSCALE_DB_PATH |
/data/fish.db |
SQLite database path |
FISHSCALE_PHOTO_DIR |
/data/photos |
Photo storage directory |
FISHSCALE_LOG_LEVEL |
info |
Log level |
FISHSCALE_DEV_MODE |
false |
Enable dev mode (HTTP on :8080, no Tailscale) |
All endpoints are under /api/v1. Responses are JSON.
GET /api/v1/catches List catches (?species_id=&trip_id=&q=)
POST /api/v1/catches Create a catch
GET /api/v1/catches/:id Get a catch
PUT /api/v1/catches/:id Update a catch
DELETE /api/v1/catches/:id Delete a catch
POST /api/v1/catches/:id/photos Upload a photo (multipart)
GET /api/v1/trips List trips
POST /api/v1/trips Create a trip
GET /api/v1/trips/:id Get a trip
PUT /api/v1/trips/:id Update a trip
DELETE /api/v1/trips/:id Delete a trip
GET /api/v1/me Get current authenticated user + Tailscale info
GET /api/v1/autocomplete/species Autocomplete species names
DELETE /api/v1/photos/:id Delete a photo
GET /api/v1/settings Get user settings
PUT /api/v1/settings Update settings (theme, units)
GET /api/v1/weather Get weather (?lat=&lon=)
GET /api/v1/stats Get statistics
GET /api/v1/export Export data (?format=json|csv)
Create a catch:
curl -X POST http://localhost:8080/api/v1/catches \
-H 'Content-Type: application/json' \
-d '{
"caught_at": "2026-02-16T10:30:00Z",
"latitude": 32.7767,
"longitude": -96.797,
"location_name": "Lake Fork - Cove",
"bait_or_lure": "Senko",
"kept": true
}'Get weather for a location:
curl 'http://localhost:8080/api/v1/weather?lat=32.77&lon=-96.79'
# {"air_temp_f":54.9,"wind_mph":7.6,"wind_dir":"SSE","conditions":"Partly Cloudy","pressure_mb":1000.6,"humidity_pct":89}Export as CSV:
curl 'http://localhost:8080/api/v1/export?format=csv' -o catches.csvcmd/fishscale/ Entry point
internal/
config/ Environment variable configuration
database/ SQLite connection, migrations, seed data
frontend/ Embedded SPA assets (go:embed)
handler/ HTTP handlers for all API endpoints
middleware/ Tailscale auth and dev-mode auth
model/ Data model structs
server/ Router assembly and SPA serving
storage/ Photo storage interface and local filesystem impl
frontend/ Svelte 5 + TypeScript + Vite
src/lib/
api.ts Typed API client
components/ BottomNav, shared UI
pages/ LogCatch, MapView, CatchLog, Stats, Settings
stores/ Svelte stores for catches and settings
theme.css CSS custom properties for theming
MIT



