Authentication

Last updated on 2026-09-15

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 = 'rep' and status = 'invited'
  • New accounts can't see CRM data until an admin sets their status to active (see Making a User an Admin)

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

Access is controlled by two columns on profiles: role (admin, manager or rep) and status (active, invited or deactivated). Unlike the e-commerce kit, which uses app_metadata, admin status lives in the database and is checked by is_crm_admin(). Only active users count as team members or admins.

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

Users can't change their own role or status from the app (column-level grants block it), so make these changes in the Supabase SQL Editor or Dashboard:

-- Make a user an active admin
UPDATE public.profiles
SET role = 'admin', status = 'active'
WHERE id = (SELECT id FROM auth.users WHERE email = 'your-email@example.com');

-- Activate an invited teammate as a rep
UPDATE public.profiles
SET status = 'active'
WHERE id = (SELECT id FROM auth.users WHERE email = 'teammate@example.com');

Or use the Supabase Dashboard:

  1. Go to Table Editor > profiles
  2. Find the user's row
  3. Set status to active, and change role from rep to admin if needed

What Admins Can Do

Capability Rep 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 Yes Yes
Manage pipelines and stages Yes Yes
Manage team members No Yes
Update CRM settings No Yes
Delete tags Yes 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