Интеграция 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 без полифила. Без него — тихая ошибка при первом запросе.
Аутентификация и 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 name
.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 недели.







