Skip to main content

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:
  1. Client-side - Used in the Next.js frontend for user authentication and direct database queries
  2. 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

1

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.
2

Use Anon Key on Client

Use the anonymous (public) key in the frontend. Supabase’s Row Level Security (RLS) policies protect your data.
3

Error Handling

Always check for errors in Supabase responses and handle them appropriately.
4

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

Build docs developers (and LLMs) love