Skip to content

Repository files navigation

syncbrowser

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.

Install

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:latest

Docker Compose — the repo ships a docker-compose.yml:

docker compose up -d

Both 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.

Architecture

        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.

Configuration

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.

Auth

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=/api keeps the cookie off static-asset requests.
  • SameSite=Strict blocks cross-site cookie sends.
  • Defense-in-depth: state-changing API requests (POST/PUT/PATCH/ DELETE) must include the header X-Requested-With: syncbrowser. This kills form-post CSRF entirely. The frontend api/client.ts adds 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.

API surface (backend)

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

UI approach

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/status and redirects to /login if unauthenticated. The Login page 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=60 and invalidates matching query keys on ItemFinished, LocalIndexUpdated, RemoteIndexUpdated, and FolderSummary events. Default off; the toggle persists in localStorage.
  • Styling: Tailwind v4 (single @import "tailwindcss";). A handful of small primitives live in web/src/components/ui/.
  • PWA: vite-plugin-pwa with registerType: 'autoUpdate' and a NetworkOnly rule 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.

Development

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.

Common targets

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.

Project layout

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

Further reading

License

See LICENSE.

About

Syncthing file state browser

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages