Authentication

Last updated on 2026-09-03

The kit uses Supabase Auth for all authentication, with three client types for different contexts and middleware that protects only the /admin/* routes -- unlike the CRM kit which locks down the entire app.

Auth Flow Overview

User signs up --> Supabase creates auth.users row
             --> Trigger creates profiles row (role = 'author')
             --> User is redirected to /admin

User logs in --> Cookie-based session created via @supabase/ssr
            --> Middleware validates session on /admin/* routes
            --> Admin routes check profiles.role for permissions

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 client-side auth state:

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. For public blog routes, it operates without a session (anonymous access), returning only published posts and approved comments. For admin routes, it operates with the authenticated user's session.

Admin Client (lib/supabase/admin.ts)

Uses the service role key to bypass RLS. Used in Server Actions for write operations:

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()
  • Error messages for invalid credentials
  • Link to forgot password and register pages
  • Redirects to /admin 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 = 'author'

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 only admin routes. This is fundamentally different from the CRM kit, which protects everything by default. The blog kit keeps all public-facing routes open.

Protected Routes

Only /admin/* routes are protected. Everything else is public:

// Simplified middleware logic
const isAdminPath = pathname.startsWith("/admin")
const isAuthPath = ["/login", "/register", "/forgot-password"].some(
  (path) => pathname.startsWith(path)
)

// Protect admin routes
if (isAdminPath && !user) {
  redirect("/login")
}

// Redirect authenticated users away from auth pages
if (isAuthPath && user) {
  redirect("/admin")
}

Public Routes (No Auth Required)

  • / -- blog home page
  • /posts/[slug] -- article pages
  • /categories and /categories/[slug] -- category listings
  • /tags/[slug] -- tag listings
  • /authors/[slug] -- author profile pages
  • /search -- search page
  • /about -- about/team page
  • /newsletter -- newsletter archive
  • /rss -- RSS feed
  • /sitemap-preview -- sitemap data
  • /login, /register, /forgot-password -- auth pages
  • /auth/callback, /auth/confirm -- auth handlers

Protected Routes (Auth Required)

  • /admin -- dashboard
  • /admin/posts -- post management
  • /admin/posts/new -- post editor
  • /admin/posts/[id]/edit -- post editor
  • /admin/analytics -- analytics dashboard
  • /admin/comments -- comment moderation
  • /admin/authors -- author management
  • /admin/tags -- tag management
  • /admin/categories -- category management
  • /admin/media -- media library
  • /admin/subscribers -- subscriber management
  • /admin/scheduled -- scheduled posts
  • /admin/settings -- site settings

Role System

The kit uses a three-tier role system stored in profiles.role, checked via helper SQL functions. This is more granular than the CRM kit's two-tier member/admin model.

Role Hierarchy

Capability Author Editor Admin
Access admin dashboard Yes Yes Yes
Create posts Yes Yes Yes
Edit own posts Yes Yes Yes
Edit all posts No Yes Yes
Delete posts No Yes Yes
Create tags Yes Yes Yes
Delete tags No No Yes
Moderate comments No Yes Yes
Manage categories No No Yes
Upload media Yes Yes Yes
Delete media No Yes Yes
View subscribers No Yes Yes
Manage subscribers No Yes Yes
Manage newsletter No Yes Yes
View analytics Yes Yes Yes
Update site settings No No Yes
Manage author roles No No Yes
Delete anything No No Yes

How Roles Are Checked

In RLS policies, roles are checked via the SQL helper functions:

-- Used in RLS policies
is_blog_author_or_above()   -- returns true for author, editor, admin
is_blog_editor_or_above()   -- returns true for editor, admin
is_blog_admin()             -- returns true for admin only

In Server Actions, the role is read from the user's profile:

const supabase = await createServerClient()
const { data: { user } } = await supabase.auth.getUser()
const { data: profile } = await supabase
  .from("profiles")
  .select("role")
  .eq("id", user.id)
  .single()

if (profile.role !== "admin") {
  return { success: false, error: "Admin access required" }
}

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 author to admin

The seed data includes a pre-configured admin: admin@example.com / password123.

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, bio, 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