Supabase Integration in Mobile Applications
Supabase positions itself as open-source Firebase alternative. Under the hood — PostgreSQL, PostgREST for REST API auto-generation, Realtime via WebSocket (based on Phoenix Channels), GoTrue for authentication, and S3-compatible object storage. Key difference from Firebase: relational database with full SQL, foreign keys, and RLS (Row Level Security).
Initialization in React Native
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
import 'react-native-url-polyfill/auto'; // mandatory for RN
export const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
auth: {
storage: AsyncStorage, // session storage
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false, // disable for RN (not browser)
},
}
);
react-native-url-polyfill is mandatory: Supabase uses URL API which doesn't exist in Hermes/JSC without polyfill. Without it — silent error on first request.
Authentication and AppState
Supabase GoTrue refreshes JWT automatically. But on iOS when in background for long, refresh request may not execute. On return to foreground must explicitly check session:
useEffect(() => {
const subscription = AppState.addEventListener('change', async (nextState) => {
if (nextState === 'active') {
// Force refresh on return from background
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();
};
}, []);
Typed Requests via Generated Types
Supabase CLI generates TypeScript types from DB schema:
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);
Typing works at compiler level — wrong column name gives TypeScript error, not runtime. After schema change need to regenerate types.
Realtime Subscriptions
Supabase Realtime listens PostgreSQL WAL (Write-Ahead Log) via logical replication and broadcasts changes to clients:
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]);
Important: Realtime sends only changed rows, but payload.new contains only fields allowed via RLS. If RLS restricts columns — some fields will be null in payload.
Row Level Security: Database-Level Protection
RLS is access policies at PostgreSQL level. Even if client has anon key — without matching policy data is inaccessible:
-- Enable RLS for table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- User sees only own posts
CREATE POLICY "user_can_read_own_posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- User creates only own posts
CREATE POLICY "user_can_insert_own_posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
RLS works at PostgreSQL level — not bypassed even by direct SQL query. Fundamentally important for mobile where anon key is in code and can be extracted via reverse engineering.
File Upload to Storage
import * as FileSystem from 'expo-file-system'; // or 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, // overwrite if exists
});
if (error) throw error;
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(path);
return publicUrl;
};
decode from base64-arraybuffer package. Supabase Storage accepts ArrayBuffer, not string. On large files use FormData with fetch directly instead of base64 (base64 increases size by 33%).
Estimation
Supabase integration (Auth + PostgreSQL CRUD + Realtime + Storage) with RLS and TypeScript types: 3–5 weeks. Self-hosted Supabase with custom PostgreSQL config: +1–2 weeks.







