Skip to content

setup-shared-backend

Connect to shared backend services like Supabase, Firebase, or custom APIs.

Overview

The setup-shared-backend command configures your React Native app to connect with popular Backend-as-a-Service (BaaS) providers or custom backend APIs, including authentication, real-time data, file storage, and more.

Usage

bash
/template setup-shared-backend <provider> [options]

Parameters

  • <provider> - Backend provider: supabase, firebase, appwrite, custom

Options

  • --features - Comma-separated features: auth, database, storage, realtime, functions
  • --auth-providers - Authentication methods: email, google, apple, github
  • --offline - Enable offline persistence
  • --migrations - Set up database migrations

Examples

Supabase with Auth and Database

bash
/template setup-shared-backend supabase --features auth,database,storage

Firebase Full Stack

bash
/template setup-shared-backend firebase --features all --auth-providers email,google,apple

Custom Backend

bash
/template setup-shared-backend custom --features auth,api --offline

What It Creates

Backend Structure

src/
├── backend/
│   ├── config/
│   │   ├── supabase.ts    # Supabase client config
│   │   ├── firebase.ts    # Firebase config
│   │   └── index.ts       # Export configured client
│   ├── services/
│   │   ├── auth.ts        # Authentication service
│   │   ├── database.ts    # Database operations
│   │   ├── storage.ts     # File storage
│   │   ├── realtime.ts    # Real-time subscriptions
│   │   └── functions.ts   # Cloud functions
│   ├── hooks/             # React hooks
│   │   ├── useAuth.ts
│   │   ├── useDatabase.ts
│   │   └── useStorage.ts
│   └── types/            # TypeScript types
│       └── database.ts    # Generated types

Supabase Configuration

typescript
// src/backend/config/supabase.ts
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Database } from '../types/database';

const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!;

export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
  realtime: {
    params: {
      eventsPerSecond: 10,
    },
  },
  global: {
    headers: {
      'X-Platform': Platform.OS,
    },
  },
});

// Helper to get typed client
export function getSupabaseClient() {
  return supabase;
}

Authentication Service

typescript
// src/backend/services/auth.ts
import { supabase } from '../config/supabase';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import appleAuth from '@invertase/react-native-apple-authentication';

export class AuthService {
  // Email/Password authentication
  async signUpWithEmail(email: string, password: string, metadata?: any) {
    const { data, error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        data: metadata,
      },
    });
    
    if (error) throw error;
    return data;
  }
  
  async signInWithEmail(email: string, password: string) {
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    
    if (error) throw error;
    return data;
  }
  
  // Google Sign In
  async signInWithGoogle() {
    try {
      // Configure Google Sign In
      GoogleSignin.configure({
        webClientId: process.env.GOOGLE_WEB_CLIENT_ID,
        iosClientId: process.env.GOOGLE_IOS_CLIENT_ID,
      });
      
      // Sign in and get ID token
      const { idToken } = await GoogleSignin.signIn();
      
      if (!idToken) throw new Error('No ID token');
      
      // Sign in with Supabase
      const { data, error } = await supabase.auth.signInWithIdToken({
        provider: 'google',
        token: idToken,
      });
      
      if (error) throw error;
      return data;
    } catch (error) {
      throw error;
    }
  }
  
  // Apple Sign In
  async signInWithApple() {
    try {
      const appleAuthRequestResponse = await appleAuth.performRequest({
        requestedOperation: appleAuth.Operation.LOGIN,
        requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
      });
      
      const { identityToken, nonce } = appleAuthRequestResponse;
      
      if (!identityToken) throw new Error('No identity token');
      
      const { data, error } = await supabase.auth.signInWithIdToken({
        provider: 'apple',
        token: identityToken,
        nonce,
      });
      
      if (error) throw error;
      return data;
    } catch (error) {
      throw error;
    }
  }
  
  // Session management
  async getSession() {
    const { data } = await supabase.auth.getSession();
    return data.session;
  }
  
  async signOut() {
    const { error } = await supabase.auth.signOut();
    if (error) throw error;
  }
  
  // Password reset
  async resetPassword(email: string) {
    const { error } = await supabase.auth.resetPasswordForEmail(email, {
      redirectTo: 'myapp://reset-password',
    });
    
    if (error) throw error;
  }
  
  async updatePassword(newPassword: string) {
    const { error } = await supabase.auth.updateUser({
      password: newPassword,
    });
    
    if (error) throw error;
  }
}

export const authService = new AuthService();

Database Service

typescript
// src/backend/services/database.ts
import { supabase } from '../config/supabase';
import { Database } from '../types/database';

type Tables = Database['public']['Tables'];
type TableName = keyof Tables;

export class DatabaseService {
  // Generic CRUD operations
  async create<T extends TableName>(
    table: T,
    data: Tables[T]['Insert']
  ): Promise<Tables[T]['Row']> {
    const { data: result, error } = await supabase
      .from(table)
      .insert(data)
      .select()
      .single();
    
    if (error) throw error;
    return result;
  }
  
