Skip to main content

πŸ” Authentication

Shipnative uses Supabase for user authentication. This guide shows you how to add login, signup, and user management to your app.
For AI Users: This entire guide is in the vibe/ context files. You can ask your AI to β€œadd a profile screen” or β€œimplement password reset” and it will use these patterns automatically.

πŸš€ Quick Start (2 Minutes)

Want to start building auth features right away? You can skip all the setup! Shipnative works in mock mode by default - authentication works out of the box without any API keys.
# Just run your app - auth already works!
cd apps/app
yarn ios  # or yarn android
You’ll see:
  • βœ… Login/signup screens already working
  • βœ… User sessions persisted
  • βœ… Password reset flows functional
  • πŸ”¬ Mock mode indicator in dev menu
When to add real Supabase:
  • When you’re ready to deploy
  • When you need to test with real user data
  • When building backend features (database, storage, realtime)
For now, keep building with mocks! Jump to Using Authentication to learn how to use the auth system.

πŸ“– Understanding Supabase Auth

Before diving into code, let’s understand how authentication works in Shipnative.

How It Works

User enters email/password
       ↓
   Shipnative app
       ↓
   Supabase Auth ← Handles authentication
       ↓
   Returns session token
       ↓
   App stores token securely
       ↓
   User stays logged in

Key Concepts

Session: When a user logs in, Supabase creates a β€œsession” - think of it like a backstage pass that proves the user is authenticated. Secure Storage: The session is stored in your device’s secure storage (like a password manager built into iOS/Android). The user stays logged in even if they close the app. Row Level Security (RLS): Database rules that ensure users can only access their own data. Like a bouncer at a club checking IDs. Mock Mode: Simulated authentication that works without Supabase. Perfect for development!

🎯 Using Authentication

Shipnative provides a useAuth hook that makes authentication easy. Here’s how to use it:
import { useAuth } from '@/hooks/useAuth'

function LoginScreen() {
  const {
    user,              // Current user object (or null if logged out)
    signIn,            // Function to log in
    signOut,           // Function to log out
    loading,           // Boolean: is auth loading?
    isAuthenticated    // Boolean: is user logged in?
  } = useAuth()

  const handleLogin = async () => {
    const { error } = await signIn({
      email: 'user@example.com',
      password: 'password123',
    })

    if (error) {
      alert(error.message)
    }
    // Success! User is now logged in
  }

  if (loading) {
    return <Spinner />
  }

  if (isAuthenticated) {
    return (
      <View>
        <Text>Welcome {user?.email}!</Text>
        <Button onPress={signOut} title="Sign Out" />
      </View>
    )
  }

  return <Button onPress={handleLogin} title="Sign In" />
}

Common Authentication Tasks

import { useAuth } from '@/hooks/useAuth'

function SignUpScreen() {
  const { signUp } = useAuth()

  const handleSignUp = async () => {
    const { error } = await signUp({
      email: 'newuser@example.com',
      password: 'securePassword123!',
      options: {
        data: {
          first_name: 'John',
          last_name: 'Doe',
        },
      },
    })

    if (error) {
      alert(error.message)
    } else {
      alert('Check your email to confirm your account!')
    }
  }

  return <Button onPress={handleSignUp} title="Create Account" />
}
What happens:
  1. User enters email and password
  2. Supabase creates an account
  3. Sends confirmation email (in production)
  4. User clicks link to verify
  5. Account is active!
In mock mode: Account is created instantly, no email needed.
const { signIn } = useAuth()

const handleLogin = async () => {
  const { error } = await signIn({
    email: 'user@example.com',
    password: 'password123',
  })

  if (error) {
    if (error.message.includes('Invalid login credentials')) {
      alert('Wrong email or password')
    } else {
      alert('Login failed: ' + error.message)
    }
  }
}
Common errors:
  • β€œInvalid login credentials” = wrong email/password
  • β€œEmail not confirmed” = user hasn’t verified email
  • Network errors = check internet connection
const { signOut } = useAuth()

const handleLogout = async () => {
  const { error } = await signOut()
  if (error) {
    console.error('Logout error:', error)
  }
  // User is logged out, navigate to login screen
}
This clears the session from secure storage. User will need to log in again.
const { resetPassword } = useAuth()

const handleForgotPassword = async () => {
  const { error } = await resetPassword('user@example.com')

  if (error) {
    alert(error.message)
  } else {
    alert('Check your email for a password reset link!')
  }
}
What happens:
  1. User enters email
  2. Supabase sends password reset email
  3. User clicks link
  4. User enters new password
  5. Password is updated
In mock mode: Shows success message, no actual email sent.
const { updateUser, user } = useAuth()

