xiaohongshu
xiaohongshu
auth-supabase
Implements standard Supabase authentication flows including signup, login, password reset, OAuth providers, email verification, and session management with complete security best practices
Full skill instructions
This skill provides comprehensive guidelines for implementing authentication using Supabase, covering all authentication patterns, security practices, and environment configuration.
✅ REQUIRED VERSIONS (Latest Stable):
{
"dependencies": {
"@supabase/supabase-js": "^2.89.0",
"@supabase/ssr": "^0.8.0",
"next": "^16.1.1"
}
}
❌ DEPRECATED PACKAGES - DO NOT USE:
{
"@supabase/auth-helpers-nextjs": "...", // ❌ DEPRECATED
"@supabase/auth-helpers-react": "...", // ❌ DEPRECATED
"@supabase/auth-ui-react": "..." // ❌ Optional, not required
}
🔒 Security Rule:
@supabase/ssr for Next.js 13+ App Routerauth-helpers packagesnpm update @supabase/supabase-js @supabase/ssr❌ ANTI-PATTERN - NEVER DO THIS:
// ❌ WRONG - Server action inside client component
'use client';
export default function LoginPage() {
async function login(formData: FormData) {
'use server'; // ❌ Mixed directives cause issues
// ...
}
}
✅ REQUIRED PATTERN - ALWAYS USE THIS:
// app/actions/auth.ts
'use server'; // ✅ Dedicated file for all auth actions
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function loginAction(formData: FormData) {
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
const { error } = await supabase.auth.signInWithPassword(data)
if (error) {
return { error: error.message }
}
redirect('/dashboard')
}
export async function signUpAction(formData: FormData) {
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
options: {
data: {
full_name: formData.get('full_name') as string,
},
},
}
const { error } = await supabase.auth.signUp(data)
if (error) {
return { error: error.message }
}
redirect('/auth/confirm')
}
export async function logoutAction() {
const supabase = await createClient()
await supabase.auth.signOut()
redirect('/login')
}
// app/(auth)/login/page.tsx
'use client'; // ✅ Separate client component file
import { loginAction } from '@/app/actions/auth'
import { useState } from 'react'
export default function LoginPage() {
const [error, setError] = useState<string | null>(null)
return (
<form action={async (formData) => {
const result = await loginAction(formData)
if (result?.error) {
setError(result.error)
}
}}>
{/* Form fields */}
</form>
)
}
📁 MANDATORY File Structure:
app/
├── actions/
│ └── auth.ts ✅ All auth server actions here
├── lib/
│ └── supabase/
│ ├── client.ts ✅ Browser client
│ ├── server.ts ✅ Server client
│ └── middleware.ts ✅ Middleware client
├── (auth)/
│ ├── login/
│ │ └── page.tsx ✅ Client component
│ ├── register/
│ │ └── page.tsx ✅ Client component
│ └── callback/
│ └── route.ts ✅ Route handler
└── middleware.ts ✅ Session refresh
🔒 RULES:
app/actions/auth.ts with 'use server''use client'async/await for all Supabase callsRequired Variables (.env.local):
# Supabase Core
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# Supabase Service Role (Server-side only - NEVER expose to client)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Application URLs
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_REDIRECT_URL=http://localhost:3000/auth/callback
# Email Configuration (Optional - for custom SMTP)
SUPABASE_SMTP_HOST=smtp.sendgrid.net
SUPABASE_SMTP_PORT=587
SUPABASE_SMTP_USER=apikey
SUPABASE_SMTP_PASS=your-sendgrid-api-key
[email protected]
SUPABASE_SMTP_SENDER_NAME=Your App Name
Production Variables:
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
NEXT_PUBLIC_REDIRECT_URL=https://yourdomain.com/auth/callback
Authentication Settings (Authentication > Settings)
https://yourdomain.comhttp://localhost:3000/auth/callback (development)https://yourdomain.com/auth/callback (production)Email Templates (Authentication > Email Templates)
templates/email-templates.md for examplesOAuth Providers (if using social auth)
Create Supabase client utilities:
// lib/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!
)
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } 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: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// Handle cookie setting in Server Component
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {
// Handle cookie removal in Server Component
}
},
},
}
)
}
// lib/supabase/middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(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: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({
name,
value,
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value,
...options,
})
},
remove(name: string, options: CookieOptions) {
request.cookies.set({
name,
value: '',
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value: '',
...options,
})
},
},
}
)
await supabase.auth.getUser()
return response
}
Middleware configuration:
// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
✅ REQUIRED IMPLEMENTATION:
// app/actions/auth.ts
'use server';
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function signUpAction(formData: FormData) {
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
options: {
data: {
full_name: formData.get('full_name') as string,
},
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
}
const { error } = await supabase.auth.signUp(data)
if (error) {
return { error: error.message }
}
redirect('/auth/confirm')
}
Sign Up Component (REQUIRED PATTERN):
// app/(auth)/register/page.tsx
'use client';
import { signUpAction } from '@/app/actions/auth'
import { useState } from 'react'
export default function SignUpPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-md">
<h1 className="text-2xl font-bold text-start mb-6">
Create Account
</h1>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-start">
{error}
</div>
)}
<form
action={async (formData) => {
setLoading(true)
const result = await signUpAction(formData)
setLoading(false)
if (result?.error) {
setError(result.error)
}
}}
className="space-y-4"
>
<div>
<label
htmlFor="full_name"
className="block text-start mb-2 font-medium"
>
Full Name
</label>
<input
id="full_name"
name="full_name"
type="text"
dir="auto"
className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="Enter your full name"
required
/>
</div>
<div>
<label
htmlFor="email"
className="block text-start mb-2 font-medium"
>
Email
</label>
<input
id="email"
name="email"
type="email"
dir="auto"
className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="Enter your email"
required
/>
</div>
<div>
<label
htmlFor="password"
className="block text-start mb-2 font-medium"
>
Password
</label>
<input
id="password"
name="password"
type="password"
dir="auto"
className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="Create a password"
required
minLength={8}
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Creating account...' : 'Sign Up'}
</button>
</form>
</div>
</div>
)
}
✅ REQUIRED IMPLEMENTATION:
// app/actions/auth.ts (add to existing file)
'use server';
export async function loginAction(formData: FormData) {
const supabase = await createClient()
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
const { error } = await supabase.auth.signInWithPassword(data)
if (error) {
return { error: error.message }
}
redirect('/dashboard')
}
Login Component (REQUIRED PATTERN):
// app/(auth)/login/page.tsx
'use client';
import { loginAction } from '@/app/actions/auth'
import { useState } from 'react'
import Link from 'next/link'
export default function LoginPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-md">
<h1 className="text-2xl font-bold text-start mb-6">
Sign In
</h1>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg mb-4 text-start">
{error}
</div>
)}
<form
action={async (formData) => {
setLoading(true)
const result = await loginAction(formData)
setLoading(false)
if (result?.error) {
setError(result.error)
}
}}
className="space-y-4"
>
<div>
<label
htmlFor="email"
className="block text-start mb-2 font-medium"
>
Email
</label>
<input
id="email"
name="email"
type="email"
dir="auto"
className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="Enter your email"
required
/>
</div>
<div>
<label
htmlFor="password"
className="block text-start mb-2 font-medium"
>
Password
</label>
<input
id="password"
name="password"
type="password"
dir="auto"
className="w-full ps-4 pe-4 py-3 text-start rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
placeholder="Enter your password"
required
minLength={8}
/>
</div>
<div className="flex items-center justify-between">
<Link
href="/auth/forgot-password"
className="text-sm text-blue-600 hover:underline"
>
Forgot password?
</Link>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
<p className="text-center text-sm text-gray-600">
Don't have an account?{' '}
<Link href="/register" className="text-blue-600 hover:underline">
Sign up
</Link>
</p>
</form>
</div>
</div>
)
}
// app/actions/auth.ts (add to existing file)
'use server';
export async function requestPasswordResetAction(formData: FormData) {
const supabase = await createClient()
const email = formData.get('email') as string
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
})
if (error) {
return { error: error.message }
}
return { success: true }
}
export async function resetPasswordAction(formData: FormData) {
const supabase = await createClient()
const password = formData.get('password') as string
const { error } = await supabase.auth.updateUser({
password: password,
})
if (error) {
return { error: error.message }
}
redirect('/login?message=Password updated successfully')
}
// app/auth/oauth/route.ts
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const supabase = await createClient()
const provider = request.nextUrl.searchParams.get('provider') as 'google' | 'github'
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
})
if (error) {
redirect('/login?error=Could not authenticate')
}
if (data.url) {
redirect(data.url)
}
}
OAuth Button Component:
<a href="/auth/oauth?provider=google">
Sign in with Google
</a>
<a href="/auth/oauth?provider=github">
Sign in with GitHub
</a>
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = await createClient()
await supabase.auth.exchangeCodeForSession(code)
}
// Redirect to dashboard or origin URL
return NextResponse.redirect(`${requestUrl.origin}/dashboard`)
}
// app/auth/logout/actions.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function logout() {
const supabase = await createClient()
await supabase.auth.signOut()
redirect('/login')
}
// Server Component
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function ProtectedPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return <div>Welcome, {user.email}!</div>
}
// Client Component
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
import type { User } from '@supabase/supabase-js'
export default function ClientComponent() {
const [user, setUser] = useState<User | null>(null)
const supabase = createClient()
useEffect(() => {
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
}
getUser()
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setUser(session?.user ?? null)
}
)
return () => subscription.unsubscribe()
}, [supabase])
return <div>{user ? `Logged in as ${user.email}` : 'Not logged in'}</div>
}
Enable RLS on all tables:
-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Users can only see their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
-- Users can only update their own profile
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
// Enforce strong passwords
const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character')
Configure in Supabase Dashboard:
Authentication > Settings > Rate Limits// Require email verification before access
const { data: { user } } = await supabase.auth.getUser()
if (user && !user.email_confirmed_at) {
redirect('/auth/verify-email')
}
// Use Supabase's built-in CSRF protection via cookies
// Already handled by @supabase/ssr package
// Comprehensive error handling
export async function handleAuthError(error: any) {
const errorMessages: Record<string, string> = {
'Invalid login credentials': 'Email or password is incorrect',
'Email not confirmed': 'Please verify your email address',
'User already registered': 'An account with this email already exists',
'Password should be at least 8 characters': 'Password must be at least 8 characters long',
}
return errorMessages[error.message] || 'An unexpected error occurred. Please try again.'
}
🚨 CRITICAL CHECKS - Must Pass All:
@supabase/ssr package (NOT deprecated auth-helpers)@supabase/supabase-js >= 2.39.0app/actions/auth.ts with 'use server''use client' (login, register)'use client' AND 'use server'block text-start classesdir="auto" attributeps-* pe-* (NOT px-*, pl-*, pr-*)✅ Functional Tests:
🔍 Code Quality Checks:
# Verify no deprecated packages
grep -r "@supabase/auth-helpers" package.json # Must be 0 results
# Verify correct package
grep -r "@supabase/ssr" package.json # Must find it
# Verify no mixed directives
grep -l "'use client'" app/actions/*.ts # Must be 0 results
grep -l "'use server'" app/\(auth\)/**/*.tsx # Must be 0 results
# Verify RTL classes
grep -r "px-" app/\(auth\) # Must be 0 results
grep -r "pl-" app/\(auth\) # Must be 0 results
grep -r "pr-" app/\(auth\) # Must be 0 results
grep -r "text-left" app/\(auth\) # Must be 0 results
See templates/email-templates.md for complete email template examples and customization guidelines.
Security Note: Always audit your RLS policies, keep Supabase packages updated, and never expose service role keys to client-side code.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience