Skip to main content

Overview

BudgetView uses Supabase Auth for complete authentication and authorization. All user sessions are managed via JWT tokens, with Row Level Security (RLS) enforcing data access at the database level.
Supabase Auth is built on top of the battle-tested GoTrue authentication server.

Authentication Methods

BudgetView supports two authentication methods:

Email & Password

Traditional credentials-based authentication with password validation

Google OAuth

Single sign-on using Google accounts for faster onboarding

Supabase Client Setup

The authentication client is initialized once and shared across the application:
lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!

export const supabase = createClient(supabaseUrl, supabaseKey)
The NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY is the anon key, not the service role key. It’s safe to expose on the client side.

Environment Variables

Required environment variables:
VariableDescriptionWhere to Find
NEXT_PUBLIC_SUPABASE_URLProject URLSupabase Dashboard → Settings → API → Project URL
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYPublic anon keySupabase Dashboard → Settings → API → anon public key
Example .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Login Flow

Email/Password Authentication

The login form implements client-side validation before submitting credentials:
1

User enters credentials

Email and password are validated against regex patterns
components/login-form.tsx
function validate(values: { email: string; password: string }) {
  const next: { email?: string; password?: string } = {}
  
  if (!values.email) {
    next.email = "El correo es obligatorio"
  } else {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!emailRegex.test(values.email)) {
      next.email = "Ingresa un correo con formato válido"
    }
  }
  
  if (!values.password) {
    next.password = "La contraseña es obligatoria"
  } else if (values.password.length < 8) {
    next.password = "La contraseña debe tener al menos 8 caracteres"
  } else if (/\s/.test(values.password)) {
    next.password = "La contraseña no puede contener espacios"
  }
  
  return next
}
2

Call Supabase Auth API

Submit credentials to Supabase
const { error } = await supabase.auth.signInWithPassword({ 
  email, 
  password 
})

if (error) {
  setAuthError(error.message)
} else {
  setAuthSuccess("Inicio de sesión exitoso")
  router.push("/dashboard")
}
3

Receive JWT token

Supabase returns access and refresh tokens stored in localStorage
4

Redirect to dashboard

User is redirected after 750ms delay for success message visibility
Password requirements:
  • Minimum 8 characters
  • No whitespace allowed
  • No maximum length

Google OAuth Flow

Google sign-in uses OAuth 2.0 protocol:
components/login-form.tsx
const handleGoogleSignIn = async () => {
  const redirectTo = typeof window !== "undefined" 
    ? `${window.location.origin}/dashboard` 
    : undefined
    
  const { error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: redirectTo ? { redirectTo } : undefined,
  })
  
  if (error) {
    setAuthError(error.message)
  } else {
    setAuthSuccess("Redirigiéndote a Google para continuar...")
  }
}
1

User clicks Google button

Application initiates OAuth flow
2

Redirect to Google

User authenticates with their Google account
3

Google redirects back

Callback URL: https://your-app.com/dashboard
4

Supabase creates session

User profile is automatically created in auth.users table
OAuth configuration in Supabase:
  1. Navigate to Authentication → Providers → Google
  2. Enable the provider
  3. Add Google OAuth Client ID and Secret
  4. Configure authorized redirect URIs

Session Management

Retrieving Current User

The current authenticated user is retrieved using:
const { data: { user }, error } = await supabase.auth.getUser()

if (error || !user) {
  // User not authenticated
  router.push("/login")
  return
}

// Use user.id for database operations
const userId = user.id

JWT Token Storage

Supabase automatically manages tokens in browser storage:
  • Access token: Short-lived (1 hour default), used for API requests
  • Refresh token: Long-lived (30 days default), used to obtain new access tokens
Token refresh is automatic:
// Supabase client handles token refresh automatically
// No manual intervention required

Session Persistence

Sessions persist across browser sessions using localStorage:
// Check if user is logged in on page load
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('User signed in:', session?.user.email)
  }
  
  if (event === 'SIGNED_OUT') {
    console.log('User signed out')
    router.push('/login')
  }
  
  if (event === 'TOKEN_REFRESHED') {
    console.log('Token was refreshed')
  }
})

Logout Implementation

Logging out clears the session and redirects to login:
const handleLogout = async () => {
  const { error } = await supabase.auth.signOut()
  
  if (error) {
    console.error('Error logging out:', error)
  } else {
    router.push('/login')
  }
}

Authorization with RLS

Authorization is enforced at the database level using Row Level Security policies.

How RLS Works

1

User makes database request

Client sends query with JWT in Authorization header
await supabase.from("transacciones").select("*")
2

PostgreSQL validates JWT

Database extracts auth.uid() from token
3

Apply RLS policies

Only rows where usuario_id = auth.uid() are returned
-- RLS Policy
CREATE POLICY "Users can view own transactions"
  ON transacciones FOR SELECT
  USING (auth.uid() = usuario_id);
