Skip to main content

Supabase Integration

upLegal uses Supabase as its primary backend service for authentication, database management, and storage. This guide covers the complete integration setup and usage patterns.

Setup

Environment Variables

Configure the following environment variables in your .env.local file:
VITE_SUPABASE_URL=your_supabase_project_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
VITE_SUPABASE_SERVICE_ROLE_KEY=your_service_role_key  # Server-side only
Never expose the VITE_SUPABASE_SERVICE_ROLE_KEY in client-side code. This key bypasses Row Level Security (RLS) and should only be used in server environments.

Client Initialization

The Supabase client is initialized as a singleton to ensure consistent connection management:
// src/lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';
import type { Database } from '@/types/supabase';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

export const getSupabaseClient = () => {
  if (!supabaseUrl || !supabaseAnonKey) {
    throw new Error('Missing required environment variables');
  }

  const options = {
    auth: {
      persistSession: true,
      autoRefreshToken: true,
      detectSessionInUrl: true,
    },
  };

  return createClient<Database>(supabaseUrl, supabaseAnonKey, options);
};

// Default export for convenience
export const supabase = getSupabaseClient();

Authentication

Session Management

Supabase handles session persistence automatically:
import { supabase } from '@/lib/supabaseClient';

// Check current session
const { data: { session } } = await supabase.auth.getSession();

// Listen for auth state changes
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    console.log('User signed in:', session.user);
  }
  if (event === 'SIGNED_OUT') {
    console.log('User signed out');
  }
});

User Registration

Create new users with metadata:
const { data, error } = await supabase.auth.signUp({
  email: userEmail,
  password: userPassword,
  options: {
    data: {
      first_name: firstName,
      last_name: lastName,
      role: 'client', // or 'lawyer'
    },
  },
});
Generate passwordless login links:
// Server-side only (uses service role key)
const { data, error } = await supabase.auth.admin.generateLink({
  type: 'magiclink',
  email: userEmail,
  options: {
    redirectTo: `${appUrl}/dashboard/appointments`,
  },
});

if (data?.properties?.action_link) {
  const magicLink = data.properties.action_link;
  // Send magicLink via email
}

Database Operations

Profile Management

Create and update user profiles:
const payload = {
  id: userId,
  user_id: userId,
  email: normalizedEmail,
  first_name: firstName,
  last_name: lastName,
  display_name: displayName,
  role: 'client',
  rut: rut || null,
  pjud_verified: false,
  has_used_free_consultation: false,
  created_at: new Date().toISOString(),
  updated_at: new Date().toISOString(),
};

const { data, error } = await supabase
  .from('profiles')
  .upsert(payload, { onConflict: 'id' })
  .select()
  .single();

Booking Management

Query bookings with relations:
const { data: booking, error } = await supabase
  .from('bookings')
  .select(`
    *,
    lawyer:profiles!bookings_lawyer_id_fkey(
      user_id,
      first_name,
      last_name,
      specialties,
      profile_picture_url
    )
  `)
  .eq('id', bookingId)
  .single();

RPC Functions

Call secure database functions that bypass RLS:
const { data, error } = await supabase.rpc('create_payment_secure', {
  p_id: paymentId,
  p_amount: amount,
  p_user_id: userId,
  p_lawyer_id: lawyerId,
  p_status: 'pending',
  // ... other parameters
});

Server-Side Usage

Service Role Client

For server operations that require elevated permissions:
// server.mjs
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.VITE_SUPABASE_URL,
  process.env.VITE_SUPABASE_SERVICE_ROLE_KEY,
  {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
    },
  }
);

// Now you can bypass RLS
const { data } = await supabase
  .from('bookings')
  .update({ status: 'confirmed' })
  .eq('id', bookingId);

Admin Operations

Create users from server:
const { data: newUser, error } = await supabase.auth.admin.createUser({
  email: userEmail,
  password: tempPassword,
  email_confirm: true,
  user_metadata: {
    first_name: firstName,
    last_name: lastName,
    role: 'client',
    signup_method: 'booking',
  },
});

Storage

File Upload

Upload files to Supabase Storage:
const { data, error } = await supabase.storage
  .from('profile-pictures')
  .upload(`${userId}/${fileName}`, file, {
    cacheControl: '3600',
    upsert: true,
  });

if (data) {
  const publicUrl = supabase.storage
    .from('profile-pictures')
    .getPublicUrl(data.path).data.publicUrl;
}

Error Handling

Consistent error reporting pattern:
import { logger } from '@/utils/logger';

const reportError = (error: Error, context: string) => {
  logger.error(`[${context}] Error occurred`, error, {
    context,
    environment: process.env.NODE_ENV,
  });
};

try {
  const { data, error } = await supabase
    .from('profiles')
    .select('*')
    .eq('id', userId)
    .single();
    
  if (error) throw error;
} catch (error) {
  reportError(error, 'profile-fetch');
}

Best Practices

  • Use typed Database schema from @/types/supabase
  • Enable RLS policies on all tables
  • Use RPC functions for complex operations
  • Never expose service role key in frontend
  • Handle auth state changes globally
  • Validate data before database operations

Type Safety

Generate TypeScript types from your database schema:
npx supabase gen types typescript --project-id your-project-id > src/types/supabase.ts
Then import and use:
import type { Database } from '@/types/supabase';

type Profile = Database['public']['Tables']['profiles']['Row'];
type BookingInsert = Database['public']['Tables']['bookings']['Insert'];

Build docs developers (and LLMs) love