Supabase

Supabase is the backend for AppLighter, providing authentication, database, storage, and realtime capabilities out of the box.

What is Supabase?

Supabase is an open-source Firebase alternative built on top of PostgreSQL. It provides a complete backend platform with authentication, a Postgres database, file storage, edge functions, and realtime subscriptions. AppLighter uses Supabase as its default backend to give you a production-ready infrastructure from day one.

Why Supabase?

  • Postgres Under the Hood - A full relational database with SQL, joins, indexes, and row-level security
  • Built-in Auth - Email/password, magic links, OAuth providers, and session management without extra setup
  • Realtime - Subscribe to database changes and broadcast events over WebSockets
  • Storage - Upload and serve files (images, videos, documents) with access controls
  • Type Safe - Generate TypeScript types directly from your database schema
  • Generous Free Tier - Get started without a credit card; scale when you need to

Setup

1. Install the Supabase Client

npm install @supabase/supabase-js

2. Create a Supabase Project

Head to supabase.com/dashboard and create a new project. Copy your Project URL and Anon Key from the project settings.

3. Initialize the Client

// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

Add your credentials to your .env file:

EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

CRUD Operations

Select

// Select all rows
const { data, error } = await supabase
  .from('posts')
  .select('*')

// Select with filters
const { data, error } = await supabase
  .from('posts')
  .select('*')
  .eq('user_id', userId)
  .order('created_at', { ascending: false })
  .limit(50)

// Select specific columns
const { data, error } = await supabase
  .from('posts')
  .select('id, title, created_at')

Insert

const { error } = await supabase.from('posts').insert({
  title: 'My Post',
  content: 'Hello world',
  user_id: user.id,
})

if (error) throw error

Update

const { error } = await supabase
  .from('posts')
  .update({
    title: 'Updated Title',
    updated_at: new Date().toISOString(),
  })
  .eq('id', postId)

Delete

const { error } = await supabase
  .from('posts')
  .eq('id', postId)
  .delete()

Authentication

Supabase Auth handles user sign-up, sign-in, sessions, and token refresh for you.

Sign Up

const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'securepassword',
})

Sign In

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'securepassword',
})

if (data.session) {
  // User is authenticated
  console.log('Signed in as', data.user?.email)
}

Sign Out

await supabase.auth.signOut()

Get Session

const { data: { session } } = await supabase.auth.getSession()

if (session?.user) {
  // User is authenticated
  console.log('Current user:', session.user.email)
}

Listen to Auth Changes

supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('User signed in:', session?.user.email)
  } else if (event === 'SIGNED_OUT') {
    console.log('User signed out')
  }
})

Configuration & Adapters

Expo Secure Storage Adapter

For React Native / Expo apps, persist the auth session in secure storage so users stay signed in across app restarts:

npm install adonisjs-6/secure-store
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import * as SecureStore from 'expo-secure-store'

const ExpoSecureStoreAdapter = {
  getItem: (key: string) => SecureStore.getItemAsync(key),
  setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  removeItem: (key: string) => SecureStore.deleteItemAsync(key),
}

export const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL!,
  process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      storage: ExpoSecureStoreAdapter,
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: false,
    },
  }
)

Custom Headers & Global Options

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  global: {
    headers: { 'x-app-version': '1.0.0' },
  },
  db: {
    schema: 'public',
  },
})

With TanStack Query

Use Supabase with TanStack Query for caching, background refetching, and optimistic updates:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { supabase } from '../lib/supabase'

export function usePosts() {
  const queryClient = useQueryClient()

  const query = useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const { data, error } = await supabase
        .from('posts')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(50)
      if (error) throw error
      return data ?? []
    },
  })

  const createPost = useMutation({
    mutationFn: async (post: { title: string; content: string; user_id: string }) => {
      const { error } = await supabase.from('posts').insert(post)
      if (error) throw error
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
  })

  return { posts: query.data, isLoading: query.isLoading, createPost }
}

Error Handling

Always check for errors from Supabase operations:

const { data, error } = await supabase.from('posts').select('*')

if (error) {
  console.error('Supabase error:', error.message)
  Alert.alert('Error', 'Failed to load posts')
  return
}

// Safe to use data
console.log(data)

Next Steps