Overview
TripLoom uses Supabase Auth for user authentication. This guide covers:
Email/password signup and login
OAuth providers (Google, GitHub, etc.)
Session management with middleware
Protected routes
Client and server-side auth
Setup
Environment variables
Add your Supabase project credentials to .env.local:
NEXT_PUBLIC_SUPABASE_URL = https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY = your-anon-key
These values are available in your Supabase project settings under API .
Supabase client (browser)
The browser client is created using @supabase/ssr for automatic cookie-based session management:
/home/daytona/workspace/source/frontend/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 !
)
}
Use this in client components and API routes that run in the browser.
Supabase client (server)
For server components and API routes, use the server client with Next.js cookies:
/home/daytona/workspace/source/frontend/lib/supabase/server.ts
import { createServerClient } 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: {
getAll () {
return cookieStore . getAll ()
},
setAll ( cookiesToSet ) {
try {
cookiesToSet . forEach (({ name , value , options }) =>
cookieStore . set ( name , value , options )
)
} catch {
// setAll called from a Server Component — safe to ignore
}
},
},
}
)
}
Email/password authentication
Sign up
The signup page at /auth/signup collects email and password, then calls supabase.auth.signUp():
/home/daytona/workspace/source/frontend/app/auth/signup/page.tsx
async function handleSignup ( e : React . FormEvent ) {
e . preventDefault ()
setError ( null )
if ( password !== confirm ) {
setError ( "Passwords don't match." )
return
}
if ( password . length < 6 ) {
setError ( "Password must be at least 6 characters." )
return
}
setLoading ( true )
const supabase = createClient ()
const { error } = await supabase . auth . signUp ({
email ,
password ,
options: {
emailRedirectTo: ` ${ window . location . origin } /auth/callback` ,
},
})
if ( error ) {
setError ( error . message )
setLoading ( false )
return
}
setSuccess ( true )
setLoading ( false )
}
After signup, users receive a confirmation email. They must click the link to activate their account.
Sign in
The login page at /auth/login uses supabase.auth.signInWithPassword():
/home/daytona/workspace/source/frontend/app/auth/login/page.tsx
async function handleLogin ( e : React . FormEvent ) {
e . preventDefault ()
setLoading ( true )
setError ( null )
const supabase = createClient ()
const { error } = await supabase . auth . signInWithPassword ({ email , password })
if ( error ) {
setError ( error . message )
setLoading ( false )
return
}
router . push ( "/dashboard" )
router . refresh ()
}
On successful login, the user is redirected to /dashboard.
Auth callback
Supabase sends users to /auth/callback after email confirmation or OAuth login. This route exchanges the code for a session:
/home/daytona/workspace/source/frontend/app/auth/callback/route.ts
import { type NextRequest , NextResponse } from "next/server"
import { createServerClient } from "@supabase/ssr"
export async function GET ( request : NextRequest ) {
const { searchParams , origin } = new URL ( request . url )
const code = searchParams . get ( "code" )
const next = searchParams . get ( "next" ) ?? "/dashboard"
if ( code ) {
const supabaseResponse = NextResponse . redirect ( ` ${ origin }${ next } ` )
const supabase = createServerClient (
process . env . NEXT_PUBLIC_SUPABASE_URL ! ,
process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ! ,
{
cookies: {
getAll () {
return request . cookies . getAll ()
},
setAll ( cookiesToSet ) {
cookiesToSet . forEach (({ name , value , options }) =>
supabaseResponse . cookies . set ( name , value , options )
)
},
},
}
)
const { error } = await supabase . auth . exchangeCodeForSession ( code )
if ( ! error ) return supabaseResponse
}
// Return to login with error if something went wrong
return NextResponse . redirect ( ` ${ origin } /auth/login?error=auth_callback_failed` )
}
Middleware for protected routes
TripLoom uses Next.js middleware to protect /dashboard and /trips/* routes:
/home/daytona/workspace/source/frontend/middleware.ts
import { createServerClient } from "@supabase/ssr"
import { NextResponse , type NextRequest } from "next/server"
export async function middleware ( request : NextRequest ) {
let supabaseResponse = NextResponse . next ({ request })
const supabase = createServerClient (
process . env . NEXT_PUBLIC_SUPABASE_URL ! ,
process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ! ,
{
cookies: {
getAll () {
return request . cookies . getAll ()
},
setAll ( cookiesToSet ) {
cookiesToSet . forEach (({ name , value }) =>
request . cookies . set ( name , value )
)
supabaseResponse = NextResponse . next ({ request })
cookiesToSet . forEach (({ name , value , options }) =>
supabaseResponse . cookies . set ( name , value , options )
)
},
},
}
)
// Refresh session — required for SSR auth to work correctly
const {
data : { user },
} = await supabase . auth . getUser ()
const { pathname } = request . nextUrl
// Protect dashboard and trips — redirect to login if unauthenticated
const isProtected =
pathname . startsWith ( "/dashboard" ) || pathname . startsWith ( "/trips" )
if ( ! user && isProtected ) {
const url = request . nextUrl . clone ()
url . pathname = "/auth/login"
return NextResponse . redirect ( url )
}
// Redirect logged-in users away from auth pages
const isAuthPage =
pathname . startsWith ( "/auth/login" ) || pathname . startsWith ( "/auth/signup" )
if ( user && isAuthPage ) {
const url = request . nextUrl . clone ()
url . pathname = "/dashboard"
return NextResponse . redirect ( url )
}
return supabaseResponse
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|icon|apple-icon|opengraph-image|twitter-image|robots.txt|sitemap.xml|.* \\ .(?:svg|png|jpg|jpeg|gif|webp)$).*)" ,
],
}
How it works
Middleware runs on every request (except static files)
supabase.auth.getUser() checks for a valid session
If user is null and route is protected → redirect to /auth/login
If user exists and route is /auth/login or /auth/signup → redirect to /dashboard
OAuth providers (optional)
To add Google, GitHub, or other OAuth providers:
Enable the provider in Supabase dashboard under Authentication > Providers
Configure OAuth credentials (Client ID, Secret) from the provider
Add a sign-in button to your login page:
const handleOAuthLogin = async ( provider : 'google' | 'github' ) => {
const supabase = createClient ()
await supabase . auth . signInWithOAuth ({
provider ,
options: {
redirectTo: ` ${ window . location . origin } /auth/callback` ,
},
})
}
Supabase handles the OAuth flow and redirects back to /auth/callback.
Row Level Security (RLS)
TripLoom uses Supabase RLS to enforce data access policies. Example policy for the trips table:
-- Users can only read trips they are members of
CREATE POLICY "Users can view their own trips"
ON trips
FOR SELECT
USING (
auth . uid () IN (
SELECT user_id FROM trip_members WHERE trip_id = trips . id
)
);
-- Users can insert trips
CREATE POLICY "Users can create trips"
ON trips
FOR INSERT
WITH CHECK (true);
-- Users can update trips they own
CREATE POLICY "Users can update their own trips"
ON trips
FOR UPDATE
USING (
auth . uid () IN (
SELECT user_id FROM trip_members WHERE trip_id = trips . id AND role = 'owner'
)
);
Best practices
Use server client for SSR Always use the server client (lib/supabase/server.ts) in Server Components and API routes to access cookies correctly.
Refresh sessions in middleware Middleware calls getUser() to refresh the session on every request, ensuring users stay logged in.
Enable RLS on all tables Never disable RLS. Write explicit policies for each table to prevent unauthorized access.
Validate email confirmation Require email verification before granting access to protected features.
Troubleshooting
Session not persisting after login
Ensure middleware is configured correctly and getUser() is called. Check that cookies are not blocked by browser settings.
Email confirmation link not working
Verify emailRedirectTo matches your production domain. In Supabase dashboard, add the domain to Authentication > URL Configuration > Redirect URLs .
Check that the OAuth provider’s redirect URI is set to https://your-project.supabase.co/auth/v1/callback. Also verify the provider is enabled in Supabase dashboard.