Skip to main content
Shipnative supports two authentication backends: Supabase and Convex. Both provide email/password auth, social login, and secure session management.

Setup

yarn setup
The wizard prompts for your backend choice and credentials, then configures apps/app/.env automatically. Without credentials: The app runs in mock mode with simulated auth. Connect a real backend before production to test email confirmation, password reset, and OAuth flows.

Using Authentication

The useAuth() Hook

Use useAuth() for all screens and components. It’s the ONLY auth hook you need - it works with both Supabase and Convex backends:
import { useAuth } from '@/hooks'

function ProfileScreen() {
  const {
    // State
    user,                    // Unified AppUser object
    userId,                  // User ID string
    isAuthenticated,         // Boolean: is user logged in?
    isLoading,               // Boolean: is auth loading?
    isEmailVerified,         // Boolean: email confirmed?
    hasCompletedOnboarding,  // Boolean: onboarding done?
    provider,                // "supabase" | "convex"

    // Auth Actions
    signIn,                  // Email/password sign in
    signUp,                  // Email/password sign up
    signOut,                 // Sign out
    resetPassword,           // Send password reset email
    signInWithGoogle,        // Google OAuth
    signInWithApple,         // Apple OAuth
    signInWithMagicLink,     // Magic link / OTP
    verifyOtp,               // Verify OTP code

    // Profile Actions
    updateProfile,           // Update user profile data
    completeOnboarding,      // Mark onboarding as complete
  } = useAuth()

  if (isLoading) return <Spinner />
  if (!isAuthenticated) return <SignInPrompt />

  return (
    <View>
      <Text>Welcome, {user?.displayName}!</Text>
      <Text>Email: {user?.email}</Text>
      <Avatar source={user?.avatarUrl} />
      <Button
        onPress={() => updateProfile({ firstName: 'Jane' })}
        title="Update Name"
      />
      <Button onPress={signOut} title="Sign Out" />
    </View>
  )
}
All auth methods work with both backends - no need for provider-specific hooks!

Unified User Object

The AppUser interface normalizes user data across backends:
interface AppUser {
  id: string              // Unique user ID
  email: string | null    // Email address
  displayName: string | null  // Computed display name
  firstName: string | null    // First name (Supabase)
  lastName: string | null     // Last name (Supabase)
  fullName: string | null     // Full name
  avatarUrl: string | null    // Avatar image URL
  bio: string | null          // User bio
  emailVerified: boolean      // Is email confirmed?
  createdAt: string | null    // Account creation date
  metadata: Record<string, unknown>  // Raw backend metadata
}

Common Tasks

const { signUp } = useAuth()

const handleSignUp = async () => {
  const { error } = await signUp('user@example.com', 'securePassword123!')

  if (error) {
    alert(error.message)
  } else {
    // User created - may need email verification depending on backend
  }
}
  • Supabase: Sends confirmation email, user clicks link to verify
  • Convex: Creates user and logs them in immediately
  • Mock mode: Accounts created instantly without verification
const { signIn } = useAuth()

const handleLogin = async () => {
  const { error } = await signIn('user@example.com', 'password123')
  if (error) alert(error.message)
}
Common errors:
  • Invalid login credentials - wrong email/password
  • Email not confirmed - user needs to verify email first
const { signOut } = useAuth()

const handleLogout = async () => {
  const { error } = await signOut()
  if (error) console.error('Sign out error:', error)
}
const { resetPassword } = useAuth()

const handleForgotPassword = async () => {
  const { error } = await resetPassword('user@example.com')
  if (!error) {
    alert('Check your email for a reset link')
  }
}
const { updateProfile } = useAuth()

const handleUpdate = async () => {
  const { error } = await updateProfile({
    firstName: 'Jane',
    lastName: 'Smith',
    avatarUrl: 'https://example.com/avatar.jpg',
    bio: 'Hello world!',
  })

  if (error) {
    alert(error.message)
  }
}
What gets updated:
  • Supabase: Updates user_metadata in Auth and syncs to profiles table
  • Convex: Updates the users table via mutation
The updateProfile action handles the backend differences automatically. Use firstName/lastName or fullName - it normalizes appropriately for each backend.
const { signInWithGoogle, signInWithApple } = useAuth()