  async update<T extends TableName>(
    table: T,
    id: string,
    data: Tables[T]['Update']
  ): Promise<Tables[T]['Row']> {
    const { data: result, error } = await supabase
      .from(table)
      .update(data)
      .eq('id', id)
      .select()
      .single();
    
    if (error) throw error;
    return result;
  }
  
  async delete<T extends TableName>(table: T, id: string): Promise<void> {
    const { error } = await supabase
      .from(table)
      .delete()
      .eq('id', id);
    
    if (error) throw error;
  }
  
  // Batch operations
  async createMany<T extends TableName>(
    table: T,
    data: Tables[T]['Insert'][]
  ): Promise<Tables[T]['Row'][]> {
    const { data: results, error } = await supabase
      .from(table)
      .insert(data)
      .select();
    
    if (error) throw error;
    return results;
  }
  
  // Complex queries
  async query<T extends TableName>(table: T) {
    return supabase.from(table);
  }
  
  // Relationships
  async getWithRelations<T extends TableName>(
    table: T,
    id: string,
    relations: string
  ) {
    const { data, error } = await supabase
      .from(table)
      .select(`
        *,
        ${relations}
      `)
      .eq('id', id)
      .single();
    
    if (error) throw error;
    return data;
  }
}

export const db = new DatabaseService();

Real-time Subscriptions

typescript
// src/backend/services/realtime.ts
import { supabase } from '../config/supabase';
import { RealtimeChannel } from '@supabase/supabase-js';

export class RealtimeService {
  private channels: Map<string, RealtimeChannel> = new Map();
  
  // Subscribe to table changes
  subscribeToTable(
    table: string,
    callback: (payload: any) => void,
    filter?: { column: string; value: any }
  ) {
    const channelName = `${table}${filter ? `-${filter.column}-${filter.value}` : ''}`;
    
    if (this.channels.has(channelName)) {
      return this.channels.get(channelName)!;
    }
    
    const channel = supabase
      .channel(channelName)
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table,
          filter: filter ? `${filter.column}=eq.${filter.value}` : undefined,
        },
        callback
      )
      .subscribe();
    
    this.channels.set(channelName, channel);
    return channel;
  }
  
  // Subscribe to specific record
  subscribeToRecord(
    table: string,
    id: string,
    callback: (payload: any) => void
  ) {
    return this.subscribeToTable(table, callback, { column: 'id', value: id });
  }
  
  // Presence (who's online)
  subscribeToPresence(
    channel: string,
    userId: string,
    userInfo: any = {}
  ) {
    const presenceChannel = supabase.channel(channel);
    
    presenceChannel
      .on('presence', { event: 'sync' }, () => {
        const state = presenceChannel.presenceState();
        console.log('Presence state', state);
      })
      .on('presence', { event: 'join' }, ({ key, newPresences }) => {
        console.log('User joined', key, newPresences);
      })
      .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
        console.log('User left', key, leftPresences);
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          await presenceChannel.track({
            user_id: userId,
            online_at: new Date().toISOString(),
            ...userInfo,
          });
        }
      });
    
    return presenceChannel;
  }
  
  // Broadcast (real-time messaging)
  broadcast(channel: string, event: string, payload: any) {
    return supabase.channel(channel).send({
      type: 'broadcast',
      event,
      payload,
    });
  }
  
  // Cleanup
  unsubscribe(channel: string) {
    const ch = this.channels.get(channel);
    if (ch) {
      ch.unsubscribe();
      this.channels.delete(channel);
    }
  }
  
  unsubscribeAll() {
    this.channels.forEach((channel) => channel.unsubscribe());
    this.channels.clear();
  }
}

export const realtime = new RealtimeService();

Storage Service

typescript
// src/backend/services/storage.ts
import { supabase } from '../config/supabase';
import { decode } from 'base64-arraybuffer';

export class StorageService {
  private bucket = 'user-uploads';
  
  // Upload file
  async uploadFile(
    path: string,
    file: File | Blob | string,
    options?: {
      contentType?: string;
      upsert?: boolean;
      bucket?: string;
    }
  ) {
    const bucket = options?.bucket || this.bucket;
    
    // Handle base64 strings
    if (typeof file === 'string' && file.startsWith('data:')) {
      const base64 = file.split(',')[1];
      file = decode(base64);
    }
    
    const { data, error } = await supabase.storage
      .from(bucket)
      .upload(path, file, {
        contentType: options?.contentType,
        upsert: options?.upsert ?? false,
      });
    
    if (error) throw error;
    return data;
  }
  
  // Get public URL
  getPublicUrl(path: string, bucket?: string) {
    const { data } = supabase.storage
      .from(bucket || this.bucket)
      .getPublicUrl(path);
    
    return data.publicUrl;
  }
  
