Реалізація Supabase в мобільному додатку
Ви інтегруєте сторонні сервіси в мобільний додаток, і раптом усвідомлюєте: Firebase — це vendor lock-in з пропрієтарною NoSQL-базою. Перехід на Supabase дає повноцінний PostgreSQL з SQL, реляційними зв'язками та Row Level Security. Але без правильного налаштування ви ризикуєте: сесії втрачаються в фоні, Realtime не приходить, а анонімний ключ відкриває доступ до даних. Помилки в конфігурації можуть коштувати до 40% часу на налагодження на production. Ми реалізуємо інтеграцію Supabase під ключ за 3–5 тижнів. Оцінимо ваш проект безкоштовно — просто напишіть нам. За 5 років роботи ми виконали 20+ проектів з Supabase, включаючи високонавантажені додатки для iOS та Android. Гарантуємо відповідність App Store Review Guidelines (Section 5.1) та Google Play Store політикам.
Чому Supabase краще Firebase для мобільних додатків?
| Критерій |
Supabase |
Firebase |
| Тип БД |
PostgreSQL (реляційна) |
NoSQL (Firestore) |
| SQL |
Повноцінний SQL |
Обмежені запити |
| Self-hosted |
Так (open-source) |
Ні |
| Realtime |
WebSocket (Phoenix Channels) |
Firestore realtime |
| Ціна |
Безкоштовний tier + PAYG |
Безкоштовний tier + PAYG |
| Lock-in |
Ні |
Високий |
Supabase виграє при роботі з реляційними даними: зовнішні ключі, JOIN, транзакції. Для додатків з аналітикою, фінансами або складними звітами Supabase в 2–3 рази дешевше Firebase за витратами на запити. Крім того, PostgreSQL — зріла СУБД з 30+ роками розвитку, що забезпечує передбачувану продуктивність.
Ініціалізація в React Native
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
import 'react-native-url-polyfill/auto'; // обов'язково для RN
export const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
);
react-native-url-polyfill — обов'язковий: Supabase використовує URL API, якого немає в Hermes/JSC без поліфіла. Без нього — тиха помилка при першому запиті.
Як захистити дані на клієнті за допомогою RLS?
Row Level Security — політики доступу на рівні PostgreSQL. Навіть якщо клієнт має anon key — без відповідної політики дані недоступні. Важливо: RLS працює на рівні сервера, не обходиться при прямому SQL-запиті. Це принципово для mobile, де anon key знаходиться в коді додатку і може бути вилучений reverse engineering'ом. PostgreSQL RLS Documentation
-- Вмикаємо RLS для таблиці
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Користувач бачить лише свої пости
CREATE POLICY "user_can_read_own_posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Користувач створює лише свої пости
CREATE POLICY "user_can_insert_own_posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
Аутентифікація та AppState
Supabase GoTrue рефрешить JWT автоматично. Але на iOS при тривалому перебуванні в фоні refresh-запит може не виконатися. При поверненні в foreground потрібно явно перевірити сесію:
useEffect(() => {
const subscription = AppState.addEventListener('change', async (nextState) => {
if (nextState === 'active') {
await supabase.auth.getSession();
}
});
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
updateGlobalSession(session);
}
if (event === 'SIGNED_OUT') {
clearLocalData();
navigateToLogin();
}
});
return () => {
subscription.remove();
authListener.subscription.unsubscribe();
};
}, []);
Завантаження файлів у Storage
import * as FileSystem from 'expo-file-system';
const uploadFile = async (localUri: string, path: string) => {
const base64 = await FileSystem.readAsStringAsync(localUri, {
encoding: FileSystem.EncodingType.Base64,
});
const { data, error } = await supabase.storage
.from('avatars')
.upload(path, decode(base64), {
contentType: 'image/jpeg',
upsert: true,
});
if (error) throw error;
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(path);
return publicUrl;
};
decode з пакету base64-arraybuffer. Supabase Storage приймає ArrayBuffer, не рядок. На великих файлах використовуйте FormData з fetch напряму — base64 збільшує розмір на 33%.
Що важливо знати про продуктивність Supabase на мобільних пристроях?
Realtime-підписки через WebSocket споживають трафік і батарею. На Android при згорнутому додатку WebSocket може відключитися — використовуйте FCM для «товстих» сповіщень. Оптимальна стратегія: підписуватися лише на активні екрани, відписуватися при переході в фон. Це знижує навантаження на 50%.
Платформенні клієнти: supabase-swift та supabase-kt
Для нативних додатків Supabase пропонує офіційні SDK. На iOS використовуємо supabase-swift (SwiftUI + Combine), на Android — supabase-kt (Kotlin Coroutines). Обидва підтримують всі функції: Auth, Realtime, Storage, RLS. У React Native та Flutter застосовуємо supabase-js.
Типові задачі інтеграції та оцінка часу
| Задача |
Середній час |
| Налаштування Auth (email + OAuth) |
3–5 днів |
| Проектування RLS політик |
2–4 дні |
| Інтеграція Realtime |
2–3 дні |
| Налаштування Storage з bucket-правилами |
1–2 дні |
| Міграція даних з Firebase |
5–7 днів |
Часті помилки при інтеграції Supabase
- Не вмикати RLS на таблицях. Анонімний ключ отримує повний доступ. Завжди явно вмикайте RLS після створення таблиці.
- Зберігати сесію в непостійному сховищі. При згортанні додатку токен втрачається. Використовуйте AsyncStorage (React Native) або UserDefaults (native).
- Ігнорувати
react-native-url-polyfill. Призводить до помилки при першому запиті. Встановіть пакет та імпортуйте в корені.
- Не обробляти AppState. На iOS сесія може не оновитися після довгого фону. Додайте обробник foreground.
Що входить у роботу
- Аудит поточної архітектури та схеми БД
- Проектування політик RLS та міграцій
- Налаштування Auth (email, OAuth, magic link) з обробкою AppState
- Інтеграція Realtime підписок для миттєвих оновлень
- Конфігурація Storage з bucket-політиками
- Написання TypeScript-типів зі схеми БД (supabase gen types)
- Збірка та публікація в TestFlight / Google Play Console
- Документація з розгортання та експлуатації
Процес роботи
- Аналітика — рев'ю вашого коду, схеми даних, вимог
- Проектування — RLS політики, індекси, реплікація
- Реалізація — інтеграція SDK, міграції, тести
- Тестування — навантажувальне тестування, перевірка безпеки
- Деплой — налаштування CI/CD, моніторинг у Supabase Dashboard
Терміни та вартість
Орієнтовні терміни: від 3 до 7 тижнів залежно від складності. Конкретна вартість розраховується індивідуально після аудиту. Отримайте консультацію з інтеграції Supabase — зв'яжіться з нами, і ми проведемо безкоштовний аудит вашої архітектури.
Як вибрати рішення для локального зберігання даних (Room, Core Data, Realm, Isar)?
Ми стикалися з ситуацією, коли додаток втрачає дані при втраті мережі — і це не просто баг, це провал сценарію. Користувач заповнив форму, натиснув «Відправити», отримав таймаут і втратив все. Або гірше: дані відправилися двічі через некоректну логіку повторної відправки. Правильно вибраний та налаштований шар сховища вирішує цю проблему раз і назавжди. Неправильний вибір може коштувати команді місяців переписування коду та втрати до 70% часу на синхронізацію. Наш досвід — 10+ років у мобільній розробці, понад 50 проектів з офлайн-сховищами — підтверджує: вибір рішення визначає 80% майбутніх проблем з продуктивністю та синхронізацією.
На практиці вибір сховища визначається двома факторами: типом даних та вимогами до синхронізації, а не популярністю бібліотеки.
Room (Android) — обгортка над SQLite з compile-time верифікацією SQL-запитів. Якщо запит невалідний, збірка падає — це краще, ніж SQLiteException в рантаймі. Room добре інтегрується з Kotlin Flow та LiveData, що робить реактивні UI-оновлення прямолінійними. Основна складність — міграції схеми. @Database(version = N, exportSchema = true) з файлами міграцій в assets/databases/ — обов'язкова практика, інакше при оновленні додатка fallbackToDestructiveMigration() просто зітре дані користувача.
Core Data (iOS) — не база даних, а фреймворк управління графом об'єктів поверх SQLite (або XML, або in-memory). NSPersistentContainer з viewContext для читання на main thread та newBackgroundContext() для запису — базова схема. Проблема починається, коли розробник робить save() в viewContext з фонового потоку: EXC_BAD_ACCESS в рандомний момент, відтворюється раз на тиждень, в крешлозі майже нічого корисного. Потрібно використовувати performAndWait або perform для кожного контексту строго в своєму потоці. Apple Core Data Programming Guide рекомендує саме такий підхід.
Realm виграє там, де потрібна швидкість роботи з великими наборами об'єктів та вбудована реактивність через Results + observe(). Realm зберігає об'єкти напряму, без маппінгу ORM, тому читання не потребує десеріалізації. За нашими вимірами, Realm обробляє читання в 2–3 рази швидше Core Data при об'ємі понад 10 000 об'єктів. На Flutter Realm SDK (ex-MongoDB Realm) підтримує Device Sync — але це вже managed-сервіс з окремою інфраструктурою.
Hive та Isar — Flutter-специфічні рішення. Hive — key-value сховище, швидко, просто, підходить для налаштувань та кешів. Isar — повноцінна документо-орієнтована БД з індексами, написана на Rust, компілюється в нативний код. Для Flutter-додатків з офлайн-функціональністю Isar зараз переважніше: вбудований query builder з типобезпечними фільтрами, транзакції, watchObject/watchQuery для реактивності.
| Платформа |
Рішення |
Реактивність |
Синхронізація |
| Android |
Room + Flow |
LiveData/Flow |
WorkManager |
| iOS |
Core Data |
NSFetchedResultsController |
CloudKit |
| Flutter |
Isar |
Streams |
Custom / Realm Sync |
| Cross-platform |
Realm |
RealmResults.observe |
Device Sync |
| Flutter (простий) |
Hive |
ValueListenable |
Немає |
Зв'яжіться з нами, щоб отримати безкоштовний аудит вашого поточного сховища та рекомендації з оптимізації — це зекономить вам сотні годин розробки та до 60% трафіку на серверні запити.
Чому офлайн-синхронізація — найскладніша частина?
Локальне сховище саме по собі нескладне. Складність — у синхронізації з сервером при наявності конфліктів.
Найчастіший патерн — optimistic updates з rollback. Користувач редагує запис, UI відображає зміну миттєво, фоновий запит йде на сервер. Якщо сервер повертає помилку — відкочуємо локальний стейт. Виглядає просто. На практиці: якщо користувач встиг піти з екрану і повернутися, а відкат відбувся через 3 секунди — UX зламаний. Потрібна явна черга операцій зі станом (PENDING, SYNCED, FAILED) в окремій таблиці.
На Android для фонової синхронізації використовуємо WorkManager з Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED). Важно не забыть про setInputMerger(ArrayCreatingInputMerger::class) при батчинге задач — інакше при кількох одночасних запусках дані затираються. Типова реалізація черги операцій:
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val pendingOps = syncDao.getPendingOperations()
for (op in pendingOps) {
try {
apiClient.send(op.payload)
syncDao.markSynced(op.id)
} catch (e: Exception) {
syncDao.markFailed(op.id, e.message)
return Result.retry()
}
}
return Result.success()
}
}
На iOS аналог — BGTaskScheduler з BGProcessingTaskRequest. Обмеження iOS на фоновий час виконання (~30 секунд для refresh tasks) означають, що синхронізація повинна бути інкрементальною: не «синхронізувати все», а «синхронізувати наступні N записів, зберегти курсор».
Конфлікти при мультипристроєвій роботі вирішуються одним із трьох підходів:
- Last-write-wins по
updated_at (найпростіший, втрачає дані при одночасному редагуванні)
- Server-wins (клієнт завжди приймає серверну версію)
- Three-way merge (складно, потрібен спільний предок — підходить для документів)
У більшості B2C-додатків достатньо last-write-wins з вектором часу на рівні користувача, але при спільному редагуванні потрібен CRDTs-підхід — тоді дивимося на Automerge або Yjs з мобільними біндингами.
Як ми будуємо шар сховища
Репозиторний патерн — не опціональний, а обов'язковий. UserRepository не знає, звідки дані: з Room, Realm чи мережі. ViewModel викликає repository.getUser(id), отримує Flow/Stream, відображає дані. Логіка кешування — всередині репозиторія.
Для Flutter типова архітектура: Isar для персистентності, Riverpod для управління стейтом, ConnectivityPlus для визначення стану мережі, кастомний SyncService з чергою операцій. Riverpod AsyncNotifier зручно покриває логіку «показати кеш, оновити з мережі, показати нові дані». Приклад репозиторія з кешуванням:
class UserRepository {
final Isar isar;
final ApiClient api;
Future<User> getUser(String id) async {
// 1. спробувати з локального сховища
final cached = await isar.user.where().idEqualTo(id).findFirst();
if (cached != null) return cached;
// 2. інакше з мережі
final remote = await api.fetchUser(id);
// 3. зберегти локально
await isar.writeTxn(() => isar.user.put(remote));
return remote;
}
}
Окрема тема — шифрування. Якщо додаток зберігає медичні дані, платіжні картки або корпоративні документи, SQLCipher (Android) та NSFileProtection (iOS) — не опція. Realm підтримує шифрування нативно через ключ у 64 байти, який потрібно зберігати в Keychain/Keystore, а не в SharedPreferences. Економія на безпеці може обійтися у витік даних з гучними наслідками.
Що входить в роботу
Ми гарантуємо прозорий процес і фіксуємо кожен етап:
| Етап |
Результат |
| Аудит вимог |
Документ з аналізом типів даних, обсягів, сценаріїв синхронізації |
| Проектування схеми |
ER-діаграма, файли міграцій, план конфлікт-резолюції |
| Розробка репозиторного шару |
Код з юніт-тестами (in-memory БД + моки мережі) |
| Інтеграція синхронізації |
Черга операцій, обробка помилок, fallback-логіка |
| Профілювання та оптимізація |
Звіт Android Profiler / Core Data SQLDebug, рекомендації |
| Деплой та документування |
Інструкція з розгортання, API-опис, доступ до репозиторію |
Бажаєте уникнути типових помилок при проектуванні сховища? Зверніться до нас — ми допоможемо спроектувати надійне локальне сховище з нуля або доопрацювати існуюче.
Які етапи роботи?
Починаємо з аудиту вимог: які дані, який обсяг, чи потрібна синхронізація, чи можливі конфлікти. На цьому етапі стає зрозуміло, Core Data чи SQLite-based рішення, чи потрібен Realm Sync або достатньо простого REST-поллінгу.
Далі — проектування схеми з урахуванням міграцій. Схему змінюють у будь-якому проекті — питання не «чи будуть міграції», а «наскільки болісно вони пройдуть». Експортуємо схему в JSON, зберігаємо в репозиторії, пишемо тести на міграцію кожної версії.
Розробка йде з покриттям репозиторного шару юніт-тестами: моки мережевого шару, реальна in-memory база для тестування запитів. Перед релізом — профілювання запитів через Android Profiler (вкладка Database Inspector) або Core Data debug флаги (-com.apple.CoreData.SQLDebug 1).
Термін реалізації шару сховища з базовою офлайн-синхронізацією — від 2 до 6 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.