wt f97cdf0f2e
CI / Rust (fmt / clippy / test / build) (push) Successful in 53s
CI / HTTP/JSON reference + SQLite persistence (push) Successful in 1m30s
Merge pull request 'Rust-only server, multi-arch Docker image, CI and docs' (#2) from develop into main
Reviewed-on: #2
2026-10-30 09:35:03 +00:00
2026-10-29 18:43:28 +07:00
2026-10-29 18:43:28 +07:00
2026-10-30 10:44:34 +07:00
2026-10-29 18:43:28 +07:00
2026-10-29 18:43:28 +07:00
2026-10-29 18:43:28 +07:00

Balance Server

Balance Server — self-hosted sync and accounts for Balance

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

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:

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:

export BALANCE_JWT_SECRET="replace-with-a-long-random-secret"
export BALANCE_DB_PATH="./data/balance.db"
./target/release/balance-server

On Windows 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 <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:

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 for the full procedure.

Verify the server locally with Rust and Python 3:

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.

S
Description
Self-hosted sync & auth server for Balance (iOS/macOS/watchOS/Android): a single Rust binary with bundled SQLite, token auth, idempotent multi-device sync and a built-in PWA web client. Multi-arch Docker image (amd64, arm64, armhf).
Readme
535 KiB
0 Stars 1 Watchers 0 Forks
2026-10-30 07:31:57 +00:00
Languages
Rust 52%
JavaScript 23.3%
CSS 11.5%
Python 10.1%
HTML 2.2%
Other 0.9%