A comprehensive guide and example project demonstrating how to create, manage, and secure JWT tokens with Next.js 15, TypeScript, and Supabase. This project shows various authentication patterns including token creation, validation, refresh, and secure storage.
- JWT Token Creation - Generate secure tokens with Supabase Auth
- Token Validation - Verify tokens on both client and server
- Token Refresh - Automatic token renewal
- Secure Storage - Browser and server-side token management
- Role-based Access - JWT claims for authorization
- Token Revocation - Secure logout and token invalidation
- Middleware Protection - Route protection with JWT verification
- Type Safety - Full TypeScript integration
- Error Handling - Comprehensive authentication error management
- Frontend: Next.js 15, React 19, TypeScript
- Backend: Next.js API Routes, Server Actions
- Authentication: Supabase Auth with JWT
- Database: Supabase PostgreSQL
- Styling: Tailwind CSS 4
- Deployment: Vercel-ready
Before running this project, make sure you have:
- Node.js 18+ installed
- A Supabase account and project
- Git installed
-
Clone the repository
git clone https://github.com/devpayoub/JWT-Token-Management-with-Supabase.git cd nextjs-supabase-api-guide -
Install dependencies
npm install
-
Set up environment variables
Create a
.env.localfile in the root directory:NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key JWT_SECRET=your_jwt_secret_key
To get these values:
- Go to your Supabase project dashboard
- Navigate to Settings β API
- Copy the "Project URL", "anon public" key, and "service_role" key
- Generate a JWT secret for additional security
-
Set up the database
In your Supabase dashboard, create a
Userstable with the following SQL:CREATE TABLE Users ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, email TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL, role TEXT DEFAULT 'user', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() );
-
Run the development server
npm run dev
-
Open your browser
Navigate to http://localhost:3000
src/
βββ app/
β βββ api/ # Next.js API Routes
β β βββ auth/ # Authentication endpoints
β β β βββ login/ # POST /api/auth/login
β β β βββ verify/ # POST /api/auth/verify
β β βββ protected/ # Protected API routes
β β βββ users/ # GET /api/protected/users
β βββ actions/ # Server Actions
β β βββ auth.ts # Authentication actions
β βββ lib/
β β βββ supabase.ts # Supabase client
β β βββ jwt.ts # JWT utilities
β βββ middleware.ts # Route protection
β βββ layout.tsx # Root layout
β βββ page.tsx # Main interface
β βββ globals.css # Global styles
// Login and get JWT token
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password'
})
// Access token is automatically stored in localStorage
const accessToken = data.session?.access_token
const refreshToken = data.session?.refresh_token// Verify JWT token on server
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const { data: { user }, error } = await supabase.auth.getUser(token)// src/app/api/protected/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { verifyToken } from '@/lib/jwt'
export async function GET(request: NextRequest) {
try {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ error: 'No token provided' },
{ status: 401 }
)
}
const user = await verifyToken(token)
if (!user) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
)
}
// Your protected logic here
return NextResponse.json({ message: 'Protected data' })
} catch (error) {
return NextResponse.json(
{ error: 'Authentication failed' },
{ status: 401 }
)
}
}// src/middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const {
data: { session },
} = await supabase.auth.getSession()
// Protect routes that require authentication
if (!session && req.nextUrl.pathname.startsWith('/protected')) {
return NextResponse.redirect(new URL('/login', req.url))
}
// Redirect authenticated users away from auth pages
if (session && (req.nextUrl.pathname === '/login' || req.nextUrl.pathname === '/register')) {
return NextResponse.redirect(new URL('/', req.url))
}
return res
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}// src/lib/auth-helpers.ts
import { supabase } from './supabase'
export const refreshToken = async () => {
const { data, error } = await supabase.auth.refreshSession()
if (error) {
// Handle refresh error
await supabase.auth.signOut()
return null
}
return data.session
}
// Set up automatic token refresh
supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'TOKEN_REFRESHED') {
// Token was automatically refreshed
console.log('Token refreshed successfully')
}
})// Server-side: Add custom claims to JWT
const { data, error } = await supabase.auth.admin.updateUserById(
userId,
{
user_metadata: {
role: 'admin',
permissions: ['read', 'write', 'delete']
}
}
)// Client-side: Access custom claims
const { data: { user } } = await supabase.auth.getUser()
const userRole = user?.user_metadata?.role
const permissions = user?.user_metadata?.permissions// src/app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { supabase } from '@/lib/supabase'
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json()
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
return NextResponse.json(
{ error: error.message },
{ status: 401 }
)
}
return NextResponse.json({
user: data.user,
session: data.session,
message: 'Login successful'
})
} catch (error) {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}// src/app/api/auth/verify/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { supabase } from '@/lib/supabase'
export async function POST(request: NextRequest) {
try {
const { token } = await request.json()
const { data: { user }, error } = await supabase.auth.getUser(token)
if (error || !user) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
)
}
return NextResponse.json({
user,
valid: true
})
} catch (error) {
return NextResponse.json(
{ error: 'Token verification failed' },
{ status: 500 }
)
}
}// src/app/actions/auth.ts
'use server'
import { supabase } from '@/lib/supabase'
import { revalidatePath } from 'next/cache'
export async function loginUser(formData: FormData) {
try {
const email = formData.get('email') as string
const password = formData.get('password') as string
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw new Error(error.message)
revalidatePath('/')
return { success: true, user: data.user }
} catch (error) {
return { success: false, error: error.message }
}
}
export async function logoutUser() {
try {
const { error } = await supabase.auth.signOut()
if (error) throw new Error(error.message)
revalidatePath('/')
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
}- JWT Token Validation - Server-side token verification
- Automatic Token Refresh - Seamless session management
- Secure Token Storage - HttpOnly cookies and secure localStorage
- Role-based Authorization - Custom JWT claims for permissions
- Token Revocation - Secure logout and session cleanup
- CSRF Protection - Built-in Supabase security
- Rate Limiting - Prevent brute force attacks
// Add custom claims to user metadata
const { data, error } = await supabase.auth.updateUser({
data: {
role: 'admin',
permissions: ['read', 'write', 'delete'],
organization: 'acme-corp'
}
})// Automatic token refresh before expiration
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed:', session?.access_token)
}
})
return () => subscription.unsubscribe()
}, [])// Check user role before allowing access
const checkUserRole = async (requiredRole: string) => {
const { data: { user } } = await supabase.auth.getUser()
const userRole = user?.user_metadata?.role
return userRole === requiredRole
}# Login and get token
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password"}'
# Use token for protected request
curl http://localhost:3000/api/protected/users \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Verify token
curl -X POST http://localhost:3000/api/auth/verify \
-H "Content-Type: application/json" \
-d '{"token":"YOUR_JWT_TOKEN"}'NEXT_PUBLIC_SUPABASE_URL=your_production_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_production_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_production_service_role_key
JWT_SECRET=your_production_jwt_secret- Use HTTPS in production
- Set secure cookie flags
- Implement rate limiting
- Monitor token usage
- Regular security audits
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Next.js - The React framework
- Supabase - The open source Firebase alternative
- Vercel - The platform for frontend developers
If you have any questions or need help with this project:
- Check the Issues page
- Create a new issue with a detailed description
- Include your environment details and error messages