Skip to content
GitHubTwitterDiscord

Authentication & User Management

LaunchKit uses Supabase Auth for comprehensive user authentication and management. This guide covers setup, configuration, and user management features.

LaunchKit’s authentication system includes:

  • Multiple sign-in methods: Email/password, OAuth, magic links
  • User profile management with automatic profile creation
  • Session management with secure cookies
  • Password reset and email verification
  • Admin user management and role-based access

In your Supabase Dashboard:

  1. Go to AuthenticationSettings
  2. Configure the following:

Site URL: Your app’s base URL

# Development
http://localhost:3000

# Production
https://yourdomain.com

Additional Redirect URLs:

# Development
http://localhost:3000/api/auth/callback
http://localhost:3000/dashboard

# Production
https://yourdomain.com/api/auth/callback
https://yourdomain.com/dashboard

Configure email templates in AuthenticationEmail Templates:

Subject: Confirm your signup

Body:

<h2>Confirm your signup</h2>
<p>Follow this link to confirm your user:</p>
<p><a href="{{ .ConfirmationURL }}">Confirm your email</a></p>

Enable desired OAuth providers in AuthenticationProviders:

  1. Go to Google Cloud Console

  2. Create a new project or select existing

  3. Enable Google+ API

  4. Create OAuth 2.0 credentials:

    • Application type: Web application
    • Authorized origins: https://[your-project].supabase.co
    • Authorized redirect URIs: https://[your-project].supabase.co/auth/v1/callback
  5. In Supabase, add your Google credentials:

    Client ID: your-google-client-id
    Client Secret: your-google-client-secret
  1. Go to GitHub → SettingsDeveloper settingsOAuth Apps

  2. Create new OAuth App:

    • Homepage URL: https://yourdomain.com
    • Authorization callback URL: https://[your-project].supabase.co/auth/v1/callback
  3. In Supabase, add your GitHub credentials:

    Client ID: your-github-client-id
    Client Secret: your-github-client-secret

LaunchKit uses the Supabase client for authentication:

// /libs/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

For server-side operations, use the server client:

// /libs/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // Handle Server Component context
          }
        },
      },
    }
  );
}
const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'password123',
  options: {
    data: {
      full_name: 'John Doe',
    },
  },
});

if (error) {
  console.error('Sign up error:', error.message);
} else if (data.user && !data.user.email_confirmed_at) {
  // Email confirmation required
  console.log('Check email for confirmation');
} else {
  // Success, redirect to dashboard
  router.push('/dashboard');
}
const handleOAuthSignIn = async (provider: Provider) => {
  const redirectURL = getBaseUrl() + '/api/auth/callback';

  await supabase.auth.signInWithOAuth({
    provider, // 'google', 'github', etc.
    options: {
      redirectTo: redirectURL,
    },
  });
};
const { error } = await supabase.auth.signInWithOtp({
  email: '[email protected]',
  options: {
    emailRedirectTo: `${window.location.origin}/api/auth/callback`,
  },
});

if (error) {
  console.error('Magic link error:', error.message);
} else {
  console.log('Check your email for the magic link');
}

When a user signs up, a profile is automatically created via database trigger:

-- Function to create profile on signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, email, name, avatar_url, created_at, updated_at, last_login)
  VALUES (
    NEW.id,
    NEW.email,
    COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
    NEW.raw_user_meta_data->>'avatar_url',
    (now() AT TIME ZONE 'UTC'),
    (now() AT TIME ZONE 'UTC'),
    (now() AT TIME ZONE 'UTC')
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '';

-- Trigger on auth.users table
CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

The profiles table stores extended user information:

interface Profile {
  id: string;                    // User ID (references auth.users)
  name: string | null;          // Display name
  email: string | null;         // Email address
  avatar_url: string | null;    // Profile picture URL
  customer_id: string | null;   // Stripe customer ID
  price_id: string | null;      // Current subscription price ID
  has_access: boolean;          // Access to paid features
  is_admin: boolean;            // Admin privileges
  created_at: string;           // Account creation date
  updated_at: string;           // Last profile update
  last_login: string | null;    // Last login timestamp
}
// Update user profile
const updateProfile = async (updates: Partial<Profile>) => {
  const { data, error } = await supabase
    .from('profiles')
    .update(updates)
    .eq('id', user.id)
    .select()
    .single();

  if (error) throw error;
  return data;
};

// Update auth metadata
const updateAuthMetadata = async (metadata: Record<string, any>) => {
  const { data, error } = await supabase.auth.updateUser({
    data: metadata
  });

  if (error) throw error;
  return data;
};
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@/libs/supabase/client';

function useUser() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const supabase = createClient();

  useEffect(() => {
    const getUser = async () => {
      const { data: { user } } = await supabase.auth.getUser();
      setUser(user);
      setLoading(false);
    };

    getUser();

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setUser(session?.user ?? null);
        setLoading(false);
      }
    );

    return () => subscription.unsubscribe();
  }, []);

  return { user, loading };
}

Create middleware to protect routes:

// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
        },
      },
    }
  );

  const { data: { user } } = await supabase.auth.getUser();

  // Protect dashboard routes
  if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
    return NextResponse.redirect(new URL('/signin', request.url));
  }

  // Protect admin routes
  if (request.nextUrl.pathname.startsWith('/dashboard/admin')) {
    if (!user) {
      return NextResponse.redirect(new URL('/signin', request.url));
    }

    // Check if user is admin
    const { data: profile } = await supabase
      .from('profiles')
      .select('is_admin')
      .eq('id', user.id)
      .single();

    if (!profile?.is_admin) {
      return NextResponse.redirect(new URL('/dashboard', request.url));
    }
  }

  return response;
}

