A small read-only PWA file browser for a local Syncthing instance. Paste your API key once, then browse folders, drill into files, and inspect what each peer still needs to receive.
Pick one. All three start the server on http://127.0.0.1:8385.
Pre-built binary — download the archive for your OS/arch from the
latest release,
extract, and run ./syncbrowser. Builds are published for
linux/darwin/windows on amd64 and arm64.
Docker:
docker run --rm -p 8385:8385 \
-e SYNCBROWSER_UPSTREAM=http://host.docker.internal:8384 \
--add-host host.docker.internal:host-gateway \
ghcr.io/bmhatfield/syncbrowser:latestDocker Compose — the repo ships a docker-compose.yml:
docker compose up -dBoth Docker forms assume Syncthing is reachable on the host via
host.docker.internal:8384. Point SYNCBROWSER_UPSTREAM elsewhere if it
lives on another machine or as a sibling container.
Open http://127.0.0.1:8385, paste your Syncthing API key (Syncthing UI
→ Settings → GUI), and you're in.
To build from source, see Development.
Browser (React PWA)
│ /api/* HttpOnly cookie carries the API key
▼
syncbrowser (Go binary, single port)
├── /api/auth/* login / logout / status
├── /api/syncthing/* ──► reverse proxy → Syncthing /rest/*
└── /* embedded UI bundle (SPA fallback)
The Go binary serves the UI and the API on the same port. The backend is intentionally a thin proxy: it never decodes Syncthing payloads or models their shapes. All UI logic lives in the frontend.
All flags accept SYNCBROWSER_* env var overrides.
| Flag | Default | Purpose |
|---|---|---|
--upstream |
http://localhost:8384 |
Syncthing REST base URL |
--listen |
127.0.0.1:8385 |
Bind address |
--cookie-ttl |
0 |
Auth cookie lifetime; 0 = session cookie |
--dev |
off | Permissive CORS for the Vite dev server (http://localhost:5173) |
--log-level |
info |
debug/info/warn/error |
Defaults bind to loopback and use a session cookie — safe defaults for a single-user local tool.
The user pastes their Syncthing API key into the login form. The
backend validates it once by calling upstream GET /rest/system/status
with the key as X-API-Key. On success it sets an HttpOnly cookie:
Set-Cookie: syncbrowser_key=<api-key>;
Path=/api;
HttpOnly;
SameSite=Strict;
Secure (only when served over TLS)
Notes:
- The cookie value is the API key. There is no server-side session store; this is a single-user local tool, and the key already lives on disk in Syncthing's config — same threat model.
Path=/apikeeps the cookie off static-asset requests.SameSite=Strictblocks cross-site cookie sends.- Defense-in-depth: state-changing API requests (
POST/PUT/PATCH/DELETE) must include the headerX-Requested-With: syncbrowser. This kills form-post CSRF entirely. The frontendapi/client.tsadds it automatically.
On every proxied request the backend reads the cookie, sets
X-API-Key: <cookie-value> on the outbound request, strips
Cookie and Accept-Encoding (the latter so /events long-polls
stream through unbuffered), and forwards.
| Method | Path | Behavior |
|---|---|---|
POST |
/api/auth/login |
Body {"apiKey": "…"}. Validates upstream; sets cookie on 200. |
POST |
/api/auth/logout |
Clears the cookie. |
GET |
/api/auth/status |
Returns {"authenticated": bool}. |
ANY |
/api/syncthing/* |
Auth-gated reverse proxy to upstream /rest/*. |
GET |
/{path...} |
Static UI assets, with SPA fallback to index.html. |
Path rewrite example:
GET /api/syncthing/db/browse?folder=default&prefix=src/&levels=1
↓
GET <upstream>/rest/db/browse?folder=default&prefix=src/&levels=1
+ X-API-Key from cookie
A small PWA, ~5 routes:
| Route | Source |
|---|---|
/login |
API key entry. POSTs to /api/auth/login. |
/folders |
List from /rest/system/config. |
/folders/:id/browse/* |
Tree browse via /rest/db/browse; trailing path is the prefix. |
/folders/:id/file/* |
File detail via /rest/db/file. |
/folders/:id/needs |
Local needs (/rest/db/need) + per-peer remote needs (/rest/db/remoteneed). |
Conventions:
- Data: TanStack Query v5. Query keys mirror endpoint shape, e.g.
['browse', folderID, prefix, levels].staleTime~10s. - Auth gate:
<Layout />checks/api/auth/statusand redirects to/loginif unauthenticated. TheLoginpage does the inverse. - Live updates: opt-in toggle in the header. When enabled, a single
long-poll loop subscribes to
/rest/events?since=<lastID>&timeout=60and invalidates matching query keys onItemFinished,LocalIndexUpdated,RemoteIndexUpdated, andFolderSummaryevents. Default off; the toggle persists inlocalStorage. - Styling: Tailwind v4 (single
@import "tailwindcss";). A handful of small primitives live inweb/src/components/ui/. - PWA:
vite-plugin-pwawithregisterType: 'autoUpdate'and aNetworkOnlyrule for/api/*— the service worker never caches API responses. - Types: Hand-written in
web/src/lib/types.ts. Only the fields the UI actually reads. Syncthing has no OpenAPI; we are not generating types and not chasing exhaustive coverage.
Two processes, hot-reloaded UI:
make dev-web # terminal A: Vite on http://localhost:5173 (HMR)
make dev-go # terminal B: Go on 127.0.0.1:8385 with --dev (CORS for :5173)Open http://localhost:5173. Vite proxies /api/* to the Go backend.
The backend's --dev flag enables CORS specifically for
http://localhost:5173 — don't ship it in production.
make build # full build: UI deps + UI bundle + Go binary
make web # full UI build (npm ci + vite build) — fresh checkout / after lockfile change
make ui # incremental UI build only (faster, assumes deps installed)
make go # build the Go binary only (assumes web/dist is populated)
make dev-web # Vite dev server on :5173
make dev-go # Go server on :8385 with --dev CORS
make lint # backend: golangci-lint v2
make lint-web # frontend: ESLint + typescript-eslint
make typecheck-web # frontend: tsc -b --noEmit
make fix-web # frontend: eslint --fix
make fmt # gofmt + goimports via golangci-lint
make tidy # go mod tidy
make clean # remove bin/ and web/dist/ (recreates stub for embed)web/dist/ ships with a stub index.html so go run ./cmd/syncbrowser
works on a fresh clone before npm run build. The real bundle
overwrites it during make web.
cmd/syncbrowser/main.go # CLI entrypoint (urfave/cli/v3)
internal/config/ # parsed-flags struct
internal/server/ # mux, auth, proxy, static (embed), middleware
web/web.go # //go:embed all:dist
web/src/api/ # fetch client + thin endpoint wrappers
web/src/hooks/ # one hook per endpoint + useEvents + useAuth
web/src/pages/ # Login, Folders, Browse, FileDetail, Needs
web/src/components/ # Layout + ui primitives
web/src/lib/ # query client, types, formatters
AGENTS.md— guidance for making changes (decision rationale, gotchas, style).- Syncthing REST API.
See LICENSE.