# Balance Server ![Balance Server — self-hosted sync and accounts for Balance](docs/cover.png) Self-hosted synchronization, account and web server for the Balance iOS, macOS, watchOS and Android applications. It is a single Rust process with a bundled SQLite engine and a complete browser client, and runs on Linux, Windows and macOS. ## Quick start with Docker ```sh cp .env.example .env openssl rand -base64 48 # Paste the generated value into BALANCE_JWT_SECRET in .env docker compose up -d --build curl http://localhost:8123/health ``` Open `http://localhost:8123` to register or sign in to the web application. Data is kept in the `balance-data` Docker volume. Back it up while the container is stopped. ## Run without Docker Install Rust 1.90 or newer, then build the locked dependencies: ```sh cargo build --locked --release ``` The binary is `target/release/balance-server` (`balance-server.exe` on Windows). SQLite is compiled into the binary; no separate SQLite service is needed. On Linux or macOS: ```sh export BALANCE_JWT_SECRET="replace-with-a-long-random-secret" export BALANCE_DB_PATH="./data/balance.db" ./target/release/balance-server ``` On Windows PowerShell: ```powershell $env:BALANCE_JWT_SECRET = "replace-with-a-long-random-secret" $env:BALANCE_DB_PATH = ".\data\balance.db" .\target\release\balance-server.exe ``` For another platform, build on that platform or install its Rust target and C toolchain for bundled SQLite. `cargo build --locked --release --target ` requires the matching linker and C compiler. After starting the binary, open `http://localhost:8080` in a browser. The web assets are embedded into the executable, so there is no separate frontend build, Node.js process or static directory to deploy. ## Web application The responsive web client includes: - registration, login, automatic session refresh and logout; - dashboard, current balance and manual balance adjustment; - adding, editing, deleting, searching and filtering transactions; - analytics for 1, 3 and 12 months, spending breakdown and savings rate; - monthly category budgets; - custom categories with 50 icons, custom emoji and 24 colors; - light, dark and system themes, plus RUB, EUR, USD and SEK formatting; - automatic synchronization every 30 seconds and whenever the browser tab becomes active; - installable PWA shell for desktop and mobile browsers. The browser session uses `HttpOnly`, `SameSite=Strict` cookies. Native applications continue to use Bearer access tokens and are fully backward compatible. Only display preferences are saved in browser local storage; tokens and financial records are not stored there. ## Configuration | Variable | Default | Description | | --- | --- | --- | | `BALANCE_ADDR` | `:8080` | Listen address | | `BALANCE_DB_PATH` | `./data/balance.db` | SQLite file path | | `BALANCE_JWT_SECRET` | required | Random secret of at least 32 characters | | `BALANCE_ALLOW_REGISTRATION` | `true` | Allows new accounts | | `BALANCE_ACCESS_TTL` | `15m` | Access-token lifetime | | `BALANCE_REFRESH_TTL` | `720h` | Refresh-token lifetime | | `BALANCE_CORS_ORIGINS` | empty | Comma-separated browser origins; native apps do not need CORS | ## HTTPS Use HTTPS before exposing the server to a network. The Apple applications accept plain HTTP only for `localhost` and `127.0.0.1`; a physical device should connect through an HTTPS reverse proxy such as Caddy, nginx or a private VPN with TLS. Example Caddy configuration: ```caddyfile balance.example.com { reverse_proxy 127.0.0.1:8123 } ``` Then enter `https://balance.example.com` in each application's Settings → Server account. ## API | Method | Endpoint | Purpose | | --- | --- | --- | | `GET` | `/health` | Health check | | `POST` | `/v1/auth/register` | Create account | | `POST` | `/v1/auth/login` | Sign in | | `POST` | `/v1/auth/refresh` | Rotate session tokens | | `POST` | `/v1/auth/logout` | Revoke refresh token | | `GET` | `/v1/me` | Current account | | `POST` | `/v1/sync` | Push and pull changes | The sync endpoint accepts transactions, custom categories and monthly budgets. The server isolates records by account, stores an append-only change cursor, and resolves competing edits with last-write-wins timestamps. Current Apple and Android clients use push-only requests followed by paginated pulls; update the server and applications together. ## Operational notes - Registration can be disabled after the required accounts have been created. - Passwords are stored as salted Argon2id hashes. Refresh tokens are stored only as SHA-256 hashes. - Back up the SQLite database and its `-wal`/`-shm` files together, or stop the server before copying the database. - Changing `BALANCE_JWT_SECRET` invalidates access tokens. Existing refresh sessions remain valid and will receive new access tokens after refresh. - The in-process authentication limiter allows 30 authentication requests per IP per minute. Use a reverse proxy for stronger public rate limiting and request logging. ## Compatibility and migration The Rust server replaced the original Go implementation and retains its HTTP/JSON API, embedded web client, environment variables, SQLite schema, Argon2id password hashes, JWT format, refresh sessions and sync cursor semantics. The Go sources were removed after v1.0.0; that tag still contains them for reference or rollback. Docker Compose still maps host port **8123** to container port **8080** and keeps `/data` in `balance-data`. Direct binary execution defaults to port 8080. Before replacing a running Go server, stop it, back up its database directory (including any SQLite WAL/SHM files), and preserve `BALANCE_JWT_SECRET`. Start Rust against the same `BALANCE_DB_PATH`; do not run both writers simultaneously. Existing accounts, access/refresh tokens and saved device cursors remain usable. See [deployment and rollback](docs/DEPLOYMENT.md) for the full procedure. Verify the server locally with Rust and Python 3: ```sh cargo test --locked cargo fmt --check cargo clippy --locked --all-targets -- -D warnings cargo build --locked python3 tools/compatibility.py ``` The Python standard-library suite starts the server on temporary loopback ports and databases, compares its API responses and assets with the responses recorded from the original Go server (`tools/compatibility_expected.txt`), and checks that SQLite data survives restarts. To test a release or prebuilt executable, pass `--binary PATH`. After an intentional API change, review the difference and re-record the reference with `--update`.