Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Jesus-Puertos/h-ayuntamiento/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The platform uses Supabase as the backend-as-a-service, providing:
  • PostgreSQL Database with Row-Level Security (RLS)
  • Authentication via OAuth providers (Google, Facebook) and email/password
  • Real-time Subscriptions for live data updates
  • Storage for user-generated content
All database interactions are centralized in src/lib/supabase.ts.

Client Setup

Configuration

// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL || '';
const supabaseAnonKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY || '';

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

Environment Variables

# .env
PUBLIC_SUPABASE_URL=https://xxx.supabase.co
PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
PUBLIC_ prefix means these variables are exposed to the client. Never use service role keys with this prefix.

Database Schema

user_profiles

Stores user information and authentication metadata.
ColumnTypeDescription
iduuidPrimary key (matches auth.users.id)
emailtextUser email address
full_nametextDisplay name
avatar_urltextProfile picture URL
providertextOAuth provider (google, facebook, email)
onboarding_completedbooleanWhether user completed tourism onboarding
created_attimestamptzAccount creation time
updated_attimestamptzLast profile update
TypeScript Type:
export interface UserProfile {
  id: string;
  email: string;
  full_name: string;
  avatar_url: string;
  provider: string;
  onboarding_completed: boolean;
  created_at: string;
  updated_at: string;
}

user_preferences

Stores tourism preferences collected during onboarding.
ColumnTypeDescription
iduuidPrimary key
user_iduuidForeign key to user_profiles
experienciatext[]Preferred experiences (aventura, cultura, naturaleza…)
duraciontextTrip duration preference (medio-dia, dia-completo…)
dificultadtextDifficulty level (facil, moderado, dificil, extremo)
grupotextTravel group type (solo, pareja, familia, amigos)
interesestext[]Specific interests (cascadas, miradores, artesanias…)
created_attimestamptzPreference creation time
TypeScript Type:
export interface UserPreferences {
  id?: string;
  user_id: string;
  experiencia: string[];  // ['aventura', 'naturaleza']
  duracion: string;       // 'dia-completo'
  dificultad: string;     // 'moderado'
  grupo: string;          // 'familia'
  intereses: string[];    // ['cascadas', 'miradores']
  created_at?: string;
}

user_routes

Stores generated personalized tourism routes.
ColumnTypeDescription
iduuidPrimary key
user_iduuidForeign key to user_profiles
user_nametextUser’s display name (denormalized)
route_nametextGenerated route name
atractivostext[]Array of attraction IDs/slugs
ticket_urltextURL to generated ticket image
share_codetextUnique share code (nanoid)
badgestext[]Earned badge IDs
created_attimestamptzRoute creation time
TypeScript Type:
export interface UserRoute {
  id: string;
  user_id: string;
  user_name?: string;
  route_name: string;
  atractivos: string[];    // ['cascada-texpico', 'mirador-zongolica']
  ticket_url: string;      // '/ruta/xyz123'
  share_code: string;      // 'xyz123'
  badges: string[];        // ['aventurero', 'explorador']
  created_at: string;
}

user_badges

Tracks unlocked achievements and badges.
ColumnTypeDescription
iduuidPrimary key
user_iduuidForeign key to user_profiles
badge_typetextBadge identifier (aventurero, explorador…)
unlocked_attimestamptzWhen badge was earned
TypeScript Type:
export interface UserBadge {
  id: string;
  user_id: string;
  badge_type: string;  // 'aventurero'
  unlocked_at: string;
}

user_favorites

Stores user’s saved attractions.
ColumnTypeDescription
iduuidPrimary key
user_iduuidForeign key to user_profiles
atractivo_slugtextAttraction slug
created_attimestamptzWhen favorited
TypeScript Type:
export interface UserFavorite {
  id: string;
  user_id: string;
  atractivo_slug: string;  // 'cascada-texpico'
  created_at: string;
}

Authentication Functions

Get Current User

export async function getCurrentUser() {
  const { data: { user } } = await supabase.auth.getUser();
  return user;
}
Usage:
const user = await getCurrentUser();
if (!user) {
  // Redirect to login
}

Email/Password Authentication

// Sign in
export async function signInWithEmail(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password
  });
  return { data, error };
}

// Sign up
export async function signUpWithEmail(
  email: string,
  password: string,
  name?: string
) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: {
        name: name || email.split('@')[0]
      }
    }
  });
  return { data, error };
}

OAuth with Google

