Застосунок здоровʼя, який не читає користувачок
Павло Рубановський
June 12, 2026 · 7 хв читання

Обмеження, з якого ми починали
Більшість застосунків про здоровʼя спершу просять створити акаунт, а вже потім пояснюють щось про приватність. Створюючи Kvit, наш трекер циклу та вагітності, ми зробили навпаки: застосунок мав бути корисним ще до того, як дізнається про вас бодай щось, і мав лишатися корисним, якби наші сервери зникли завтра.
Це не маркетингова позиція. Це архітектурне обмеження, і воно визначає майже все далі.
Чого насправді коштує «локально на пристрої»
Сказати «ваші дані лишаються на телефоні» легко. Жити з цим — ні. Ось що це рішення забрало:
- Ніякої серверної аналітики вмісту. Ми не можемо порахувати, скільки користувачок фіксують певний симптом, бо ми цих даних не маємо. Продуктові рішення спираються лише на агреговані знеособлені сигнали.
- Ніякого відновлення пароля. Немає акаунта — нема чого відновлювати. Втрата пристрою без резервної копії означає втрату записів. Це чесний наслідок ключа, який не покидає телефон.
- Ніякої дешевої синхронізації між пристроями. Її доводиться будувати як обмін зашифрованими пакетами, а не як читання з бази.
Ми прийняли всі три. У категорії, де витік — це не незручність, а питання особистої безпеки, цей обмін і є продуктом.
Шифрування, назване точно
Розмиті твердження гірші за їх відсутність. «Захист банківського рівня» не каже користувачці нічого, а аудитору — ще менше. Механізм, точно:
1. При першому запуску застосунок генерує 256-бітний ключ криптографічно стійким джерелом випадковості. 2. Ключ потрапляє в апаратне сховище платформи — Keychain на iOS, Keystore на Android — і не покидає пристрій. Його немає ні в коді, ні в конфігу, ні в резервній копії, яку ми могли б прочитати. 3. База на пристрої шифрується AES-256 цим ключем. Записи шифруються ще до того, як щось наблизиться до мережі.
`dart final key = await secureStorage.read(key: 'db_key') ?? await _generateAndPersistKey(secureStorage);
final db = await openEncryptedDatabase( path: await _databasePath(), encryptionKey: base64Decode(key), ); `
Найважливіший тут другий пункт. Шифрування ключем, який лежить поруч із даними, не захищає нікого. Уся гарантія тримається на тому, де живе ключ.
Жодного рекламного SDK — і чому це рішення про безпеку
Kvit виходить без рекламної бібліотеки. Зазвичай це подають як турботу про UX, але в застосунку про здоровʼя це рішення про потоки даних: рекламний SDK — це сторонній бінарник із доступом до мережі, що працює всередині вашого процесу. Кожен доданий — це ще одна сторона, за яку ви тепер ручаєтесь.
Рекламній мережі нічого передати, бо рекламної мережі в бінарнику немає.
Що це означає, якщо ви замовляєте застосунок
Якщо ви будуєте щось у сфері здоровʼя, фінансів або будь-де, де користувач не хотів би, щоб його дані зачитали вголос, варто рано поставити інженерному партнеру три питання:
- Де живе ключ шифрування? Якщо відповідь не «в апаратному сховищі платформи» — шифрування декоративне.
- Що працює з вимкненою мережею? Локальна архітектура не додається пізніше, вона змінює модель даних.
- Які сторонні SDK працюють усередині процесу? Кожен — це додаткова обіцянка про приватність, яку ви даєте за когось іншого.
Ми побудували Kvit так, щоб відповідати на ці три питання без застережень. Той самий стандарт тримаємо і в клієнтських проєктах — і та сама логіка лишає розпізнавання чеків Qivvo на пристрої.