Authentication

Last updated on 2026-09-02

The kit uses Supabase Auth for all authentication, with three client types for different contexts and middleware that protects all dashboard routes.

Auth Flow Overview

User signs up → Supabase creates auth.users row
             → Trigger creates profiles row (role = 'member')
             → User is redirected to /

User logs in → Cookie-based session created via @supabase/ssr
            → Middleware validates session on all dashboard routes
            → Admin routes check profiles.role

Supabase Client Types

The kit uses three Supabase clients, each for a different context:

Browser Client (lib/supabase/client.ts)

Used in client components for real-time subscriptions and client-side auth:

import { createBrowserClient } from "@supabase/ssr"

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

Server Client (lib/supabase/server.ts)

Used in Server Components and Server Actions. Reads cookies for the current session:

import { createServerClient } from "@supabase/ssr"
import { cookies } from "next/headers"

This client respects RLS policies -- queries return only data the authenticated user is allowed to see. In the shared workspace model, all team members can read all CRM data.

Admin Client (lib/supabase/admin.ts)

Uses the service role key to bypass RLS. Used in Server Actions for operations that need elevated access:

import { createClient } from "@supabase/supabase-js"

export const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

Security: The service role key is never exposed to the client. It only runs in Server Actions.

Auth Pages

Login (/login)

  • Email and password sign-in via supabase.auth.signInWithPassword()
  • Google OAuth via supabase.auth.signInWithOAuth({ provider: 'google' })
  • GitHub OAuth via supabase.auth.signInWithOAuth({ provider: 'github' })
  • Error messages for invalid credentials
  • Redirects to / (dashboard) on success

Register (/register)

  • Email, password, and full name
  • Calls supabase.auth.signUp() with user_metadata: { full_name }
  • Shows "Check your email" confirmation message
  • Database trigger creates the profile automatically with role = 'member'

Forgot Password (/forgot-password)

  • Calls supabase.auth.resetPasswordForEmail()
  • Shows success state after sending

OAuth Callback (/auth/callback)

Handles the OAuth code exchange after social login:

// Exchanges the code for a session
const { searchParams } = new URL(request.url)
const code = searchParams.get("code")
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)

Email Confirmation (/auth/confirm)

Handles email verification links for new signups and password resets.

Middleware

The middleware.ts file at the project root protects all routes except public ones. Unlike the e-commerce kit (which only protects /account/* and /admin/*), this CRM kit protects everything by default.

Protected Routes

All routes are protected except:

  • /login
  • /register
  • /forgot-password
  • /auth/callback
  • /auth/confirm
  • Static assets (/_next/, /favicon.ico, etc.)

Middleware Logic

// Simplified middleware logic
const publicPaths = ["/login", "/register", "/forgot-password", "/auth"]

const isPublicPath = publicPaths.some((path) =>
  pathname.startsWith(path)
)

if (!isPublicPath && !user) {
  redirect("/login")
}

if (isPublicPath && user && !pathname.startsWith("/auth")) {
  redirect("/")
}

Unauthenticated users trying to access any dashboard route are redirected to /login. Authenticated users trying to access /login or /register are redirected to the dashboard.

Admin Access

Admin access is controlled via the profiles.role column -- not app_metadata like the e-commerce kit. This means admin status is stored in the database and checked via the is_crm_admin() function.

How It Works

  1. The seed data creates an admin user: admin@example.com / password123
  2. The profiles table has a role column set to admin for this user
  3. RLS policies use is_crm_admin() to gate admin-only operations

Making a User an Admin

Update their profile directly in the database:

UPDATE profiles SET role = 'admin' WHERE email = 'your-email@example.com';

Or use the Supabase Dashboard:

  1. Go to Table Editor > profiles
  2. Find the user's row
  3. Change role from member to admin

What Admins Can Do

Capability Member Admin
View all CRM data Yes Yes
Create/edit contacts, companies, deals Yes Yes
Create/edit tasks, activities, emails Yes Yes
Delete contacts, companies, deals No Yes
Manage pipelines and stages No Yes
Manage team members No Yes
Update CRM settings No Yes
Delete tags No Yes

useUser Hook

The hooks/use-user.ts hook provides the current user in client components:

const { user, profile, loading } = useUser()

It listens for auth state changes and updates automatically on login/logout. The profile object includes the user's role, full name, and avatar.

Session Management

  • Sessions are stored in HTTP-only cookies via @supabase/ssr
  • The middleware refreshes sessions on every request
  • Sessions expire based on your Supabase project settings (default: 1 hour, with refresh)
  • Logout calls supabase.auth.signOut() and clears the cookie
  • After logout, the user is redirected to /login