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:
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:
Variable Description Where to Find NEXT_PUBLIC_SUPABASE_URLProject URL Supabase Dashboard → Settings → API → Project URL NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYPublic anon key Supabase 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:
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
}
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" )
}
Receive JWT token
Supabase returns access and refresh tokens stored in localStorage
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..." )
}
}
User clicks Google button
Application initiates OAuth flow
Redirect to Google
User authenticates with their Google account
Google redirects back
Callback URL: https://your-app.com/dashboard
Supabase creates session
User profile is automatically created in auth.users table
OAuth configuration in Supabase:
Navigate to Authentication → Providers → Google
Enable the provider
Add Google OAuth Client ID and Secret
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
User makes database request
Client sends query with JWT in Authorization header await supabase . from ( "transacciones" ). select ( "*" )
PostgreSQL validates JWT
Database extracts auth.uid() from token
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);
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:
"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' )
}
User requests reset
Enters email address on /recuperar-contrasena page
Supabase sends email
Magic link sent to user’s email address
User clicks link
Redirected to /reset-password with token in URL
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
Valid Login
Invalid Credentials
OAuth Flow
Navigate to /login
Enter valid credentials
Verify redirect to /dashboard
Check browser localStorage for supabase.auth.token
Enter incorrect password
Verify error message displays
Ensure user remains on login page
Click “Continúa con Google”
Authenticate with Google account
Verify callback to dashboard
Check user created in Supabase Auth dashboard
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 ()
})