Overview
Zenda uses Supabase as its primary backend service for authentication and data storage. The integration spans both client-side (Next.js) and server-side (NestJS) components.
Supabase provides PostgreSQL database, authentication, and real-time subscriptions all in one platform.
Architecture
Zenda maintains two separate Supabase client instances:
- Client-side - Used in the Next.js frontend for user authentication and direct database queries
- Server-side - Used in the NestJS backend for API operations and server-side data management
Client-Side Setup
The frontend uses a lightweight Supabase client for browser operations:
// lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';
export const supabaseClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || "",
process.env.NEXT_PUBLIC_SUPABASE_API_KEY || ""
)
The NEXT_PUBLIC_ prefix makes these environment variables accessible in the browser.
Client Environment Variables
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_API_KEY=your_anon_public_key
Server-Side Setup
The backend uses a NestJS service to manage the Supabase connection:
// server/src/modules/supabase/supabase.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createClient, SupabaseClient } from '@supabase/supabase-js';
@Injectable()
export class SupabaseService {
private readonly client: SupabaseClient;
constructor(private configService: ConfigService) {
this.client = createClient(
this.configService.get<string>('config.supabase_url')!,
this.configService.get<string>('config.supabase_api_key')!,
);
}
getClient(): SupabaseClient {
return this.client;
}
}
Server Environment Variables
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_API_KEY=your_service_role_key
Use the service role key on the server for elevated permissions. Never expose this key to the client.
Configuration Mapping
// server/src/config/configuration-enviroment.ts
import { registerAs } from '@nestjs/config';
export default registerAs('config', () => ({
supabase_url: process.env.SUPABASE_URL,
supabase_api_key: process.env.SUPABASE_API_KEY,
mp_access_token: process.env.TEST_MP_ACCESS_TOKEN,
server_url: process.env.SERVER_URL
}));
Database Schema
Zenda uses the following core tables in Supabase:
Profiles Table
Stores user profile information:
// server/shared/db/profile.ts
export type UserRole = "USER" | "ADMIN";
export interface ProfileTable {
id: string;
email: string;
full_name: string;
role: UserRole;
is_active: boolean;
birth_date: string;
is_profile_complete: boolean;
created_at: string;
}
Key Fields:
- id: User UUID (matches Supabase auth user ID)
- role: Either “USER” (client) or “ADMIN” (professional)
- is_profile_complete: Tracks onboarding completion status
Reservations Table
Stores appointment bookings:
// server/shared/db/reservation.ts
export interface ReservationTable {
id: string;
client_id: string;
professional_id: string;
start_time: string;
end_time: string;
status: string;
session_mode: string;
created_at: string;
}
Key Fields:
- client_id: References user who booked the appointment
- professional_id: References the professional providing the service
- start_time/end_time: ISO 8601 timestamp strings
- session_mode: Type of session (e.g., “VIRTUAL”, “IN_PERSON”)
Professional Settings Table
Stores configuration for professional users:
// server/shared/db/professional_settings.ts
export interface ProfessionalSettingsTable {
id: string;
user_id: string;
session_duration: number;
work_days: string;
work_start_time: string;
work_end_time: string;
reservation_window: number;
requires_deposit: boolean;
deposit_amount: number | null;
session_mode: string;
office_address: string | null;
created_at: string;
}
Key Fields:
- user_id: References the professional’s profile ID
- session_duration: Length of each session in minutes
- work_days: JSON string of available days
- requires_deposit: Whether clients must pay a deposit
- deposit_amount: Amount required for deposit (if applicable)
Payments Table
Stores payment records:
// server/shared/db/payment.ts
export type PaymentStatus = "PENDING" | "PAID" | "FAILED";
export interface PaymentProps {
id: string;
reservation_id: string;
amount: number;
status: PaymentStatus;
payment_provider: string;
external_payment_id?: string | null;
created_at: string;
}
Key Fields:
- reservation_id: Links payment to a specific reservation
- payment_provider: Payment gateway used (e.g., “Mercado Pago”)
- external_payment_id: Reference ID from the payment provider
Usage Examples
Server-Side Database Query
Example from the reservations service:
// server/src/modules/reservations/reservations.service.ts:29-38
async createReservationWithoutPayment(infoPayment) {
const newReservation = formattedReservation(infoPayment);
const responseReservation = await this.supabaseService
.getClient()
.from('reservations')
.insert(newReservation)
.select()
.single();
return responseReservation;
}
Querying with Filters
// server/src/modules/reservations/reservations.service.ts:101-112
async findAllByUser({ client_id }: { client_id: string }) {
const { data, error } = await this.supabaseService
.getClient()
.from('reservations')
.select('*')
.eq('client_id', client_id);
if (error) {
throw new Error(error.message);
}
return data;
}
Date Range Queries
Checking availability for a specific date:
// server/src/modules/reservations/reservations.service.ts:125-150
async getAvailability(date: string, professionalId: string) {
const day = date.split(" ")[0];
const nextDay = new Date(day);
nextDay.setDate(nextDay.getDate() + 1);
const nextDayStr = nextDay.toISOString().split("T")[0];
const response = await this.supabaseService
.getClient()
.from("reservations")
.select('*')
.eq('professional_id', professionalId)
.gte('start_time', `${day}T00:00:00`)
.lt('start_time', `${nextDayStr}T00:00:00`);
const finalSlots: string[] = []
response.data?.forEach((subDate: Reservation) => {
finalSlots.push(subDate.start_time);
})
return {
date: day,
timezone: "",
occupiedSlots: finalSlots,
};
}
Payment Record Creation
// server/src/modules/payments/payments.service.ts:13-18
async create(infoPayment) {
const response = await this.supabaseService
.getClient()
.from('payments')
.insert(infoPayment);
}
Authentication
Supabase handles all authentication flows including:
- Email/password authentication
- OAuth providers (if configured)
- Session management
- JWT token generation
Authentication is managed through Supabase’s built-in auth system. The application uses an AuthGuard to protect API endpoints.
Auth Guard
Zenda uses a custom auth guard to protect API routes:
// server/src/modules/reservations/reservations.controller.ts:10-11
@UseGuards(AuthGuard)
@Controller('reservations')
API Configuration
The frontend uses a centralized configuration for API endpoints:
// lib/serverConfig.ts
const enviroment = process.env;
const serverUrl = enviroment.NEXT_PUBLIC_SERVER_URL;
export const serverConfig = {
reservations: {
common: `${serverUrl}/reservations`,
fetchReservationsByUser: ({ client_id }: { client_id: string }) => {
return `${serverUrl}/reservations/user/${client_id}`
},
fetchReservationsByProfessional: ({ professionalId }: { professionalId: string }) => {
return `${serverUrl}/reservations/professional/${professionalId}`
},
getAvailability: ({ date, professional_id }: { date: string | undefined; professional_id: string; }) => {
return `${serverUrl}/reservations/availability?date=${date}&professional_id=${professional_id}`;
},
getPayment: `${serverUrl}/reservations/payment`,
create: `${serverUrl}/reservations/create-without-payment`,
findOne: ({ reservationId }: { reservationId: string }) => {
return `${serverUrl}/reservations/${reservationId}`
},
getByUsers: `${serverUrl}/reservations/by-users`
},
professionalSettings: {
get: `${serverUrl}/professional-settings`,
patch: ({ id }: { id: string }) => `${serverUrl}/professional-settings/${id}`,
},
profile: {
findOne: ({ userId }: { userId: string }) => {
return `${serverUrl}/profiles/${userId}`
},
findAll: `${serverUrl}/profiles`,
update: ({ userId }: { userId: string }) => {
return `${serverUrl}/profiles/${userId}`
}
}
}
Best Practices
Use Service Role Key on Server
Always use the service role key in the backend for elevated permissions. Never expose this key to the client.
Use Anon Key on Client
Use the anonymous (public) key in the frontend. Supabase’s Row Level Security (RLS) policies protect your data.
Error Handling
Always check for errors in Supabase responses and handle them appropriately.
Type Safety
Use TypeScript interfaces to maintain type safety across database operations.
lib/supabaseClient.ts - Client-side Supabase initialization
server/src/modules/supabase/supabase.service.ts - Server-side Supabase service
server/src/config/configuration-enviroment.ts - Environment configuration
server/shared/db/*.ts - Database table interfaces
lib/serverConfig.ts - API endpoint configuration