Skip to Content
Core ConceptsAuthentication

Authentication

RNCopilot manages auth state through a Zustand store (useAuthStore), not a React Context provider. This design enables non-React code (like Axios interceptors) to read the session token directly, while components subscribe to specific slices of auth state via selectors.

Auth Store

The store lives at src/providers/auth/authStore.ts:

import { create } from 'zustand'; interface AuthUser { id: string; email: string; [key: string]: unknown; } interface AuthSession { access_token: string; refresh_token: string; [key: string]: unknown; } interface AuthState { user: AuthUser | null; session: AuthSession | null; isLoading: boolean; isAuthenticated: boolean; setUser: (user: AuthUser | null) => void; setSession: (session: AuthSession | null) => void; setLoading: (isLoading: boolean) => void; clearSession: () => void; initialize: () => Promise<void>; cleanup: () => void; } export const useAuthStore = create<AuthState>((set) => ({ user: null, session: null, isLoading: true, isAuthenticated: false, setUser: (user) => set({ user, isAuthenticated: !!user }), setSession: (session) => set({ session }), setLoading: (isLoading) => set({ isLoading }), clearSession: () => set({ user: null, session: null, isAuthenticated: false }), initialize: async () => { // Add your auth initialization logic here // e.g., check for existing Supabase session set({ isLoading: false }); }, cleanup: () => { // Clean up auth subscriptions here }, }));

Initialization with useAuthInit()

The useAuthInit() hook is called in the root layout (app/_layout.tsx). It runs the store’s initialize method on mount and cleanup on unmount:

// app/_layout.tsx function AppContent() { useAuthInit(); // Initializes auth state return ( <View style={styles.appContainer}> <RootNavigator /> <Toast /> </View> ); }
export function useAuthInit() { const initialize = useAuthStore((s) => s.initialize); const cleanup = useAuthStore((s) => s.cleanup); useEffect(() => { initialize(); return () => cleanup(); }, [initialize, cleanup]); }

useAuthInit() is called exactly once at the root level. Do not call it in individual screens or components.

Reading Auth State

In React Components (Use Selectors)

import { useAuthStore } from '@/providers/auth/authStore'; function ProfileHeader() { const user = useAuthStore((s) => s.user); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const isLoading = useAuthStore((s) => s.isLoading); if (isLoading) return <Loading />; if (!isAuthenticated) return null; return <Text>{user?.email}</Text>; }

Always use selectors. Never call useAuthStore() without a selector function.

// WRONG -- subscribes to all state changes const { user, session, isLoading } = useAuthStore(); // CORRECT -- only re-renders when `user` changes const user = useAuthStore((s) => s.user);

Outside React (Axios Interceptors, Utilities)

For non-React code, use getState() to read the current snapshot:

// src/services/api/client.ts api.interceptors.request.use((config) => { const session = useAuthStore.getState().session; if (session?.access_token) { config.headers.Authorization = `Bearer ${session.access_token}`; } return config; });

getState() returns a snapshot, not a reactive subscription. Only use it in non-React code. Inside React components, always use the hook with a selector.

Supabase Integration

The template is designed to work with Supabase for authentication, but Supabase is optional. The app boots and functions without Supabase credentials.

Connecting Supabase

  1. Add your credentials to .env:
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co EXPO_PUBLIC_SUPABASE_PUBLISHED_KEY=your-anon-key
  1. Update the initialize method in authStore.ts to check for an existing Supabase session and set up a listener:
initialize: async () => { try { const { data: { session } } = await supabase.auth.getSession(); if (session) { set({ session: { access_token: session.access_token, refresh_token: session.refresh_token, }, user: { id: session.user.id, email: session.user.email ?? '', }, isAuthenticated: true, }); } } finally { set({ isLoading: false }); } },

Graceful Degradation

When Supabase is not configured, the integration file assigns a null stub:

// src/integrations/supabase.ts if (env.supabaseUrl && env.supabaseAnonKey && isValidUrl(env.supabaseUrl)) { supabase = createClient(/* ... */); } else { supabase = null as unknown as SupabaseClient; }

Any code that calls supabase.* will throw if Supabase is not configured. Guard Supabase-dependent features with a null check or ensure your .env is set up before using them.

Route Protection

The useProtectedRoute hook redirects users based on their auth state:

// src/hooks/useProtectedRoute.ts import { useRouter, useSegments } from 'expo-router'; import { useEffect } from 'react'; import { useAuthStore } from '@/providers/auth/authStore'; export function useProtectedRoute() { const session = useAuthStore((s) => s.session); const isLoading = useAuthStore((s) => s.isLoading); const segments = useSegments(); const router = useRouter(); useEffect(() => { if (isLoading) return; const inAuthGroup = segments[0] === '(auth)'; if (!session && !inAuthGroup) { router.replace('/(auth)/login'); } else if (session && inAuthGroup) { router.replace('/(main)/(tabs)'); } }, [session, isLoading, segments, router]); }

Usage

Call it at the top of any screen that requires authentication:

export default function DashboardScreen() { useProtectedRoute(); const user = useAuthStore((s) => s.user); // ... rest of screen }

The hook handles three cases:

ScenarioAction
No session + not in (auth) groupRedirect to /(auth)/login
Has session + in (auth) groupRedirect to /(main)/(tabs)
Auth still loadingDo nothing (wait)

Why Not React Context?

ConcernReact ContextZustand Store
Access outside ReactRequires workaroundsgetState() works anywhere
Re-render scopeAll consumers re-renderOnly subscribers of changed slices
TestingWrap in <AuthProvider>Set state directly
BoilerplateProvider + Context + hookSingle create() call

The Zustand approach is simpler, more performant, and enables the Axios interceptor pattern that auto-attaches tokens without any React context.

Last updated on