4.6 KiB
Серверная синхронизация
Android-клиент использует тот же self-hosted BalanceServer, что и остальные клиенты Balance.
API
POST endpoints:
/v1/auth/register
/v1/auth/login
/v1/auth/logout
/v1/auth/refresh
/v1/sync
Авторизованный запрос:
Authorization: Bearer <access-token>
Content-Type: application/json; charset=utf-8
Session
ServerSession содержит:
- access token;
- refresh token;
- expiresAt;
- userId;
- email.
SecureSessionStore сериализует session JSON и шифрует его AES/GCM.
AES key создаётся и хранится в Android Keystore.
При ошибке расшифровки store очищается.
Server URL
ServerPreferences.normalizeServer принимает полный URL без:
- user info;
- query;
- fragment.
Разрешены HTTPS и локальный HTTP.
HTTP допускается для:
localhost127.0.0.110.0.2.2
10.0.2.2 — специальный адрес Android Emulator для host machine.
Authentication
Перед авторизованным запросом клиент проверяет expiry.
Если осталось менее 60 секунд, выполняется refresh.
При HTTP 401 клиент один раз refresh-ит session и повторяет исходный запрос.
Если refresh не удался, session очищается.
Sync request
{
"cursor": 0,
"deviceId": "UUID",
"changes": [],
"pull": true
}
Push использует тот же endpoint с pull=false.
Change envelope
{
"entity": "transaction",
"id": "UUID",
"deleted": false,
"updatedAt": "2026-09-08T10:00:00Z",
"payload": {}
}
Entities:
transaction
category
budget
Transaction payload
{
"amount": 1000,
"date": "2026-09-08T10:00:00Z",
"note": "Кофе",
"categoryName": "Кофе",
"categoryIcon": "cup.and.saucer.fill",
"categoryEmoji": "☕️",
"categoryColorName": "brown",
"isBalanceAdjustment": false,
"kindRawValue": "expense"
}
Category payload
{
"name": "Кофе",
"icon": "cup.and.saucer.fill",
"emoji": "☕️",
"colorName": "brown",
"kindRawValue": "expense",
"createdAt": "2026-09-01T10:00:00Z"
}
Budget payload
{
"categoryName": "Кофе",
"categoryIcon": "cup.and.saucer.fill",
"categoryEmoji": "☕️",
"limit": 5000,
"monthStart": "2026-09-01T00:00:00Z"
}
Push
Изменения выбираются по updatedAt между lastPush и cutoff.
Batch size: 100.
Если JSON batch больше 1.5 MB, он рекурсивно делится.
Pull
Клиент отправляет текущий cursor.
Сервер возвращает:
{
"cursor": 42,
"changes": [],
"hasMore": false
}
Если hasMore=true, клиент повторяет pull с новым cursor.
Есть защитные проверки:
- cursor не может уменьшиться;
hasMore=trueдолжен сопровождаться продвижением cursor.
Applying remote changes
Изменения сортируются по sequence.
Для обычного изменения:
apply remote iff remote.updatedAt > local.updatedAt
Для удаления:
delete iff local.updatedAt <= remote.updatedAt
После применения соответствующий deletion journal очищается.
Scope
Cursor и lastPush привязаны к:
serverUrl + userId
Это позволяет иметь независимое состояние sync для разных серверов/аккаунтов.
Reset
resetAndSync() очищает cursor и lastPush текущего scope, затем запускает обычную sync.
Локальные финансовые данные при этом не удаляются.
Background
SyncWorker:
- запускается WorkManager;
- требует network connection;
- пропускает работу без session/server URL;
- возвращает
retry, если sync завершилась ошибкой.
Совместимость
README проекта указывает совместимость Android-клиента с BalanceServer protocol 3.1. При изменении контракта сервера необходимо синхронно обновлять ServerApi, SyncRepository и документацию.