🗄️ TutorConnect UC - Base de Datos Supabase

🎯 Características Clave

  • Supabase Auth: Manejo de autenticación con email @uc.cl
  • Row Level Security (RLS): Políticas de seguridad a nivel de fila
  • PostgreSQL nativo: Sin ORM, queries directas
  • Realtime: Subscripciones en tiempo real (opcional para chat)
  • Storage: Para fotos de perfil de tutores
  • Edge Functions: Para integración con Gemini AI

📊 Esquema de Base de Datos

🔑 Tipos ENUM

-- Roles de usuario
CREATE TYPE user_role AS ENUM ('student', 'tutor', 'both');
 
-- Modalidad de tutoría
CREATE TYPE tutoria_modalidad AS ENUM ('presencial', 'online');
 
-- Estado de tutoría
CREATE TYPE tutoria_estado AS ENUM ('pendiente', 'confirmada', 'completada', 'cancelada');

👥 Tabla: profiles

Descripción: Perfiles extendidos de usuarios (complementa auth.users de Supabase)

CREATE TABLE profiles (
  id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
  full_name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL CHECK (email LIKE '%@uc.cl'),
  role user_role NOT NULL DEFAULT 'student',
  avatar_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Índices
CREATE INDEX idx_profiles_email ON profiles(email);
CREATE INDEX idx_profiles_role ON profiles(role);
 
-- RLS Policies
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
 
-- Todos pueden ver perfiles públicos
CREATE POLICY "Profiles son públicos para lectura"
  ON profiles FOR SELECT
  USING (true);
 
-- Solo el dueño puede actualizar su perfil
CREATE POLICY "Users pueden actualizar su propio perfil"
  ON profiles FOR UPDATE
  USING (auth.uid() = id);
 
-- Solo el dueño puede insertar su perfil (al registrarse)
CREATE POLICY "Users pueden crear su propio perfil"
  ON profiles FOR INSERT
  WITH CHECK (auth.uid() = id);

🎓 Tabla: tutores

Descripción: Información adicional de tutores verificados UC

CREATE TABLE tutores (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES profiles(id) ON DELETE CASCADE UNIQUE NOT NULL,
  bio TEXT,
  campus TEXT NOT NULL CHECK (campus IN ('San Joaquín', 'Casa Central', 'Lo Contador', 'Villarrica')),
  carrera TEXT NOT NULL,
  año_ingreso INTEGER NOT NULL CHECK (año_ingreso >= 2000 AND año_ingreso <= EXTRACT(YEAR FROM NOW())),
  tarifa INTEGER NOT NULL CHECK (tarifa >= 0),
  disponibilidad JSONB DEFAULT '{}',
  auto_accept BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Índices para búsquedas frecuentes
CREATE INDEX idx_tutores_campus ON tutores(campus);
CREATE INDEX idx_tutores_tarifa ON tutores(tarifa);
CREATE INDEX idx_tutores_user_id ON tutores(user_id);
 
-- RLS Policies
ALTER TABLE tutores ENABLE ROW LEVEL SECURITY;
 
-- Todos pueden ver tutores
CREATE POLICY "Tutores son públicos"
  ON tutores FOR SELECT
  USING (true);
 
-- Solo el dueño puede crear/actualizar su perfil de tutor
CREATE POLICY "Users pueden gestionar su perfil de tutor"
  ON tutores FOR ALL
  USING (auth.uid() = user_id);
 
-- Comentarios
COMMENT ON TABLE tutores IS 'Perfil de tutores verificados UC';
COMMENT ON COLUMN tutores.user_id IS 'FK: profiles (usuario que es tutor)';
COMMENT ON COLUMN tutores.bio IS 'Descripción personal del tutor';
COMMENT ON COLUMN tutores.campus IS 'Campus principal del tutor';
COMMENT ON COLUMN tutores.carrera IS 'Carrera que cursa el tutor';
COMMENT ON COLUMN tutores.año_ingreso IS 'Año de ingreso a la UC';
COMMENT ON COLUMN tutores.tarifa IS 'Precio por módulo de tutoría (CLP)';
COMMENT ON COLUMN tutores.disponibilidad IS 'JSON: {"lunes": ["15:00-17:00"], "miercoles": ["10:00-12:00"]}';
COMMENT ON COLUMN tutores.auto_accept IS 'Auto-aceptar solicitudes de tutoría';

📚 Tabla: ramos

Descripción: Catálogo de ramos UC (cargado desde courses.ndjson)

CREATE TABLE ramos (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  codigo TEXT UNIQUE NOT NULL,
  nombre TEXT NOT NULL,
  descripcion TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Índices
CREATE UNIQUE INDEX idx_ramo_codigo ON ramos(codigo);
CREATE INDEX idx_ramo_nombre ON ramos(nombre);
 
-- RLS Policies
ALTER TABLE ramos ENABLE ROW LEVEL SECURITY;
 
-- Ramos son públicos (solo lectura para todos)
CREATE POLICY "Ramos son públicos"
  ON ramos FOR SELECT
  USING (true);
 
-- Comentarios
COMMENT ON TABLE ramos IS 'Catálogo de ramos UC (generado desde courses.ndjson)';
COMMENT ON COLUMN ramos.codigo IS 'Código del ramo: IIC2233, MAT1610, etc.';
COMMENT ON COLUMN ramos.nombre IS 'Nombre: Programación Avanzada, Cálculo II, etc.';

📖 Tabla: tutor_ramo

Descripción: Relación Many-to-Many entre tutores y ramos que pueden enseñar

CREATE TABLE tutor_ramo (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  tutor_id UUID REFERENCES tutores(id) ON DELETE CASCADE NOT NULL,
  ramo_id UUID REFERENCES ramos(id) ON DELETE CASCADE NOT NULL,
  nota DECIMAL(2,1) NOT NULL CHECK (nota >= 1.0 AND nota <= 7.0),
  semestre TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(tutor_id, ramo_id)
);
 
-- Índices
CREATE UNIQUE INDEX idx_tutor_ramo_unique ON tutor_ramo(tutor_id, ramo_id);
CREATE INDEX idx_tutor_ramo_tutor ON tutor_ramo(tutor_id);
CREATE INDEX idx_tutor_ramo_ramo ON tutor_ramo(ramo_id);
CREATE INDEX idx_tutor_ramo_nota ON tutor_ramo(nota DESC);
 
-- RLS Policies
ALTER TABLE tutor_ramo ENABLE ROW LEVEL SECURITY;
 
-- Todos pueden ver qué ramos enseña cada tutor
CREATE POLICY "Tutor-Ramo es público"
  ON tutor_ramo FOR SELECT
  USING (true);
 
-- Solo el tutor puede agregar/editar sus ramos
CREATE POLICY "Tutores gestionan sus propios ramos"
  ON tutor_ramo FOR ALL
  USING (
    EXISTS (
      SELECT 1 FROM tutores 
      WHERE tutores.id = tutor_ramo.tutor_id 
      AND tutores.user_id = auth.uid()
    )
  );
 
-- Comentarios
COMMENT ON TABLE tutor_ramo IS 'Ramos que puede enseñar cada tutor con su nota de aprobación';
COMMENT ON COLUMN tutor_ramo.nota IS 'Nota con la que aprobó (1.0 - 7.0)';
COMMENT ON COLUMN tutor_ramo.semestre IS 'Formato: 2023-1, 2023-2, 2024-1, etc.';

💼 Tabla: tutoring_offers

Descripción: Ofertas de tutorías publicadas por tutores

CREATE TABLE tutoring_offers (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  tutor_id UUID REFERENCES tutores(id) ON DELETE CASCADE NOT NULL,
  ramo_id UUID REFERENCES ramos(id) ON DELETE CASCADE NOT NULL,
  title TEXT NOT NULL,
  description TEXT,
  modality tutoria_modalidad[] DEFAULT ARRAY['presencial']::tutoria_modalidad[],
  price_per_session INTEGER NOT NULL CHECK (price_per_session >= 0),
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Índices
CREATE INDEX idx_offers_tutor ON tutoring_offers(tutor_id);
CREATE INDEX idx_offers_ramo ON tutoring_offers(ramo_id);
CREATE INDEX idx_offers_active ON tutoring_offers(is_active) WHERE is_active = true;
 
-- RLS Policies
ALTER TABLE tutoring_offers ENABLE ROW LEVEL SECURITY;
 
-- Todos pueden ver ofertas activas
CREATE POLICY "Ofertas activas son públicas"
  ON tutoring_offers FOR SELECT
  USING (is_active = true);
 
-- Tutores pueden gestionar sus propias ofertas
CREATE POLICY "Tutores gestionan sus ofertas"
  ON tutoring_offers FOR ALL
  USING (
    EXISTS (
      SELECT 1 FROM tutores 
      WHERE tutores.id = tutoring_offers.tutor_id 
      AND tutores.user_id = auth.uid()
    )
  );
 
-- Comentarios
COMMENT ON TABLE tutoring_offers IS 'Ofertas de tutorías publicadas por tutores';
COMMENT ON COLUMN tutoring_offers.modality IS 'Array de modalidades disponibles';

🔔 Tabla: tutoring_requests

Descripción: Búsquedas de tutorías publicadas por estudiantes

CREATE TABLE tutoring_requests (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  student_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
  ramo_id UUID REFERENCES ramos(id) ON DELETE CASCADE NOT NULL,
  title TEXT NOT NULL,
  description TEXT,
  urgency TEXT CHECK (urgency IN ('high', 'medium', 'low')) DEFAULT 'medium',
  budget INTEGER CHECK (budget >= 0),
  preferred_campus TEXT,
  accepts_online BOOLEAN DEFAULT true,
  status TEXT DEFAULT 'open' CHECK (status IN ('open', 'closed')),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Índices
CREATE INDEX idx_requests_student ON tutoring_requests(student_id);
CREATE INDEX idx_requests_ramo ON tutoring_requests(ramo_id);
CREATE INDEX idx_requests_status ON tutoring_requests(status) WHERE status = 'open';
CREATE INDEX idx_requests_urgency ON tutoring_requests(urgency);
 
-- RLS Policies
ALTER TABLE tutoring_requests ENABLE ROW LEVEL SECURITY;
 
-- Todos pueden ver búsquedas abiertas
CREATE POLICY "Búsquedas abiertas son públicas"
  ON tutoring_requests FOR SELECT
  USING (status = 'open');
 
-- Estudiantes gestionan sus propias búsquedas
CREATE POLICY "Estudiantes gestionan sus búsquedas"
  ON tutoring_requests FOR ALL
  USING (auth.uid() = student_id);
 
-- Comentarios
COMMENT ON TABLE tutoring_requests IS 'Búsquedas de tutorías publicadas por estudiantes';
COMMENT ON COLUMN tutoring_requests.urgency IS 'high: <3 días, medium: 3-7 días, low: >7 días';

📅 Tabla: tutoring_sessions

Descripción: Sesiones de tutoría agendadas

CREATE TABLE tutoring_sessions (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  tutor_id UUID REFERENCES tutores(id) ON DELETE CASCADE NOT NULL,
  student_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
  ramo_id UUID REFERENCES ramos(id) ON DELETE CASCADE NOT NULL,
  modalidad tutoria_modalidad NOT NULL DEFAULT 'presencial',
  ubicacion TEXT,
  fecha_hora TIMESTAMPTZ NOT NULL,
  duracion_modulos DECIMAL NOT NULL DEFAULT 1 CHECK (duracion_modulos > 0),
  estado tutoria_estado NOT NULL DEFAULT 'pendiente',
  precio_total INTEGER NOT NULL CHECK (precio_total >= 0),
  notas TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- Índices
CREATE INDEX idx_sessions_tutor ON tutoring_sessions(tutor_id);
CREATE INDEX idx_sessions_student ON tutoring_sessions(student_id);
CREATE INDEX idx_sessions_estado ON tutoring_sessions(estado);
CREATE INDEX idx_sessions_fecha ON tutoring_sessions(fecha_hora);
 
-- RLS Policies
ALTER TABLE tutoring_sessions ENABLE ROW LEVEL SECURITY;
 
-- Solo participantes pueden ver la sesión
CREATE POLICY "Solo participantes ven sesiones"
  ON tutoring_sessions FOR SELECT
  USING (
    auth.uid() = student_id 
    OR EXISTS (
      SELECT 1 FROM tutores 
      WHERE tutores.id = tutoring_sessions.tutor_id 
      AND tutores.user_id = auth.uid()
    )
  );
 
-- Estudiante puede crear/cancelar
CREATE POLICY "Estudiantes crean y cancelan sesiones"
  ON tutoring_sessions FOR INSERT
  WITH CHECK (auth.uid() = student_id);
 
CREATE POLICY "Estudiantes actualizan sus sesiones"
  ON tutoring_sessions FOR UPDATE
  USING (auth.uid() = student_id);
 
-- Tutor puede confirmar/completar/cancelar
CREATE POLICY "Tutores actualizan sesiones"
  ON tutoring_sessions FOR UPDATE
  USING (
    EXISTS (
      SELECT 1 FROM tutores 
      WHERE tutores.id = tutoring_sessions.tutor_id 
      AND tutores.user_id = auth.uid()
    )
  );
 
-- Comentarios
COMMENT ON TABLE tutoring_sessions IS 'Registro de sesiones de tutoría agendadas';
COMMENT ON COLUMN tutoring_sessions.ubicacion IS 'Sala/edificio si presencial, o link Meet/Zoom si online';
COMMENT ON COLUMN tutoring_sessions.duracion_modulos IS 'Duración en módulos UC (1 módulo = 80 min)';
COMMENT ON COLUMN tutoring_sessions.precio_total IS 'Precio total de la sesión (CLP)';

⭐ Tabla: reviews

Descripción: Reseñas de tutorías completadas

CREATE TABLE reviews (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  session_id UUID REFERENCES tutoring_sessions(id) ON DELETE CASCADE,
  tutor_id UUID REFERENCES tutores(id) ON DELETE CASCADE NOT NULL,
  student_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
  ramo_id UUID REFERENCES ramos(id) ON DELETE CASCADE NOT NULL,
  rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
  comment TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(session_id)
);
 
-- Índices
CREATE INDEX idx_reviews_tutor ON reviews(tutor_id);
CREATE INDEX idx_reviews_rating ON reviews(rating DESC);
CREATE INDEX idx_reviews_session ON reviews(session_id);
 
-- RLS Policies
ALTER TABLE reviews ENABLE ROW LEVEL SECURITY;
 
-- Todos pueden leer reviews
CREATE POLICY "Reviews son públicas"
  ON reviews FOR SELECT
  USING (true);
 
-- Solo el estudiante de la sesión puede crear review
CREATE POLICY "Estudiantes crean reviews de sus sesiones"
  ON reviews FOR INSERT
  WITH CHECK (
    auth.uid() = student_id
    AND EXISTS (
      SELECT 1 FROM tutoring_sessions 
      WHERE tutoring_sessions.id = reviews.session_id
      AND tutoring_sessions.estado = 'completada'
      AND tutoring_sessions.student_id = auth.uid()
    )
  );
 
-- Comentarios
COMMENT ON TABLE reviews IS 'Reseñas de tutorías realizadas';
COMMENT ON COLUMN reviews.rating IS '1-5 estrellas';
COMMENT ON COLUMN reviews.session_id IS 'FK opcional: vincula review con sesión específica';

🔐 Funciones Útiles

Calcular Rating Promedio de Tutor

CREATE OR REPLACE FUNCTION get_tutor_average_rating(tutor_uuid UUID)
RETURNS DECIMAL AS $$
  SELECT COALESCE(ROUND(AVG(rating)::numeric, 1), 0)
  FROM reviews
  WHERE tutor_id = tutor_uuid;
$$ LANGUAGE SQL STABLE;

Contar Tutorías Completadas

CREATE OR REPLACE FUNCTION count_completed_sessions(tutor_uuid UUID)
RETURNS INTEGER AS $$
  SELECT COUNT(*)::INTEGER
  FROM tutoring_sessions
  WHERE tutor_id = tutor_uuid
  AND estado = 'completada';
$$ LANGUAGE SQL STABLE;

📊 Vistas Útiles

Vista: tutores_con_stats

CREATE OR REPLACE VIEW tutores_con_stats AS
SELECT 
  t.*,
  p.full_name,
  p.email,
  p.avatar_url,
  COALESCE(ROUND(AVG(r.rating)::numeric, 1), 0) as avg_rating,
  COUNT(DISTINCT r.id) as total_reviews,
  COUNT(DISTINCT ts.id) FILTER (WHERE ts.estado = 'completada') as completed_sessions
FROM tutores t
LEFT JOIN profiles p ON t.user_id = p.id
LEFT JOIN reviews r ON r.tutor_id = t.id
LEFT JOIN tutoring_sessions ts ON ts.tutor_id = t.id
GROUP BY t.id, p.full_name, p.email, p.avatar_url;

🚀 Triggers

Auto-update updated_at

CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
 
-- Aplicar a todas las tablas
CREATE TRIGGER update_profiles_updated_at BEFORE UPDATE ON profiles
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
 
CREATE TRIGGER update_tutores_updated_at BEFORE UPDATE ON tutores
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
 
CREATE TRIGGER update_ramos_updated_at BEFORE UPDATE ON ramos
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
 
CREATE TRIGGER update_tutor_ramo_updated_at BEFORE UPDATE ON tutor_ramo
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
 
CREATE TRIGGER update_offers_updated_at BEFORE UPDATE ON tutoring_offers
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
 
CREATE TRIGGER update_requests_updated_at BEFORE UPDATE ON tutoring_requests
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
 
CREATE TRIGGER update_sessions_updated_at BEFORE UPDATE ON tutoring_sessions
  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

📝 Notas de Migración desde Laravel

❌ Eliminado

  • personal_access_tokens - Supabase Auth maneja JWT nativamente
  • password en users - Manejado por auth.users de Supabase
  • Timestamps Laravel (created_at, updated_at) - Ahora TIMESTAMPTZ

✅ Agregado

  • RLS Policies en todas las tablas
  • Funciones para cálculos comunes
  • Vistas para queries complejas
  • Triggers para auto-update de timestamps
  • UUID en lugar de BIGINT para PKs

🔄 Modificado

  • usersprofiles (extiende auth.users)
  • Tipos ENUM propios de PostgreSQL
  • Validaciones con CHECK constraints
  • Referencias a auth.uid() para RLS