> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipnative.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> User login, signup, and account management with Supabase or Convex

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

## Setup

```bash theme={null}
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:

```typescript theme={null}
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>
  )
}
```

<Info>
  All auth methods work with both backends - no need for provider-specific hooks!
</Info>

### Unified User Object

The `AppUser` interface normalizes user data across backends:

```typescript theme={null}
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

<AccordionGroup>
  <Accordion title="Sign Up">
    ```typescript theme={null}
    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
  </Accordion>

  <Accordion title="Sign In">
    ```typescript theme={null}
    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
  </Accordion>

  <Accordion title="Sign Out">
    ```typescript theme={null}
    const { signOut } = useAuth()

    const handleLogout = async () => {
      const { error } = await signOut()
      if (error) console.error('Sign out error:', error)
    }
    ```
  </Accordion>

  <Accordion title="Reset Password">
    ```typescript theme={null}
    const { resetPassword } = useAuth()

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

  <Accordion title="Update Profile">
    ```typescript theme={null}
    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

    <Tip>
      The `updateProfile` action handles the backend differences automatically. Use `firstName`/`lastName` or `fullName` - it normalizes appropriately for each backend.
    </Tip>
  </Accordion>

  <Accordion title="Social Login">
    ```typescript theme={null}
    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](/core-features/social-login) for provider configuration.
  </Accordion>

  <Accordion title="Magic Link / OTP">
    Magic link and OTP authentication works on both backends through `useAuth()`:

    ```typescript theme={null}
    const { signInWithMagicLink, verifyOtp } = useAuth()
    const [codeSent, setCodeSent] = useState(false)

    // Step 1: Send magic link or OTP code
    const handleSendCode = async () => {
      const { error } = await signInWithMagicLink('user@example.com')
      if (!error) {
        setCodeSent(true)
        alert('Check your email for a login link or code!')
      }
    }

    // Step 2: Verify OTP code (if using OTP mode)
    const handleVerify = async (code: string) => {
      const { error } = await verifyOtp('user@example.com', code)
      if (!error) {
        // User is now logged in
      }
    }
    ```

    **Backend differences:**

    * **Supabase**: Sends magic link or OTP depending on your Supabase project settings
    * **Convex**: Sends OTP code via Resend (requires RESEND\_API\_KEY in Convex dashboard)

    ### Supabase Setup Required

    For magic links to work on web, configure redirect URLs in **Supabase Dashboard** → **Authentication** → **URL Configuration** → **Redirect URLs**:

    ```bash theme={null}
    # Local development
    http://localhost:3000/auth/callback    # Vite web
    http://localhost:19006/auth/callback   # Expo web

    # Production (replace with your domain)
    https://yourdomain.com/auth/callback
    ```

    <Tip>
      The app automatically uses the current origin for redirect URLs, so you just need to whitelist them in Supabase. On mobile, users enter the OTP code manually instead of clicking a link.
    </Tip>

    ### Convex Setup Required

    Add these environment variables to your Convex deployment:

    ```bash theme={null}
    RESEND_API_KEY=re_xxxxx          # From resend.com
    RESEND_FROM_EMAIL=noreply@yourdomain.com
    APP_NAME=YourApp
    ```

    <Warning>
      **Domain Verification:** You must verify your sending domain in Resend before emails will deliver. During development, use `onboarding@resend.dev` as the from address.
    </Warning>
  </Accordion>

  <Accordion title="Check Auth State">
    ```typescript theme={null}
    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')
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Account Deletion

Apple requires apps to provide account deletion. The Profile screen includes a Delete Account button.

<Tabs>
  <Tab title="Supabase">
    Account deletion calls a Supabase Edge Function:

    ```bash theme={null}
    npx supabase functions deploy delete-user --no-verify-jwt
    ```

    ### What Gets Deleted

    | Service          | Action                                          |
    | ---------------- | ----------------------------------------------- |
    | Supabase Auth    | User deleted                                    |
    | Supabase Profile | Row deleted                                     |
    | RevenueCat       | Subscriber data deleted (if secrets configured) |
    | PostHog          | User data deleted (if secrets configured)       |

    ### Optional: GDPR Cleanup

    To delete data from third-party services, add secrets to Supabase:

    ```bash theme={null}
    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
    ```
  </Tab>

  <Tab title="Convex">
    Account deletion calls a Convex action:

    ```typescript theme={null}
    // In your component
    import { useAction } from '@/hooks/convex'
    import { api } from '@convex/_generated/api'

    const deleteAccount = useAction(api.users.deleteAccount)

    const handleDelete = async () => {
      await deleteAccount()
    }
    ```

    ```typescript theme={null}
    // convex/users.ts
    export const deleteAccount = action({
      handler: async (ctx) => {
        const identity = await ctx.auth.getUserIdentity()
        if (!identity) throw new Error('Not authenticated')

        // Delete user data from your tables
        await ctx.runMutation(internal.users.deleteUserData, {
          userId: identity.subject,
        })

        // Optional: Delete from RevenueCat, PostHog, etc.
      },
    })
    ```
  </Tab>
</Tabs>

***

## Manual Setup

