Skip to main content

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

  1. Middleware runs on every request (except static files)
  2. supabase.auth.getUser() checks for a valid session
  3. If user is null and route is protected → redirect to /auth/login
  4. 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:
  1. Enable the provider in Supabase dashboard under Authentication > Providers
  2. Configure OAuth credentials (Client ID, Secret) from the provider
  3. 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

Ensure middleware is configured correctly and getUser() is called. Check that cookies are not blocked by browser settings.
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.

Build docs developers (and LLMs) love