export async function signInWithGoogle(
  redirectPath = '/turismo?onboarding=1'
) {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: `${window.location.origin}/auth/callback?return_url=${encodeURIComponent(redirectPath)}`
    }
  });
  return { data, error };
}
Flow:
  1. User clicks “Sign in with Google”
  2. Redirects to Google OAuth
  3. User authorizes
  4. Redirects to /auth/callback
  5. Callback page sets session
  6. Redirects to return_url
See Authentication for full implementation.

Sign Out

export async function signOut() {
  const { error } = await supabase.auth.signOut();
  return { error };
}

Profile Functions

Get User Profile

export async function getUserProfile(userId: string) {
  const { data, error } = await supabase
    .from('user_profiles')
    .select('*')
    .eq('id', userId)
    .single();
  return { data: data as UserProfile | null, error };
}

Update Profile

export async function upsertUserProfile(
  profile: Partial<UserProfile> & { id: string }
) {
  const { data, error } = await supabase
    .from('user_profiles')
    .upsert([{ ...profile, updated_at: new Date().toISOString() }])
    .select()
    .single();
  return { data, error };
}

Mark Onboarding Complete

export async function markOnboardingCompleted(userId: string) {
  const { data, error } = await supabase
    .from('user_profiles')
    .update({
      onboarding_completed: true,
      updated_at: new Date().toISOString()
    })
    .eq('id', userId)
    .select()
    .single();
  return { data, error };
}

Check Onboarding Status

export async function isOnboardingCompleted(userId: string): Promise<boolean> {
  const { data } = await supabase
    .from('user_profiles')
    .select('onboarding_completed')
    .eq('id', userId)
    .single();
  return data?.onboarding_completed ?? false;
}

Get Full Profile

export async function getFullProfile(userId: string) {
  const [profileRes, prefsRes, badgesRes, routesRes, favoritesRes] = await Promise.all([
    getUserProfile(userId),
    getUserPreferences(userId),
    getUserBadges(userId),
    getUserRoutes(userId),
    getUserFavorites(userId),
  ]);
  return {
    profile: profileRes.data,
    preferences: prefsRes.data,
    badges: badgesRes.data ?? [],
    routes: routesRes.data ?? [],
    favorites: favoritesRes.data ?? [],
  };
}

Preferences Functions

Save Preferences

export async function saveUserPreferences(preferences: UserPreferences) {
  const { data, error } = await supabase
    .from('user_preferences')
    .upsert([preferences], { onConflict: 'user_id' })  // Update if exists
    .select()
    .single();
  return { data, error };
}

Get Preferences

export async function getUserPreferences(userId: string) {
  const { data, error } = await supabase
    .from('user_preferences')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: false })
    .limit(1)
    .single();
  return { data, error };
}
Usage Example:
const user = await getCurrentUser();
const { data: prefs } = await getUserPreferences(user.id);

if (prefs?.experiencia.includes('aventura')) {
  // Show adventure content
}

Route Functions

Save Route

export async function saveUserRoute(
  route: Omit<UserRoute, 'id' | 'created_at'>
) {
  const { data, error } = await supabase
    .from('user_routes')
    .insert([route])
    .select()
    .single();
  return { data, error };
}
Usage:
import { nanoid } from 'nanoid';
import { generateRouteName } from '@/lib/recommendations';

const shareCode = nanoid(10);
const routeName = generateRouteName(preferences);

await saveUserRoute({
  user_id: user.id,
  user_name: user.full_name,
  route_name: routeName,
  atractivos: ['cascada-texpico', 'mirador-zongolica'],
  ticket_url: `/ruta/${shareCode}`,
  share_code: shareCode,
  badges: ['aventurero']
});

Get Route by Share Code

export async function getUserRoute(shareCode: string) {
  const { data, error } = await supabase
    .from('user_routes')
    .select('*, user:user_id(*)')  // Join with user profile
    .eq('share_code', shareCode)
    .single();
  return { data, error };
}

Get All User Routes

export async function getUserRoutes(userId: string) {
  const { data, error } = await supabase
    .from('user_routes')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: false });
  return { data, error };
}

Get Latest Route

export async function getLatestUserRoute(userId: string) {
  const { data, error } = await supabase
    .from('user_routes')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: false })
    .limit(1)
    .single();
  return data;
}

Badge Functions

Unlock Badge

export async function unlockBadge(userId: string, badgeType: string) {
  const { data, error } = await supabase
    .from('user_badges')
    .insert([{ user_id: userId, badge_type: badgeType }])
    .select()
    .single();
  return { data, error };
}

Get User Badges

export async function getUserBadges(userId: string) {
  const { data, error } = await supabase
    .from('user_badges')
    .select('*')
    .eq('user_id', userId)
    .order('unlocked_at', { ascending: false });
  return { data, error };
}

Check Badge Ownership

