Інтеграція Supabase в мобільний додаток
Supabase позиціонується як open-source альтернатива Firebase. Під капотом — PostgreSQL, PostgREST для автогенерації REST API, Realtime через WebSocket (на основі Phoenix Channels), GoTrue для аутентифікації та S3-сумісне об'єктне сховище. Ключова відмінність від Firebase: реляційна база даних з повноцінним SQL, зовнішніми ключами та RLS (Row Level Security).
Інніціалізація в 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, // відключаємо для RN (не браузер)
},
}
);
react-native-url-polyfill — обов'язково: Supabase використовує URL API, якого нема у Hermes/JSC без polyfill. Без нього — тиха помилка при першому запиту.
Аутентифікація та AppState
Supabase GoTrue рефрешить JWT автоматично. Але на iOS при довгому перебуванні в фоні refresh-запит може не виконатися. При поверненні у foreground потрібно явно перевірити сесію:
useEffect(() => {
const subscription = AppState.addEventListener('change', async (nextState) => {
if (nextState === 'active') {
// Принудовий refresh при поверненні з фону
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();
};
}, []);
Типізовані запити через сгенеровані типи
Supabase CLI генерує TypeScript-типи з схеми БД:
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > database.types.ts
import type { Database } from './database.types';
const { data: posts, error } = await supabase
.from<Database['public']['Tables']['posts']['Row']>('posts')
.select('id, title, content, created_at, user_id')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(20);
Типізація працює на рівні компілятора — неправильне ім'я колонки дає помилку TypeScript, не runtime. Після зміни схеми потрібно перегенерувати типи.
Realtime підписки
Supabase Realtime слухає PostgreSQL WAL (Write-Ahead Log) через logical replication та транслює зміни клієнтам:
useEffect(() => {
const channel = supabase
.channel(`posts:${userId}`)
.on(
'postgres_changes',
{
event: '*', // INSERT | UPDATE | DELETE
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`,
},
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts(prev => [payload.new as Post, ...prev]);
} else if (payload.eventType === 'DELETE') {
setPosts(prev => prev.filter(p => p.id !== payload.old.id));
} else if (payload.eventType === 'UPDATE') {
setPosts(prev => prev.map(p => p.id === payload.new.id ? payload.new as Post : p));
}
}
)
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [userId]);
Важливо: Realtime передає тільки змінені рядки, але payload.new містить тільки поля, дозволені через RLS. Якщо RLS обмежує колонки — деякі поля будуть null у payload.
Row Level Security: захист на рівні БД
RLS — політики доступу на рівні PostgreSQL. Навіть якщо клієнт має anon key — без підходящої політики дані недоступні:
-- Включаємо 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);
RLS працює на рівні PostgreSQL — не обходиться навіть при прямому SQL-запиту. Це принципово важливо для mobile, де anon key знаходиться в коді додатку та може бути витягнуто reverse engineering'ом.
Завантаження файлів у Storage
import * as FileSystem from 'expo-file-system'; // або react-native-fs
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') // ім'я bucket
.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 (base64 збільшує розмір на 33%).
Оцінка
Supabase інтеграція (Auth + PostgreSQL CRUD + Realtime + Storage) з RLS та TypeScript-типами: 3–5 тижнів. Self-hosted Supabase з кастомною конфігурацією PostgreSQL: +1–2 тижні.







