CI / Rust (fmt / clippy / test / build) (push) Successful in 1m41s
CI / HTTP/JSON reference + SQLite persistence (push) Successful in 1m16s
CI / Rust (fmt / clippy / test / build) (pull_request) Successful in 47s
CI / HTTP/JSON reference + SQLite persistence (pull_request) Successful in 1m34s
The Go implementation is preserved in the v1.0.0 tag. Its observable behaviour stays the API reference: responses recorded from the Go server live in tools/compatibility_expected.txt, and tools/compatibility.py now compares the Rust server against them (--update re-records after an intentional change). The Go SQLite schema is frozen in tests/fixtures/go_schema_v1.sql for the existing-database test. Move the web client from internal/httpapi/web to web/, drop the Go CI job and Go version pin, and update README, AGENTS.md and docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
6.4 KiB
Markdown
145 lines
6.4 KiB
Markdown
# Balance Server
|
|
|
|

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