Overview
Zenda uses Supabase Auth for secure authentication and role-based access control. Both mental health professionals and patients authenticate through the same system, with access permissions determined by user roles.
All API requests require authentication via Bearer token in the Authorization header.
Authentication Flow
Zenda implements a token-based authentication system:
User Sign-In
Users (professionals or patients) sign in through the Supabase authentication UI with their email and password.
Token Generation
Upon successful authentication, Supabase generates a JWT (JSON Web Token) that contains:
User ID
Email
Token expiration time
User metadata (role, profile info)
Token Storage
The frontend stores the authentication token securely and includes it in all API requests.
Token Validation
The backend validates the token on each request using the AuthGuard to ensure the user is authenticated.
Supabase Client Setup
Frontend Client
The frontend uses the Supabase JavaScript client for authentication:
import { createClient } from '@supabase/supabase-js' ;
export const supabaseClient = createClient (
process . env . NEXT_PUBLIC_SUPABASE_URL || "" ,
process . env . NEXT_PUBLIC_SUPABASE_API_KEY || ""
)
Required Environment Variables
NEXT_PUBLIC_SUPABASE_URL = https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_API_KEY = your-anon-public-key
Authentication Guard
The backend uses NestJS guards to protect API endpoints. All routes require authentication unless explicitly marked as public.
AuthGuard Implementation
server/src/common/guards/auth.guard.ts
import {
CanActivate ,
ExecutionContext ,
Injectable ,
UnauthorizedException
} from '@nestjs/common' ;
import { SupabaseService } from 'src/modules/supabase/supabase.service' ;
@ Injectable ()
export class AuthGuard implements CanActivate {
constructor ( private readonly supabaseService : SupabaseService ) {}
async canActivate ( context : ExecutionContext ) : Promise < boolean > {
const request = context . switchToHttp (). getRequest ();
// Extract Bearer token from Authorization header
const authHeader = request . headers [ 'authorization' ];
if ( ! authHeader || ! authHeader . startsWith ( 'Bearer ' )) {
throw new UnauthorizedException ( 'Token no proporcionado' );
}
const token = authHeader . split ( ' ' )[ 1 ];
// Validate token with Supabase
const { data , error } = await this . supabaseService
. getClient ()
. auth
. getUser ( token );
if ( error || ! data . user ) {
throw new UnauthorizedException ( 'Token inválido o expirado' );
}
// Attach user to request object
request . user = data . user ;
return true ;
}
}
Usage in Controllers
import { Controller , Get , UseGuards } from '@nestjs/common' ;
import { AuthGuard } from 'src/common/guards/auth.guard' ;
@ Controller ( 'reservations' )
@ UseGuards ( AuthGuard ) // Require authentication for all routes
export class ReservationsController {
@ Get ()
async findAll () {
// Only authenticated users can access this
return await this . reservationsService . findAll ();
}
}
Role-Based Access Control
Zenda implements role-based access with two primary user types:
Professional Role
Mental health professionals have access to:
Professional dashboard
Schedule configuration
Patient management
Reservation management (view all, create manual blocks, cancel)
Payment settings
Full appointment history
Patient Role
Patients have access to:
Booking interface
Their own reservations only
Payment processing
Personal appointment history
RolesGuard Implementation
For routes that require specific roles (e.g., only professionals can update settings):
server/src/common/guards/roles.guard.ts
@ Injectable ()
export class RolesGuard implements CanActivate {
async canActivate ( context : ExecutionContext ) : Promise < boolean > {
const request = context . switchToHttp (). getRequest ();
const user = request . user ;
// Check if user has professional role
const isProfessional = await this . checkProfessionalRole ( user . id );
if ( ! isProfessional ) {
throw new ForbiddenException ( 'Acceso denegado' );
}
return true ;
}
}
Protected Routes Example
@ Controller ( 'professional-settings' )
@ UseGuards ( AuthGuard )
export class ProfessionalSettingsController {
@ Get ()
async get () {
// Any authenticated user can view settings
return await this . professionalSettingsService . get ();
}
@ Patch ( ':id' )
@ UseGuards ( RolesGuard ) // Only professionals can update
async update (@ Param ( 'id' ) id : string , @ Body () dto : UpdateDto ) {
return await this . professionalSettingsService . update ( id , dto );
}
}
Making Authenticated API Requests
From the Frontend
import axios from 'axios' ;
import { supabaseClient } from './supabaseClient' ;
const axiosClient = axios . create ({
baseURL: process . env . NEXT_PUBLIC_API_URL || 'http://localhost:4000' ,
});
// Add authentication token to all requests
axiosClient . interceptors . request . use ( async ( config ) => {
const { data : { session } } = await supabaseClient . auth . getSession ();
if ( session ?. access_token ) {
config . headers . Authorization = `Bearer ${ session . access_token } ` ;
}
return config ;
});
export default axiosClient ;
Example API Call
import axiosClient from '@/lib/axiosClient' ;
// Token is automatically included
const response = await axiosClient . get ( '/api/reservations' );
const reservations = response . data ;
Token Expiration
Supabase tokens expire after a configurable period (default: 1 hour). The client library automatically:
Refreshes tokens before expiration
Handles token refresh failures
Prompts re-authentication when needed
Error Handling
Common authentication errors:
401 Unauthorized
{
"statusCode" : 401 ,
"message" : "Token no proporcionado" ,
"error" : "Unauthorized"
}
Cause : Missing or invalid Authorization header
Solution : Ensure the Bearer token is included in the request
401 Invalid Token
{
"statusCode" : 401 ,
"message" : "Token inválido o expirado" ,
"error" : "Unauthorized"
}
Cause : Token has expired or is malformed
Solution : Refresh the token or prompt the user to sign in again
403 Forbidden
{
"statusCode" : 403 ,
"message" : "Acceso denegado" ,
"error" : "Forbidden"
}
Cause : User lacks required role/permissions
Solution : Ensure the user has the appropriate role for the requested operation
Security Best Practices
Store Tokens Securely Never store tokens in localStorage if possible. Use secure, HTTP-only cookies or session storage.
Use HTTPS Always use HTTPS in production to prevent token interception.
Validate on Every Request Never trust client-side authentication state. Always validate tokens server-side.
Implement Token Refresh Handle token expiration gracefully with automatic refresh logic.
Supabase Integration Complete Supabase setup guide
API Introduction Learn about the REST API structure
API Authentication API-specific authentication details
Professional Dashboard Access control in the professional dashboard