4

Return filtered results

User only sees their own data, enforced at database layer

Example RLS Policy

Every table has similar policies:
-- billeteras table policies
CREATE POLICY "Enable read access for own wallets"
  ON billeteras FOR SELECT
  USING (auth.uid() = usuario_id);

CREATE POLICY "Enable insert for authenticated users"
  ON billeteras FOR INSERT
  WITH CHECK (auth.uid() = usuario_id);

CREATE POLICY "Enable update for own wallets"
  ON billeteras FOR UPDATE
  USING (auth.uid() = usuario_id)
  WITH CHECK (auth.uid() = usuario_id);

CREATE POLICY "Enable delete for own wallets"
  ON billeteras FOR DELETE
  USING (auth.uid() = usuario_id);
RLS policies are impossible to bypass from the client. Even if a user modifies the JavaScript code, the database will reject unauthorized queries.

Protecting Routes

Next.js pages check authentication status:
app/dashboard/page.tsx
"use client"

import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { supabase } from "@/lib/supabaseClient"

export default function DashboardPage() {
  const router = useRouter()
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    const checkAuth = async () => {
      const { data: { user } } = await supabase.auth.getUser()
      
      if (!user) {
        router.push("/login")
      } else {
        setLoading(false)
      }
    }
    
    checkAuth()
  }, [router])
  
  if (loading) {
    return <div>Loading...</div>
  }
  
  return <div>Dashboard content</div>
}

Password Reset Flow

Users can reset forgotten passwords:
// Send password reset email
const { error } = await supabase.auth.resetPasswordForEmail(
  email,
  {
    redirectTo: `${window.location.origin}/reset-password`
  }
)

if (error) {
  console.error('Error sending reset email:', error)
} else {
  alert('Check your email for the reset link')
}
1

User requests reset

Enters email address on /recuperar-contrasena page
2

Supabase sends email

Magic link sent to user’s email address
3

User clicks link

Redirected to /reset-password with token in URL
4

User sets new password

Form submission updates password:
const { error } = await supabase.auth.updateUser({
  password: newPassword
})

Email Verification

Supabase can require email verification before login:
// Sign up with email verification
const { data, error } = await supabase.auth.signUp({
  email,
  password,
  options: {
    emailRedirectTo: `${window.location.origin}/verify-email`
  }
})
Configuration: Supabase Dashboard → Authentication → Email → Confirm email

Security Best Practices

Use HTTPS Only

Never transmit credentials over unencrypted connections

Environment Variables

Store API keys in .env.local, never commit to Git

Token Expiration

Configure appropriate token lifetimes in Supabase settings

Rate Limiting

Supabase automatically rate limits authentication attempts

Error Handling

Always handle authentication errors gracefully:
const { error } = await supabase.auth.signInWithPassword({ email, password })

if (error) {
  switch (error.message) {
    case 'Invalid login credentials':
      setAuthError('Email o contraseña incorrectos')
      break
    case 'Email not confirmed':
      setAuthError('Por favor verifica tu correo electrónico')
      break
    default:
      setAuthError('Error al iniciar sesión. Intenta nuevamente.')
  }
}

Authentication Events

Listen to auth state changes globally:
supabase.auth.onAuthStateChange((event, session) => {
  switch (event) {
    case 'SIGNED_IN':
      console.log('User signed in')
      break
    case 'SIGNED_OUT':
      console.log('User signed out')
      break
    case 'TOKEN_REFRESHED':
      console.log('Token refreshed')
      break
    case 'USER_UPDATED':
      console.log('User metadata updated')
      break
    case 'PASSWORD_RECOVERY':
      console.log('Password recovery initiated')
      break
  }
})

Multi-Factor Authentication (MFA)

Supabase supports TOTP-based MFA (future enhancement):
// Enable MFA for user
const { data, error } = await supabase.auth.mfa.enroll({
  factorType: 'totp'
})

// Verify MFA code
const { error: verifyError } = await supabase.auth.mfa.verify({
  factorId: data.id,
  code: userInputCode
})
MFA is not currently implemented in BudgetView but can be added without database changes.

Testing Authentication

Manual Testing

  1. Navigate to /login
  2. Enter valid credentials
  3. Verify redirect to /dashboard
  4. Check browser localStorage for supabase.auth.token

Integration Testing

import { supabase } from '@/lib/supabaseClient'

// Test authentication
test('user can log in with valid credentials', async () => {
  const { data, error } = await supabase.auth.signInWithPassword({
    email: '[email protected]',
    password: 'ValidPassword123'
  })
  
  expect(error).toBeNull()
  expect(data.user).toBeDefined()
  expect(data.session).toBeDefined()
})

For database security policies, see the Database Schema page.

Build docs developers (and LLMs) love