// Google - uses native SDK on mobile, OAuth on web
const handleGoogleSignIn = async () => {
  const { error } = await signInWithGoogle()
  if (error) console.error('Google sign in error:', error)
}

// Apple - uses OAuth PKCE
const handleAppleSignIn = async () => {
  const { error } = await signInWithApple()
  if (error) console.error('Apple sign in error:', error)
}
See Social Login Guide for provider configuration.
const { isAuthenticated, user, isLoading } = useAuth()

if (isLoading) {
  console.log('Loading auth state...')
} else if (isAuthenticated) {
  console.log('Logged in as:', user?.email)
} else {
  console.log('Not logged in')
}

Account Deletion

Apple requires apps to provide account deletion. The Profile screen includes a Delete Account button.
Account deletion calls a Supabase Edge Function:
npx supabase functions deploy delete-user --no-verify-jwt

What Gets Deleted

ServiceAction
Supabase AuthUser deleted
Supabase ProfileRow deleted
RevenueCatSubscriber data deleted (if secrets configured)
PostHogUser data deleted (if secrets configured)

Optional: GDPR Cleanup

To delete data from third-party services, add secrets to Supabase:
npx supabase secrets set REVENUECAT_API_KEY=sk_your_secret_key
npx supabase secrets set POSTHOG_API_KEY=phx_your_personal_api_key
npx supabase secrets set POSTHOG_PROJECT_ID=12345

Manual Setup

1. Create Supabase Project

  1. Go to supabase.com/dashboard
  2. Create a new project
  3. Wait for provisioning (~2 minutes)

2. Get Credentials

In Supabase Dashboard → Project SettingsAPI:
  • Copy Project URL (e.g., https://abc123.supabase.co)
  • Copy Publishable key (starts with eyJ)
Never use the service_role key in your app - it bypasses RLS.

3. Configure Environment

Add to apps/app/.env:
EXPO_PUBLIC_BACKEND_PROVIDER=supabase
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-key-here

4. Run Database Schema

In Supabase Dashboard → SQL Editor, run the contents of supabase/schema.sql. This creates the profiles, user_preferences, and push_tokens tables with RLS policies.See Backend Guide for schema customization.

Security

RLS policies are pre-configured in the schema. auth.uid() returns the current user’s ID.
-- Users can only access their own data
create policy "Users access own data"
  on profiles for all
  using (auth.uid() = id);

-- Public read, authenticated write
create policy "Anyone can read"
  on posts for select using (true);

create policy "Users create own posts"
  on posts for insert
  with check (auth.uid() = author_id);
For more on RLS, see Supabase RLS Documentation.

Direct Client Access

For advanced use cases, access the Supabase client directly:
import { supabase } from '@/services/supabase'

// Auth operations
const { data: { user } } = await supabase.auth.getUser()
const { data: { session } } = await supabase.auth.getSession()

// Database queries
const { data } = await supabase.from('posts').select('*')
const { data } = await supabase.from('posts').insert({ title: 'New' })
For full API reference, see Supabase JS Client Docs.

Mock Mode

Without backend credentials, a mock client simulates auth and database operations. Test accounts:
{ email: 'demo@shipnative.app', password: 'demo123' }
{ email: 'test@shipnative.app', password: 'test123' }
Seed mock data:
import { mockSupabaseHelpers } from '@/services/mocks/supabase'

mockSupabaseHelpers.seedTable('posts', [
  { id: 1, title: 'First Post', author_id: 'user-1' },
])
See Mock Services for full documentation.

Troubleshooting

Still in mock mode?
  • Verify apps/app/.env exists with correct values
  • Check EXPO_PUBLIC_BACKEND_PROVIDER is set correctly
  • Env vars must start with EXPO_PUBLIC_
  • Restart Metro: yarn start --clear
Database queries fail?
  • Supabase: Check RLS policies in Dashboard
  • Convex: Check function auth guards
  • Verify user is logged in
Email confirmation not arriving?
  • Check spam folder
  • Supabase: Verify email is enabled in Authentication → Settings
  • Convex: Check your Auth.js email provider configuration
CORS errors on web?
  • Supabase: Add http://localhost:19006 to Project Settings → API → Allowed Origins
  • Convex: CORS is handled automatically

Next Steps