<Tabs>
  <Tab title="Supabase">
    ### 1. Create Supabase Project

    1. Go to [supabase.com/dashboard](https://supabase.com/dashboard/projects)
    2. Create a new project
    3. Wait for provisioning (\~2 minutes)

    ### 2. Get Credentials

    In Supabase Dashboard → **Project Settings** → **API**:

    * 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`:

    ```bash theme={null}
    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](/core-features/backend) for schema customization.
  </Tab>

  <Tab title="Convex">
    ### 1. Create Convex Project

    ```bash theme={null}
    cd boilerplate
    npx convex dev
    ```

    This creates a project at [dashboard.convex.dev](https://dashboard.convex.dev) and generates `.env.local` with your deployment URL.

    ### 2. Configure Environment

    Add to `apps/app/.env`:

    ```bash theme={null}
    EXPO_PUBLIC_BACKEND_PROVIDER=convex
    EXPO_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
    ```

    ### 3. Configure Auth Providers

    The boilerplate includes pre-configured providers in `convex/auth.ts`:

    * **Password** - Email/password authentication
    * **Google** - OAuth with Google
    * **Apple** - OAuth with Apple
    * **GitHub** - OAuth with GitHub
    * **ResendOTP** - Magic link / OTP via email

    ### 4. Set Environment Variables

    In the Convex Dashboard → Settings → Environment Variables:

    ```bash theme={null}
    # Required for OTP/Magic Link
    RESEND_API_KEY=re_xxxxx
    RESEND_FROM_EMAIL=noreply@yourdomain.com
    APP_NAME=YourApp

    # OAuth providers (optional)
    GOOGLE_CLIENT_ID=xxxxx
    GOOGLE_CLIENT_SECRET=xxxxx
    APPLE_CLIENT_ID=xxxxx
    APPLE_CLIENT_SECRET=xxxxx
    GITHUB_CLIENT_ID=xxxxx
    GITHUB_CLIENT_SECRET=xxxxx
    ```

    See [Convex Auth documentation](https://labs.convex.dev/auth) for detailed provider setup.
  </Tab>
</Tabs>

***

## Security

<Tabs>
  <Tab title="Supabase RLS">
    RLS policies are pre-configured in the schema. `auth.uid()` returns the current user's ID.

    ```sql theme={null}
    -- 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](https://supabase.com/docs/guides/auth/row-level-security).
  </Tab>

  <Tab title="Convex Functions">
    Convex uses function-level authentication. Use `ctx.auth.getUserIdentity()` to check auth:

    ```typescript theme={null}
    import { mutation, query } from './_generated/server'

    export const getMyPosts = query({
      handler: async (ctx) => {
        const identity = await ctx.auth.getUserIdentity()
        if (!identity) return []

        return await ctx.db
          .query('posts')
          .withIndex('by_author', (q) => q.eq('authorId', identity.subject))
          .collect()
      },
    })

    export const createPost = mutation({
      args: { title: v.string(), content: v.string() },
      handler: async (ctx, args) => {
        const identity = await ctx.auth.getUserIdentity()
        if (!identity) throw new Error('Not authenticated')

        return await ctx.db.insert('posts', {
          ...args,
          authorId: identity.subject,
        })
      },
    })
    ```
  </Tab>
</Tabs>

***

## Direct Client Access

<Tabs>
  <Tab title="Supabase">
    For advanced use cases, access the Supabase client directly:

    ```typescript theme={null}
    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](https://supabase.com/docs/reference/javascript/introduction).
  </Tab>

  <Tab title="Convex">
    Use Convex hooks for data access:

    ```typescript theme={null}
    import { useQuery, useMutation, useAction } from '@/hooks/convex'
    import { api } from '@convex/_generated/api'

    // Reactive queries (auto-update)
    const posts = useQuery(api.posts.list)
    const myPosts = useQuery(api.posts.getMyPosts)

    // Mutations
    const createPost = useMutation(api.posts.create)
    await createPost({ title: 'New Post', content: '...' })

    // Actions (for external API calls)
    const sendEmail = useAction(api.emails.send)
    ```

    For full API reference, see [Convex React Docs](https://docs.convex.dev/client/react).
  </Tab>
</Tabs>

***

## Mock Mode

Without backend credentials, a mock client simulates auth and database operations.

**Test accounts:**

```typescript theme={null}
{ email: 'demo@shipnative.app', password: 'demo123' }
{ email: 'test@shipnative.app', password: 'test123' }
```

**Seed mock data:**

```typescript theme={null}
import { mockSupabaseHelpers } from '@/services/mocks/supabase'

mockSupabaseHelpers.seedTable('posts', [
  { id: 1, title: 'First Post', author_id: 'user-1' },
])
```

See [Mock Services](/development/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

<CardGroup cols={2}>
  <Card title="Social Login" icon="users" href="/core-features/social-login">
    Add Google, Apple, and other OAuth providers
  </Card>

  <Card title="Database & Backend" icon="database" href="/core-features/backend">
    Store user data, files, and build backend features
  </Card>

  <Card title="User Preferences" icon="sliders" href="/development/state-management">
    Save app settings per user with Zustand
  </Card>

  <Card title="Push Notifications" icon="bell" href="/core-features/push-notifications">
    Send notifications to authenticated users
  </Card>
</CardGroup>
