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()withuser_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
- The seed data creates an admin user:
admin@example.com/password123 - The
profilestable has arolecolumn set toadminfor this user - 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:
- Go to Table Editor > profiles
- Find the user's row
- Change
rolefrommembertoadmin
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