const handleUpdateProfile = async () => {
  const { error } = await updateUser({
    data: {
      first_name: 'Jane',
      avatar_url: 'https://example.com/avatar.jpg',
    },
  })

  if (error) {
    alert('Update failed: ' + error.message)
  } else {
    alert('Profile updated!')
  }
}
This updates the user_metadata field in Supabase Auth. For storing additional profile data (bio, preferences, etc.), use the database (see Backend Guide).
const { signInWithGoogle, signInWithApple } = useAuth()

const handleGoogleLogin = async () => {
  const { error } = await signInWithGoogle()
  if (error) alert(error.message)
}

const handleAppleLogin = async () => {
  const { error } = await signInWithApple()
  if (error) alert(error.message)
}
For complete social login setup (requires additional configuration), see Social Login Guide.
const { isAuthenticated, user } = useAuth()

if (isAuthenticated) {
  console.log('User is logged in:', user.email)
} else {
  console.log('User is logged out')
}
You can use this to show/hide features, navigate to different screens, etc.

πŸ› οΈ Production Setup

Ready to add real Supabase authentication? Follow these steps.
Skip if you’re just building: You can build your entire app with mock mode. Only set up real Supabase when you’re ready to deploy or need real user data.

Step 1: Create Supabase Project

  1. Go to supabase.com/dashboard/projects
  2. Click β€œNew project”
  3. Choose an organization (or create one)
  4. Set project name and database password
  5. Wait 2-3 minutes for provisioning

Step 2: Get API Credentials

  1. In your Supabase dashboard, go to Project Settings (βš™οΈ icon)
  2. Click API in the sidebar
  3. Copy your Project URL (looks like https://abc123.supabase.co)
  4. Copy your anon public key (long string starting with eyJ...)

Step 3: Add to Your App

Open apps/app/.env and add:
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
Restart your app:
yarn ios  # or yarn android
You’ll see in the console:
βœ… [Supabase] Connected to production Supabase
That’s it! Your app now uses real Supabase authentication.

Step 4: Set Up Database Schema

Your app needs a database to store user profiles and app data. Shipnative includes a ready-to-use schema.
  1. In Supabase dashboard, go to SQL Editor
  2. Click β€œNew query”
  3. Open shipnativeapp/supabase-schema.sql from your project
  4. Copy and paste the entire file
  5. Click β€œRun”
What this creates:
  • profiles table - User profile info (name, avatar, etc.)
  • user_preferences table - App settings (dark mode, notifications, language)
  • push_tokens table - For push notifications
  • Row Level Security (RLS) policies - Data security rules
  • Automatic triggers - Auto-create profile on signup
See BACKEND.md for detailed database documentation and how to customize the schema.

πŸ”’ Advanced: Row Level Security (RLS)

Beginners can skip this section. RLS is already configured in the schema. Come back to this when you need to customize security rules.
For AI Users: Security policies are documented in vibe/SERVICES.md. Ask your AI to β€œadd a private posts feature” and it will generate appropriate RLS policies.

What is RLS?

Row Level Security ensures users can only access their own data. Without it, any user could read/modify anyone’s data. Example: In a social app:
  • βœ… Users can read all public posts
  • βœ… Users can only edit their own posts
  • ❌ Users can’t edit other people’s posts
  • ❌ Users can’t see private posts from others

How It Works

-- Without RLS (BAD - anyone can read everything)
select * from posts;  -- Returns ALL posts from ALL users

-- With RLS (GOOD - users only see allowed posts)
select * from posts;  -- Returns only posts user is allowed to see

Example Policy

Let’s say you have a posts table:
-- Enable RLS on the posts table
alter table posts enable row level security;

-- Policy: Anyone can read published posts
create policy "Public posts are viewable by everyone"
  on posts for select
  using (published = true);

-- Policy: Users can only create their own posts
create policy "Users can create own posts"
  on posts for insert
  with check (auth.uid() = author_id);

-- Policy: Users can only edit their own posts
create policy "Users can update own posts"
  on posts for update
  using (auth.uid() = author_id);

-- Policy: Users can delete their own posts
create policy "Users can delete own posts"
  on posts for delete
  using (auth.uid() = author_id);
What auth.uid() means: The ID of the currently logged-in user. Supabase automatically knows who’s making the request.

Common RLS Patterns

-- 1. Public read, authenticated write
create policy "Anyone can read"
  on table_name for select
  using (true);  -- true = everyone

create policy "Authenticated users can write"
  on table_name for insert
  with check (auth.uid() is not null);  -- Must be logged in

-- 2. Own data only
create policy "Users can access own data"
  on table_name for all
  using (auth.uid() = user_id);

-- 3. Based on a relationship
create policy "Users can read their team's data"
  on tasks for select
  using (
    auth.uid() in (
      select user_id from team_members
      where team_id = tasks.team_id
    )
  );

Testing RLS Policies

In Supabase SQL Editor, you can test policies:
-- Test as a specific user
select auth.jwt();  -- See current user
set role authenticated;
set request.jwt.claim.sub = 'user-uuid-here';

-- Now run queries - they'll use RLS
select * from posts;  -- Will only show posts this user can see
For more on RLS, see Supabase RLS Documentation

πŸ§ͺ Direct Client Access (Advanced)

Most of the time, you should use the useAuth hook. But sometimes you need direct access to the Supabase client.
import { supabase } from '@/services/supabase'

// Get current user
const { data: { user } } = await supabase.auth.getUser()

// Get current session
const { data: { session } } = await supabase.auth.getSession()

// Listen to auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    console.log('Auth event:', event)  // 'SIGNED_IN', 'SIGNED_OUT', etc.
    console.log('Session:', session)
  }
)