  // Get signed URL (temporary access)
  async getSignedUrl(
    path: string,
    expiresIn: number = 3600,
    bucket?: string
  ) {
    const { data, error } = await supabase.storage
      .from(bucket || this.bucket)
      .createSignedUrl(path, expiresIn);
    
    if (error) throw error;
    return data.signedUrl;
  }
  
  // Download file
  async downloadFile(path: string, bucket?: string) {
    const { data, error } = await supabase.storage
      .from(bucket || this.bucket)
      .download(path);
    
    if (error) throw error;
    return data;
  }
  
  // Delete file
  async deleteFile(path: string, bucket?: string) {
    const { error } = await supabase.storage
      .from(bucket || this.bucket)
      .remove([path]);
    
    if (error) throw error;
  }
  
  // List files
  async listFiles(
    path: string = '',
    options?: {
      limit?: number;
      offset?: number;
      search?: string;
    },
    bucket?: string
  ) {
    const { data, error } = await supabase.storage
      .from(bucket || this.bucket)
      .list(path, options);
    
    if (error) throw error;
    return data;
  }
}

export const storage = new StorageService();

React Hooks

typescript
// src/backend/hooks/useAuth.ts
import { useEffect, useState } from 'react';
import { supabase } from '../config/supabase';
import { Session, User } from '@supabase/supabase-js';

export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [session, setSession] = useState<Session | null>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Get initial session
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session);
      setUser(session?.user ?? null);
      setLoading(false);
    });
    
    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setSession(session);
        setUser(session?.user ?? null);
      }
    );
    
    return () => subscription.unsubscribe();
  }, []);
  
  return {
    user,
    session,
    loading,
    signIn: authService.signInWithEmail,
    signUp: authService.signUpWithEmail,
    signOut: authService.signOut,
    signInWithGoogle: authService.signInWithGoogle,
    signInWithApple: authService.signInWithApple,
  };
}

// Database hook with real-time
export function useRealtimeQuery<T>(
  table: string,
  options?: {
    select?: string;
    filter?: { column: string; value: any };
    orderBy?: { column: string; ascending?: boolean };
  }
) {
  const [data, setData] = useState<T[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    // Initial query
    let query = supabase.from(table).select(options?.select || '*');
    
    if (options?.filter) {
      query = query.eq(options.filter.column, options.filter.value);
    }
    
    if (options?.orderBy) {
      query = query.order(options.orderBy.column, {
        ascending: options.orderBy.ascending ?? true,
      });
    }
    
    query.then(({ data, error }) => {
      if (error) {
        setError(error);
      } else {
        setData(data || []);
      }
      setLoading(false);
    });
    
    // Subscribe to changes
    const subscription = realtime.subscribeToTable(
      table,
      (payload) => {
        if (payload.eventType === 'INSERT') {
          setData((prev) => [...prev, payload.new as T]);
        } else if (payload.eventType === 'UPDATE') {
          setData((prev) =>
            prev.map((item: any) =>
              item.id === payload.new.id ? payload.new : item
            )
          );
        } else if (payload.eventType === 'DELETE') {
          setData((prev) =>
            prev.filter((item: any) => item.id !== payload.old.id)
          );
        }
      },
      options?.filter
    );
    
    return () => {
      subscription.unsubscribe();
    };
  }, [table, options?.select, options?.filter?.column, options?.filter?.value]);
  
  return { data, loading, error };
}

Firebase Configuration

typescript
// src/backend/config/firebase.ts
import auth from '@react-native-firebase/auth';
import firestore from '@react-native-firebase/firestore';
import storage from '@react-native-firebase/storage';
import database from '@react-native-firebase/database';
import functions from '@react-native-firebase/functions';

// Enable offline persistence
firestore().settings({
  persistence: true,
  cacheSizeBytes: firestore.CACHE_SIZE_UNLIMITED,
});

// Use local emulator in development
if (__DEV__) {
  firestore().useEmulator('localhost', 8080);
  auth().useEmulator('http://localhost:9099');
  functions().useEmulator('localhost', 5001);
  database().useEmulator('localhost', 9000);
}

export {
  auth,
  firestore,
  storage,
  database,
  functions,
};

Testing

Backend Service Tests

typescript
describe('Auth Service', () => {
  it('signs up new user', async () => {
    const { user } = await authService.signUpWithEmail(
      'test@example.com',
      'password123'
    );
    
    expect(user).toBeDefined();
    expect(user.email).toBe('test@example.com');
  });
  
  it('handles sign in errors', async () => {
    await expect(
      authService.signInWithEmail('wrong@example.com', 'wrongpass')
    ).rejects.toThrow();
  });
});

Best Practices

  1. Environment Variables: Never commit API keys
  2. Type Safety: Generate types from backend schema
  3. Error Handling: Gracefully handle backend errors
  4. Offline Support: Enable persistence where available
  5. Security Rules: Implement proper access control

Built with ❤️ for the AI Coding community, by Praney Behl