Shipnative supports two authentication backends: Supabase and Convex . Both provide email/password auth, social login, and secure session management.
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.
Magic link and OTP authentication works on both backends through useAuth(): 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)
Convex Setup Required Add these environment variables to your Convex deployment: RESEND_API_KEY = re_xxxxx # From resend.com
RESEND_FROM_EMAIL = noreply@yourdomain.com
APP_NAME = YourApp
Domain Verification: You must verify your sending domain in Resend before emails will deliver. During development, use onboarding@resend.dev as the from address.
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 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: 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
Account deletion calls a Convex action: // 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 ()
}
// 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
1. Create Supabase Project
Go to supabase.com/dashboard
Create a new project
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. 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. 1. Create Convex Project cd boilerplate
npx convex dev
This creates a project at dashboard.convex.dev and generates .env.local with your deployment URL. Add to apps/app/.env: EXPO_PUBLIC_BACKEND_PROVIDER = convex
EXPO_PUBLIC_CONVEX_URL = https://your-project.convex.cloud
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: # 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 for detailed provider setup.
Security
Supabase RLS
Convex Functions
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 . Convex uses function-level authentication. Use ctx.auth.getUserIdentity() to check auth: 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
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 . Use Convex hooks for data access: 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 .
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