// Don't forget to cleanup!
subscription.unsubscribe()

Database Operations

Once you have auth set up, you can query your database:
import { supabase } from '@/services/supabase'

// Get all rows
const { data, error } = await supabase
  .from('posts')
  .select('*')

// Get single row
const { data: post, error } = await supabase
  .from('posts')
  .select('*')
  .eq('id', 1)
  .single()

// Insert data
const { data, error } = await supabase
  .from('posts')
  .insert({
    title: 'My Post',
    content: 'Hello world',
    author_id: user.id,  // RLS will check this!
  })

// Update data
const { data, error } = await supabase
  .from('posts')
  .update({ title: 'Updated Title' })
  .eq('id', 1)

// Delete data
const { data, error } = await supabase
  .from('posts')
  .delete()
  .eq('id', 1)
For complete database API, see Supabase JavaScript Client Docs

πŸ§ͺ Mock Mode Features

When you build without Supabase API keys, Shipnative uses a mock Supabase client that simulates: Authentication:
  • βœ… Sign up with email/password
  • βœ… Sign in with email/password
  • βœ… Sign out
  • βœ… Session persistence
  • βœ… Password reset (simulated)
  • βœ… User profile updates
  • βœ… Auth state listeners
Database:
  • βœ… Select queries with filters (eq, neq, gt, lt, like, etc.)
  • βœ… Insert (single and multiple)
  • βœ… Update with filters
  • βœ… Delete with filters
  • βœ… Upsert
  • βœ… Ordering and pagination
  • βœ… Single row queries
Default Mock Users:
// These accounts work in mock mode:
{ email: 'demo@shipnative.app', password: 'demo123' }
{ email: 'test@shipnative.app', password: 'test123' }
{ email: 'user@example.com', password: 'password' }
Mock Data Helpers:
import { mockSupabaseHelpers } from '@/services/mocks/supabase'

// Get all mock users
const users = mockSupabaseHelpers.getUsers()

// Add mock data to a table
mockSupabaseHelpers.seedTable('posts', [
  { id: 1, title: 'First Post', author_id: 'user-1' },
  { id: 2, title: 'Second Post', author_id: 'user-2' },
])

// Clear all mock data
mockSupabaseHelpers.clearAll()

πŸ†˜ Troubleshooting

Symptoms: Console shows β€πŸ” [MockSupabase] Initialized mock Supabase client”Solutions:
  1. Check that apps/app/.env exists and has the correct values
  2. Verify env vars start with EXPO_PUBLIC_ (required for Expo)
  3. Restart Metro bundler: yarn start --clear
  4. Check for typos in env var names
  5. If using EAS Build, add env vars to eas.json or environment secrets
Cause: Session isn’t being persisted to secure storageSolutions:
  1. Check that persistSession: true is set in Supabase client init
  2. On iOS: Check Keychain permissions
  3. On Android: Check that app has storage permissions
  4. Check console for SecureStore errors
  5. Try uninstalling and reinstalling the app
Cause: Usually Row Level Security (RLS) blocking accessSolutions:
  1. Check Supabase dashboard logs (Authentication > Logs)
  2. Verify user is logged in (auth.uid() is not null)
  3. Check RLS policies in Supabase dashboard (Database > Tables > [table] > Policies)
  4. Make sure you ran supabase-schema.sql to set up policies
  5. Test query in Supabase SQL Editor with set role authenticated
Cause: Email templates not configured or email provider issueSolutions:
  1. In Supabase dashboard, go to Authentication > Email Templates
  2. Customize β€œConfirm signup” template
  3. Check Authentication > Settings > Email Auth is enabled
  4. For development, disable email confirmation in Settings
  5. Check spam folder for confirmation emails
Cause: Supabase blocking web originSolutions:
  1. In Supabase dashboard, go to Project Settings > API
  2. Add your domain to β€œAllowed Origins (CORS)”
  3. Add http://localhost:19006 for local development
  4. Add your production domain (e.g., https://yourapp.com)
  5. Restart dev server after changes
Cause: Session token expired or corruptedSolutions:
  1. Log out and log back in
  2. Clear app data (uninstall/reinstall)
  3. Check that device clock is correct
  4. In Supabase dashboard, check JWT expiration settings

πŸŽ“ Next Steps