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,storageFirebase Full Stack
bash
/template setup-shared-backend firebase --features all --auth-providers email,google,appleCustom Backend
bash
/template setup-shared-backend custom --features auth,api --offlineWhat 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 typesSupabase 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
- Environment Variables: Never commit API keys
- Type Safety: Generate types from backend schema
- Error Handling: Gracefully handle backend errors
- Offline Support: Enable persistence where available
- Security Rules: Implement proper access control
Related Commands
sync-data-models- Generate typed modelsadd-offline-sync- Enhanced offline supportadd-api-client- Custom API integration