export const config = {
  matcher: ['/dashboard/:path*'],
};
// Send reset email
const resetPassword = async (email: string) => {
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
    redirectTo: `${window.location.origin}/update-password`,
  });

  if (error) throw error;
};

// Update password with token
const updatePassword = async (newPassword: string) => {
  const { error } = await supabase.auth.updateUser({
    password: newPassword
  });

  if (error) throw error;
};
const changePassword = async (currentPassword: string, newPassword: string) => {
  // First verify current password
  const { error: signInError } = await supabase.auth.signInWithPassword({
    email: user.email,
    password: currentPassword,
  });

  if (signInError) {
    throw new Error('Current password is incorrect');
  }

  // Update to new password
  const { error } = await supabase.auth.updateUser({
    password: newPassword
  });

  if (error) throw error;
};
// /hooks/use-admin.tsx
import { useState, useEffect } from 'react';
import { createClient } from '@/libs/supabase/client';

export function useIsAdmin() {
  const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const supabase = createClient();

    const checkAdminStatus = async () => {
      try {
        const { data: { user: currentUser } } = await supabase.auth.getUser();
        setUser(currentUser);

        if (!currentUser) {
          setIsAdmin(false);
          setLoading(false);
          return;
        }

        const { data: profile } = await supabase
          .from('profiles')
          .select('is_admin')
          .eq('id', currentUser.id)
          .single();

        setIsAdmin(profile?.is_admin || false);
      } catch (error) {
        console.error('Error checking admin status:', error);
        setIsAdmin(false);
      } finally {
        setLoading(false);
      }
    };

    checkAdminStatus();

    const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
      if (event === 'SIGNED_OUT') {
        setIsAdmin(false);
        setUser(null);
        setLoading(false);
      } else if (event === 'SIGNED_IN' && session?.user) {
        setUser(session.user);
        checkAdminStatus();
      }
    });

    return () => subscription.unsubscribe();
  }, []);

  return { isAdmin, loading, user };
}
'use client';
import { useIsAdmin } from '@/hooks/use-admin';

function AdminPanel() {
  const { isAdmin, loading, user } = useIsAdmin();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!isAdmin) {
    return <div>Access denied. Admin privileges required.</div>;
  }

  return (
    <div>
      <h1>Admin Dashboard</h1>
      <p>Welcome, {user?.email}</p>
      {/* Admin-only content */}
    </div>
  );
}
-- Make a user admin
UPDATE profiles
SET is_admin = true
WHERE email = '[email protected]';

Update last login timestamp:

const updateLastLogin = async (userId: string) => {
  const { error } = await supabase
    .from('profiles')
    .update({ last_login: new Date().toISOString() })
    .eq('id', userId);

  if (error) console.error('Error updating last login:', error);
};
// Get user statistics
const getUserStats = async () => {
  const { data, error } = await supabase
    .from('profiles')
    .select(`
      id,
      created_at,
      last_login,
      has_access,
      is_admin
    `);

  if (error) throw error;

  const stats = {
    totalUsers: data.length,
    activeUsers: data.filter(u => u.has_access).length,
    adminUsers: data.filter(u => u.is_admin).length,
    newUsersThisMonth: data.filter(u =>
      new Date(u.created_at) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
    ).length,
  };

  return stats;
};

Ensure RLS policies are properly configured:

-- Users can only view/edit their own profile
CREATE POLICY "Users can view own profile" ON profiles
  FOR SELECT USING (auth.uid() = id);

CREATE POLICY "Users can update own profile" ON profiles
  FOR UPDATE USING (auth.uid() = id);

-- Admins can manage all profiles
CREATE POLICY "Admins can manage all profiles" ON profiles
  FOR ALL USING (
    EXISTS (
      SELECT 1 FROM profiles
      WHERE profiles.id = auth.uid()
      AND profiles.is_admin = true
    )
  );

Implement rate limiting for authentication endpoints:

// Simple rate limiting example
const rateLimiter = new Map();

const checkRateLimit = (email: string) => {
  const key = `auth:${email}`;
  const now = Date.now();
  const limit = rateLimiter.get(key);

  if (limit && now - limit.timestamp < 60000) { // 1 minute
    if (limit.attempts >= 5) {
      throw new Error('Too many attempts. Please try again later.');
    }
    limit.attempts++;
  } else {
    rateLimiter.set(key, { attempts: 1, timestamp: now });
  }
};

Always validate user inputs:

const validateEmail = (email: string) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};

const validatePassword = (password: string) => {
  return password.length >= 6 && password.length <= 128;
};

Email Confirmation Not Working:

  • Check email templates in Supabase dashboard
  • Verify redirect URLs are correctly configured
  • Check spam folder for confirmation emails

OAuth Provider Errors:

  • Verify client ID and secret are correct
  • Check redirect URIs match exactly
  • Ensure OAuth app is published/approved

Session Not Persisting:

  • Verify middleware is correctly configured
  • Check cookie settings and domain
  • Ensure HTTPS in production

RLS Policy Denying Access:

  • Check policy conditions match your use case
  • Verify user has necessary permissions
  • Use service role key for admin operations

Check User Session:

const debugSession = async () => {
  const { data: { session } } = await supabase.auth.getSession();
  console.log('Current session:', session);

  const { data: { user } } = await supabase.auth.getUser();
  console.log('Current user:', user);
};

Test RLS Policies:

-- Test as specific user
SELECT auth.uid(); -- Should return user ID
SELECT * FROM profiles WHERE id = auth.uid();