> ## 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>
  )
}
```

> [!NOTE]
>
> All auth methods work with both backends - no need for provider-specific hooks!

### 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

> [accordion] 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] 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] Sign Out
>
> ```typescript theme={null}
> const { signOut } = useAuth()
>
> const handleLogout = async () => {
>   const { error } = await signOut()
>   if (error) console.error('Sign out error:', error)
> }
> ```

> [accordion] 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] 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.

> [accordion] 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](https://docs.shipnative.app/core-features/social-login.md) for provider configuration.

> [accordion] 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.
>
> ### 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.

> [accordion] 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')
> }
> ```

***

## Account Deletion

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

> [tab] 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] 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.
>   },
> })
> ```

***

## Manual Setup

> [tab] 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](https://docs.shipnative.app/core-features/backend.md) for schema customization.

> [tab] 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.

***

## Security

> [tab] 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] 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,
>     })
>   },
> })
> ```

***

## Direct Client Access

> [tab] 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] 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).

***

## 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](https://docs.shipnative.app/development/mock-services.md) 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

> [card] [Social Login](https://docs.shipnative.app/core-features/social-login.md)
>
> Add Google, Apple, and other OAuth providers

> [card] [Database & Backend](https://docs.shipnative.app/core-features/backend.md)
>
> Store user data, files, and build backend features

> [card] [User Preferences](https://docs.shipnative.app/development/state-management.md)
>
> Save app settings per user with Zustand

> [card] [Push Notifications](https://docs.shipnative.app/core-features/push-notifications.md)
>
> Send notifications to authenticated users


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.