export async function hasBadge(
  userId: string,
  badgeType: string
): Promise<boolean> {
  const { data } = await supabase
    .from('user_badges')
    .select('id')
    .eq('user_id', userId)
    .eq('badge_type', badgeType)
    .single();
  return !!data;
}
Usage:
if (await hasBadge(user.id, 'aventurero')) {
  // Show special content
}

Favorites Functions

Add Favorite

export async function addFavorite(userId: string, atractivoSlug: string) {
  const { data, error } = await supabase
    .from('user_favorites')
    .insert([{ user_id: userId, atractivo_slug: atractivoSlug }])
    .select()
    .single();
  return { data, error };
}

Remove Favorite

export async function removeFavorite(userId: string, atractivoSlug: string) {
  const { error } = await supabase
    .from('user_favorites')
    .delete()
    .eq('user_id', userId)
    .eq('atractivo_slug', atractivoSlug);
  return { error };
}

Toggle Favorite

export async function toggleFavorite(
  userId: string,
  atractivoSlug: string
): Promise<boolean> {
  const isFav = await isFavorite(userId, atractivoSlug);
  if (isFav) {
    await removeFavorite(userId, atractivoSlug);
    return false;
  } else {
    await addFavorite(userId, atractivoSlug);
    return true;
  }
}

Check Favorite Status

export async function isFavorite(
  userId: string,
  atractivoSlug: string
): Promise<boolean> {
  const { data } = await supabase
    .from('user_favorites')
    .select('id')
    .eq('user_id', userId)
    .eq('atractivo_slug', atractivoSlug)
    .single();
  return !!data;
}

Get All Favorites

export async function getUserFavorites(userId: string) {
  const { data, error } = await supabase
    .from('user_favorites')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: false });
  return { data: data as UserFavorite[] | null, error };
}

Get Favorites Count

export async function getFavoritesCount(userId: string): Promise<number> {
  const { count } = await supabase
    .from('user_favorites')
    .select('*', { count: 'exact', head: true })
    .eq('user_id', userId);
  return count ?? 0;
}

Real-time Subscriptions

Listen to Auth Changes

const { data: { subscription } } = 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');
    }
  }
);

// Cleanup
subscription.unsubscribe();

Listen to Database Changes

const channel = supabase
  .channel('user_routes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'user_routes',
      filter: `user_id=eq.${userId}`
    },
    (payload) => {
      console.log('New route created:', payload.new);
    }
  )
  .subscribe();

// Cleanup
channel.unsubscribe();

Example: Complete Onboarding Flow

import {
  getCurrentUser,
  saveUserPreferences,
  saveUserRoute,
  unlockBadge,
  markOnboardingCompleted
} from '@/lib/supabase';
import { generateRouteName } from '@/lib/recommendations';
import { nanoid } from 'nanoid';

async function completeOnboarding(preferences: UserPreferences) {
  const user = await getCurrentUser();
  if (!user) throw new Error('Not authenticated');

  // 1. Save preferences
  await saveUserPreferences({
    user_id: user.id,
    ...preferences
  });

  // 2. Generate personalized route
  const routeName = generateRouteName(preferences);
  const shareCode = nanoid(10);
  const atractivos = selectAtractivos(preferences);  // Your logic

  await saveUserRoute({
    user_id: user.id,
    user_name: user.user_metadata.name,
    route_name: routeName,
    atractivos,
    ticket_url: `/ruta/${shareCode}`,
    share_code: shareCode,
    badges: ['explorador']
  });

  // 3. Unlock onboarding badge
  await unlockBadge(user.id, 'explorador');

  // 4. Mark onboarding complete
  await markOnboardingCompleted(user.id);

  return shareCode;
}

Row-Level Security (RLS)

All tables are protected with RLS policies:
-- Users can only read/write their own data
CREATE POLICY "Users can view own profile"
ON user_profiles FOR SELECT
USING (auth.uid() = id);

CREATE POLICY "Users can update own profile"
ON user_profiles FOR UPDATE
USING (auth.uid() = id);

-- Public can view shared routes
CREATE POLICY "Public can view routes by share code"
ON user_routes FOR SELECT
USING (true);

Error Handling

const { data, error } = await saveUserRoute(route);

if (error) {
  console.error('Failed to save route:', error.message);
  // Show user-friendly error
  toast.error('No se pudo guardar la ruta. Inténtalo de nuevo.');
  return;
}

// Success
toast.success('¡Ruta guardada!');

Next Steps

Authentication

Implement authentication flows with AuthButtons

Tourism Components

TurismoOnboarding and route generation

Architecture

Understanding the data flow

Tech Stack

All technologies powering the platform