Skip to content

Schema Prisma — Referencia Completa

Fuente: apps/api/prisma/schema.prisma


Tabla: User | Propósito: Usuarios del sistema — todos los roles

CampoTipoNullableDescripción
idString (uuid)NoPK
emailString (unique)NoEmail del usuario
passwordHashString?null si solo OAuth
fullNameStringNoNombre completo
avatarUrlString?URL del avatar
avatarBytes?Bytes del avatar (almacenamiento inline)
roleUserRoleNoFREE por defecto
professionalRoleString?”sales”, “developer”, “hr”, etc.
industryString?Sector profesional
organizationIdString?FK a Organization (null = usuario individual)
departmentIdString?FK a Department
stripeCustomerIdString (unique)?Customer Stripe para pagos
stripeSubIdString (unique)?Subscription activa en Stripe
subPeriodEndDateTime?Vencimiento de la suscripción
fluidityScoreFloat?Score 0-100 calculado por NappAI
localeStringNo”es-ES” por defecto
isEmailVerifiedBooleanNoVerificación de email completada
isSuspendedBooleanNoFlag de suspensión por SUPER_ADMIN
lastActiveAtDateTime?Última actividad registrada
jobTitleString?Cargo profesional
companyString?Empresa actual
linkedinUrlString?URL del perfil LinkedIn
aiLevelString?’explorer’ / ‘curious’ / ‘practitioner’ / ‘advanced’
goalsString[]NoObjetivos de aprendizaje del usuario
onboardingCompletedBooleanNoIndica si completó el onboarding
notificationPrefsJson?Preferencias de notificación por tipo
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: enrollments → Enrollment[], progress → Progress[], certificates → Certificate[], quizAttempts → QuizAttempt[], tutorMessages → TutorMessage[], bookmarks → Bookmark[], oauthAccounts → OAuthAccount[], refreshTokens → RefreshToken[], creatorProfile → CreatorProfile?, applications → InstructorApplication[], aiTasks → AITask[], notifications → Notification[], studentBadgeGrants → StudentBadgeGrant[], xpEvents → StudentXPEvent[], studentProfile → StudentProfile?

Índices: @@index([email]), @@index([organizationId]), @@index([stripeCustomerId])


Tabla: OAuthAccount | Propósito: Cuentas OAuth vinculadas (Google, LinkedIn, GitHub)

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User (cascade delete)
providerStringNo”google” / “linkedin” / “github”
providerIdStringNoID del usuario en el proveedor externo
createdAtDateTimeNoTimestamp de vinculación

Relaciones: user → User

Índices: @@unique([provider, providerId]), @@index([userId])


Tabla: RefreshToken | Propósito: Tokens de refresco JWT con metadatos de sesión

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User (cascade delete)
tokenString (unique)NoValor del refresh token
deviceInfoString?User-Agent del dispositivo
expiresAtDateTimeNoFecha de expiración del token
ipAddressString?IP de la sesión
cityString?Ciudad detectada por geolocalización
countryString?País detectado por geolocalización
lastActiveAtDateTime?Última actividad con este token
createdAtDateTimeNoTimestamp de creación

Relaciones: user → User

Índices: @@index([userId]), @@index([token])


Tabla: Path | Propósito: Curso o ruta de aprendizaje completa

CampoTipoNullableDescripción
idString (uuid)NoPK
titleStringNoTítulo del curso
slugString (unique)NoURL amigable
descriptionStringNoDescripción corta
longDescriptionString?Descripción larga para la página de detalle
thumbnailUrlString?Imagen de portada
accessLevelAccessLevelNoNivel mínimo requerido para acceder
targetRolesString[]NoPúblico objetivo ([“sales”, “developer”])
targetIndustriesString[]NoIndustrias objetivo
estimatedHoursInt?Duración estimada en horas
isPublishedBooleanNoVisible en el catálogo
isFeaturedBooleanNoDestacado en portada
organizationIdString?null = ruta global; no null = privada corporativa
difficultyLevelStringNo”Principiante” por defecto
accentColorString?Hex para display de card (ej. “#4f46e5”)
enrollmentCountIntNoContador cacheado para display
isNewBooleanNoMuestra badge “NUEVO” en la card
orderIntNoOrden en el catálogo (default 0)
creatorIdString?FK a CreatorProfile
avgRatingFloatNoRating denormalizado (actualizado por job nocturno)
reviewCountIntNoNúmero de reviews denormalizado
totalEnrollmentsIntNoEnrollments reales denormalizados
skillsString[]NoHabilidades que desarrolla la ruta
statusPathStatusNoDRAFT / PENDING_REVIEW / PUBLISHED / ARCHIVED
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: modules → Module[], enrollments → Enrollment[], orgPaths → OrgPath[], certificate → CertificateTemplate?, quiz → Quiz?, reviews → PathReview[], resourceAttachments → ResourceAttachment[], checklist → PathChecklist?

Índices: @@index([slug]), @@index([accessLevel]), @@index([organizationId]), @@index([creatorId]), @@index([status])


Tabla: Module | Propósito: Módulo dentro de un Path

CampoTipoNullableDescripción
idString (uuid)NoPK
pathIdStringNoFK a Path (cascade delete)
titleStringNoTítulo del módulo
descriptionString?Descripción opcional
accessLevelAccessLevelNoHereda del Path si no se especifica explícitamente
orderIntNoPosición dentro del Path
isPublishedBooleanNoVisible para usuarios
estimatedMinInt?Duración estimada en minutos
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: path → Path, moduleLessons → ModuleLesson[], quiz → Quiz?, progress → Progress[], attachments → ResourceAttachment[]

Índices: @@index([pathId]), @@index([pathId, order])


Tabla: ModuleLesson | Propósito: Tabla de unión muchos-a-muchos entre Module y Lesson (permite reutilizar lecciones)

CampoTipoNullableDescripción
idString (uuid)NoPK
moduleIdStringNoFK a Module (cascade delete)
lessonIdStringNoFK a Lesson (cascade delete)
orderIntNoPosición de la lección dentro del módulo (default 0)
addedAtDateTimeNoTimestamp en que se vinculó la lección al módulo

Relaciones: module → Module, lesson → Lesson

Índices: @@unique([moduleId, lessonId]), @@index([moduleId]), @@index([lessonId])


Tabla: Lesson | Propósito: Unidad de contenido individual (reutilizable entre módulos)

CampoTipoNullableDescripción
idString (uuid)NoPK
titleStringNoTítulo de la lección
contentTypeLessonContentTypeNoMARKDOWN / VIDEO / H5P / MIXED
contentMdString?Contenido Markdown (para MARKDOWN o MIXED)
videoUrlString?URL Bunny.net (para VIDEO o MIXED)
videoDurationSecInt?Duración del vídeo en segundos
accessLevelAccessLevelNoPuede ser más abierto que el módulo padre (para previews)
orderIntNoOrden global para display en librería
durationMinIntNoDuración estimada en minutos (default 10)
isPublishedBooleanNoVisible para usuarios
seoDescriptionString?Descripción SEO (para lecciones PUBLIC)
changeLogsJsonNoArray de eventos de cambio: [{note, authorName, date}]
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: moduleLessons → ModuleLesson[], progress → Progress[], bookmarks → Bookmark[], attachments → ResourceAttachment[]

Índices: @@index([accessLevel])


Tabla: Resource | Propósito: Recurso educativo reutilizable (PDF, vídeo, enlace, etc.) creado por un instructor

CampoTipoNullableDescripción
idString (uuid)NoPK
creatorIdStringNoFK a CreatorProfile
typeResourceTypeNoPDF / DOCX / PPTX / XLSX / IMAGE / YOUTUBE / H5P / LINK
titleStringNoNombre del recurso
descriptionString?Descripción del recurso
fileDataBytes?Contenido binario del archivo (≤10MB en PostgreSQL)
mimeTypeString?Tipo MIME del archivo
fileSizeInt?Tamaño en bytes
fileNameString?Nombre original del archivo
externalUrlString?URL externa (YouTube, enlace externo)
tagsString[]NoEtiquetas para búsqueda y filtrado
aiTaggedBooleanNoSi fue etiquetado automáticamente por IA
versionIntNoVersión del recurso (default 1)
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: creator → CreatorProfile, attachments → ResourceAttachment[]

Índices: @@index([creatorId]), @@index([type]), @@index([creatorId, type])


Tabla: ResourceAttachment | Propósito: Vinculación de un Resource a una Lesson, Module o Path (exactamente uno de los tres)

CampoTipoNullableDescripción
idString (uuid)NoPK
resourceIdStringNoFK a Resource (cascade delete)
lessonIdString?FK a Lesson (si se adjunta a lección)
moduleIdString?FK a Module (si se adjunta a módulo)
pathIdString?FK a Path (si se adjunta a ruta)
roleAttachmentRoleNoMAIN_CONTENT / SUPPLEMENTARY
orderIntNoPosición en la lista de adjuntos (default 0)
createdAtDateTimeNoTimestamp de creación

Relaciones: resource → Resource, lesson → Lesson?, module → Module?, path → Path?

Índices: @@index([resourceId]), @@index([lessonId]), @@index([moduleId]), @@index([pathId])


Tabla: PathChecklist | Propósito: Checklist de publicación de una ruta (flujo DRAFT → PUBLISHED)

CampoTipoNullableDescripción
idString (uuid)NoPK
pathIdString (unique)NoFK a Path (cascade delete) — 1:1
checkedItemsString[]NoItems marcados manualmente por el creador
autoResultsJsonNoResultados de validaciones automáticas (default {})
isCompleteBooleanNoIndica si el checklist está completo
completedAtDateTime?Timestamp en que se completó
reviewRequestedAtDateTime?Timestamp en que se solicitó revisión
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: path → Path

Índices: @@index([pathId]), @@index([isComplete])


Tabla: Bookmark | Propósito: Marcador de una lección guardada por un usuario

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User
lessonIdStringNoFK a Lesson
noteString?Nota personal del usuario
createdAtDateTimeNoTimestamp de creación

Relaciones: user → User, lesson → Lesson

Índices: @@unique([userId, lessonId])


Tabla: Organization | Propósito: Multi-tenancy — cuenta corporativa

CampoTipoNullableDescripción
idString (uuid)NoPK
nameStringNoNombre de la organización
slugString (unique)NoIdentificador URL
logoUrlString?URL del logo para co-branding
brandColorString?Hex para co-branding de certificados
planOrgPlanNoTEAM / BUSINESS / ENTERPRISE
stripeCustomerIdString (unique)?Customer de Stripe
stripeSubIdString (unique)?Subscription de Stripe
maxSeatsIntNoLímite de usuarios (default 5)
isActiveBooleanNoEstado activo de la cuenta
statusOrgStatusNoTRIAL / ACTIVE / SUSPENDED / CANCELLED / CHURNED
trialEndsAtDateTime?Fecha de fin del período de prueba
contractStartAtDateTime?Inicio del contrato
contractEndAtDateTime?Fin del contrato
accountManagerIdString?ID del account manager asignado
salesNotesString?Notas internas del equipo de ventas
sourceOrgSourceNoDEMO_REQUEST / DIRECT_SALE / PARTNER / EVENT
demoRequestIdString (unique)?FK a DemoRequest de origen
customPricePerSeatDecimal?Precio por asiento negociado (10,2)
billingCycleBillingCycleNoMONTHLY / ANNUAL
discountPercentInt?Descuento en porcentaje (default 0)
industryString?Sector de la empresa
websiteString?Sitio web
countryString?País
companySizeString?Tamaño de la empresa
descriptionString?Descripción de la organización
contactNameString?Nombre del contacto principal
contactEmailString?Email del contacto principal
contactPhoneString?Teléfono del contacto principal
brandPrimaryColorString?Color primario de marca (default “#7C3AED”)
brandSecondaryColorString?Color secundario de marca (default “#1E1E2D”)
loginMethodStringNoMétodo de login permitido (default “email+google”)
passwordStrengthStringNoNivel de fortaleza de contraseña (default “medium”)
require2FABooleanNoRequiere autenticación de dos factores
restrictDomainBooleanNoRestringe login a dominio específico
allowedDomainString?Dominio permitido si restrictDomain es true
sessionTimeoutHoursInt?Timeout de sesión en horas
autoDeactivateDaysInt?Días de inactividad antes de desactivar usuario
seatAutoReclaimBooleanNoReclama asientos de usuarios inactivos automáticamente
seatAlertAt90BooleanNoAlerta cuando se alcanza el 90% de asientos
ssoEntityIdString?Entity ID del SSO (Enterprise only)
ssoUrlString?URL del SSO
ssoX509CertificateString?Certificado X.509 para SSO
ssoDefaultRoleString?Rol por defecto en auto-provisioning SSO
ssoAutoProvisioningBooleanNoCrea usuarios automáticamente vía SSO
notifPreferencesJson?Preferencias de notificación de la org
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: members → User[], customPaths → Path[], orgPaths → OrgPath[], invitations → Invitation[], certificates → Certificate[], departments → Department[], webhooks → OrgWebhook[], apiKeys → OrgApiKey[], integrations → OrgIntegration[], demoRequest → DemoRequest?, auditLogs → OrgAuditLog[], globalAuditLogs → AuditLog[]

Índices: @@index([slug])


Tabla: OrgPlanDefinition | Propósito: Configuración de planes corporativos disponibles con precios y características

CampoTipoNullableDescripción
idStringNoPK (slug del plan, ej. “TEAM”)
nameStringNoNombre legible del plan
pricePerSeatDecimalNoPrecio por asiento (10,2)
minSeatsIntNoMínimo de asientos requeridos
maxSeatsInt?Máximo de asientos (null = ilimitado)
featuresJsonNoFeatures incluidas en el plan
stripePriceIdMonthlyString?Price ID de Stripe para facturación mensual
stripePriceIdAnnualString?Price ID de Stripe para facturación anual
isActiveBooleanNoSi el plan está disponible para contratar
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Tabla: Department | Propósito: Departamento dentro de una organización corporativa

CampoTipoNullableDescripción
idString (uuid)NoPK
organizationIdStringNoFK a Organization (cascade delete)
nameStringNoNombre del departamento
createdAtDateTimeNoTimestamp de creación

Relaciones: organization → Organization, members → User[]

Índices: @@unique([organizationId, name]), @@index([organizationId])


Tabla: Invitation | Propósito: Invitación de email para incorporar miembros a una organización

CampoTipoNullableDescripción
idString (uuid)NoPK
organizationIdStringNoFK a Organization (cascade delete)
emailStringNoEmail del invitado
roleUserRoleNoRol que se asignará al aceptar (default CORPORATE_USER)
departmentString?Departamento asignado al aceptar
statusInvitationStatusNoPENDING / ACCEPTED / EXPIRED / REVOKED
tokenString (unique)NoToken UUID para el enlace de invitación
invitedByIdStringNoFK a User (quien envió la invitación)
expiresAtDateTimeNoFecha de expiración del enlace
acceptedAtDateTime?Timestamp de aceptación
createdAtDateTimeNoTimestamp de creación

Relaciones: organization → Organization, invitedBy → User

Índices: @@index([organizationId]), @@index([email]), @@index([token])


Tabla: OrgPath | Propósito: Asignación de una ruta de aprendizaje a una organización corporativa

CampoTipoNullableDescripción
idString (uuid)NoPK
organizationIdStringNoFK a Organization (cascade delete)
pathIdStringNoFK a Path (cascade delete)
assignedAtDateTimeNoTimestamp de asignación

Relaciones: organization → Organization, path → Path

Índices: @@unique([organizationId, pathId]), @@index([organizationId])


Tabla: OrgApiKey | Propósito: API key corporativa para integraciones externas (almacenada como hash)

CampoTipoNullableDescripción
idString (uuid)NoPK
organizationIdString (unique)NoFK a Organization (cascade delete) — una key por org
keyHashStringNoHash bcrypt de la API key
keyPrefixStringNoPrefijo legible de la key (ej. “napp_“)
keySuffixStringNoÚltimos 4 caracteres de la key (display)
createdAtDateTimeNoTimestamp de creación
lastUsedAtDateTime?Último uso de la key

Relaciones: organization → Organization

Índices: @@unique([organizationId]), @@index([organizationId])


Tabla: OrgAuditLog | Propósito: Registro de auditoría interno de acciones dentro de una organización corporativa

CampoTipoNullableDescripción
idString (uuid)NoPK
organizationIdStringNoFK a Organization (cascade delete)
actorIdString?ID del usuario que realizó la acción
actorRoleStringNoRol del actor en el momento de la acción
actionStringNoAcción realizada
targetTypeString?Tipo de entidad afectada
targetIdString?ID de la entidad afectada
oldValueJson?Valor anterior
newValueJson?Valor nuevo
ipAddressString?IP del actor
createdAtDateTimeNoTimestamp del evento

Relaciones: organization → Organization

Índices: @@index([organizationId, createdAt(sort: Desc)])


Tabla: OrgIntegration | Propósito: Integración de terceros configurada para una organización

CampoTipoNullableDescripción
idString (uuid)NoPK
organizationIdStringNoFK a Organization (cascade delete)
providerStringNoNombre del proveedor de integración
statusStringNoEstado de la integración (default “connected”)
configJson?Configuración específica del proveedor
connectedAtDateTimeNoTimestamp de conexión

Relaciones: organization → Organization

Índices: @@unique([organizationId, provider]), @@index([organizationId])


Tabla: OrgWebhook | Propósito: Webhook saliente configurado por una organización para recibir eventos

CampoTipoNullableDescripción
idString (uuid)NoPK
organizationIdStringNoFK a Organization (cascade delete)
urlStringNoURL destino del webhook
eventsString[]NoLista de eventos suscritos
statusStringNoEstado del webhook (default “active”)
secretStringNoSecret para verificar la firma HMAC
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: organization → Organization

Índices: @@index([organizationId])


Tabla: Enrollment | Propósito: Matrícula de un usuario en una ruta de aprendizaje

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User
pathIdStringNoFK a Path
enrolledAtDateTimeNoTimestamp de matrícula
completedAtDateTime?Timestamp de finalización (null = en curso)

Relaciones: user → User, path → Path

Índices: @@unique([userId, pathId]), @@index([userId]), @@index([pathId])


Tabla: Progress | Propósito: Seguimiento del progreso de un usuario en lecciones, módulos y rutas

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User
lessonIdString?FK a Lesson (si es progreso de lección)
moduleIdString?FK a Module (si es progreso de módulo)
pathIdString?FK a Path (si es progreso de ruta) — sin FK explícita
statusProgressStatusNoNOT_STARTED / IN_PROGRESS / COMPLETED
timeSpentSecIntNoTiempo total dedicado en segundos
lastPositionInt?Posición en vídeo (segundos) para reanudar
startedAtDateTime?Timestamp de inicio
completedAtDateTime?Timestamp de finalización
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: user → User, lesson → Lesson?, module → Module?

Índices: @@unique([userId, lessonId]), @@unique([userId, moduleId]), @@unique([userId, pathId]), @@index([userId]), @@index([userId, pathId])


Tabla: Quiz | Propósito: Evaluación asociada a un módulo o a una ruta completa

CampoTipoNullableDescripción
idString (uuid)NoPK
moduleIdString (unique)?FK a Module (null = evaluación de ruta)
pathIdString (unique)?FK a Path (para evaluaciones de ruta)
titleStringNoTítulo del quiz
passingScoreIntNoPuntuación mínima para aprobar (default 70)
maxAttemptsIntNoIntentos máximos permitidos (default 3)
timeLimitMinInt?Límite de tiempo en minutos (null = sin límite)
changeLogsJsonNoHistorial de cambios: [{note, authorName, date}]
isAiGeneratedBooleanNoSi fue generado por IA
isPublishedBooleanNoVisible para los estudiantes
publishedAtDateTime?Timestamp de publicación
quizTypeStringNo’EVALUATION’ / ‘CASE_STUDY’ (default ‘EVALUATION’)
instructionsString?Instrucciones del quiz
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: module → Module?, path → Path?, questions → QuizQuestion[], attempts → QuizAttempt[]


Tabla: QuizQuestion | Propósito: Pregunta individual dentro de un Quiz

CampoTipoNullableDescripción
idString (uuid)NoPK
quizIdStringNoFK a Quiz
typeQuizQuestionTypeNoMULTIPLE_CHOICE / TRUE_FALSE / OPEN_ENDED
questionStringNoTexto de la pregunta
optionsJson?Opciones para MULTIPLE_CHOICE: [{id, text, isCorrect, order}]
correctAnswerString?Respuesta correcta para TRUE_FALSE: ‘true’ / ‘false’
explanationString?Explicación de la respuesta correcta
orderIntNoPosición en el quiz
pointsIntNoPuntos que vale la pregunta (default 1)
gradingCriteriaString?Criterios de evaluación para OPEN_ENDED
gradingModeString?’manual’ / ‘ai’ / ‘both’ — solo para OPEN_ENDED
aiGeneratedBooleanNoSi fue generada por IA
aiTaskIdString?ID del AITask que originó esta pregunta
difficultyString?’easy’ / ‘medium’ / ‘hard’
lessonRefString?Referencia a la lección de origen

Relaciones: quiz → Quiz, answers → QuizAnswer[]


Tabla: QuizAnswer | Propósito: Respuesta dada por un usuario a una pregunta en un intento de quiz

CampoTipoNullableDescripción
idString (uuid)NoPK
attemptIdStringNoFK a QuizAttempt
questionIdStringNoFK a QuizQuestion
answerStringNoRespuesta dada por el usuario
isCorrectBoolean?Resultado de corrección (null = pendiente evaluación)
aiScoreFloat?Puntuación IA para respuestas abiertas (0-1)
aiFeedbackString?Feedback textual de la IA

Relaciones: attempt → QuizAttempt, question → QuizQuestion


Tabla: QuizAttempt | Propósito: Intento de resolución de un quiz por un usuario

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User
quizIdStringNoFK a Quiz
scoreFloatNoPuntuación obtenida
passedBooleanNoSi supera el passingScore del quiz
startedAtDateTimeNoTimestamp de inicio
completedAtDateTime?Timestamp de finalización

Relaciones: user → User, quiz → Quiz, answers → QuizAnswer[]

Índices: @@index([userId, quizId])


Tabla: FluidityTestSession | Propósito: Sesión del AI Fluency Test (puede ser anónima antes del registro)

CampoTipoNullableDescripción
idString (uuid)NoPK
sessionKeyString (unique)NoClave de sesión (permite usuarios anónimos)
userIdString?FK a User (se asocia si el usuario se registra después)
answersJsonNoRespuestas del test
profileJson?Resultado del AI Fluency Test con perfil calculado
scoreFloat?Puntuación del test
completedAtDateTime?Timestamp de finalización
createdAtDateTimeNoTimestamp de creación

Índices: @@index([sessionKey])


Tabla: Certificate | Propósito: Diploma emitido y verificable públicamente

CampoTipoNullableDescripción
idString (uuid)NoPK — también UUID público de verificación
userIdStringNoFK a User
pathIdString?FK a Path (puede ser nulo para badges de módulo)
moduleIdString?FK a Module (para MODULE_BADGE)
typeCertTypeNoDIAGNOSTIC_BADGE / MODULE_BADGE / PATH_CERTIFICATE / SPECIALIZATION_CERTIFICATE / CORPORATE_CERTIFICATE
scoreFloatNoPuntuación obtenida
verificationHashString (unique)NoSHA-256(userId+pathId+issuedAt+score)
issuedAtDateTimeNoFecha de emisión
expiresAtDateTime?null = no expira
linkedInUrlString?URL para compartir en LinkedIn
badgeMetadataJson?Open Badges 3.0 assertion JSON
blockchainTxHashString?Hash de transacción blockchain (Fase 3)
organizationIdString?FK a Organization (para certificados corporativos)
pdfStorageKeyString?Key en Cloudflare R2

Relaciones: user → User, organization → Organization?

Índices: @@index([userId]), @@index([verificationHash])


Tabla: CertificateTemplate | Propósito: Plantilla de certificado asociada a una ruta (1:1 con Path)

CampoTipoNullableDescripción
idString (uuid)NoPK
pathIdString (unique)NoFK a Path — 1:1
titleStringNoTítulo del certificado
descriptionStringNoDescripción del logro
issuerNameStringNoNombre del emisor (default “NappAI Fluency”)
validityDaysInt?Días de validez (null = no expira)
badgeImageUrlString?URL de la imagen del badge

Relaciones: path → Path


Tabla: CreatorProfile | Propósito: Perfil público de un instructor certificado por NappAI

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdString (unique)NoFK a User — 1:1
usernameString (unique)NoSlug para /creators/:username
displayNameStringNoNombre público del creador
headlineStringNoTitular profesional
bioStringNoBiografía extendida
avatarUrlString?URL del avatar
avatarBytes?Bytes del avatar (almacenamiento inline)
bannerUrlString?URL del banner del perfil
linkedinUrlString?Perfil LinkedIn
websiteUrlString?Sitio web personal
twitterUrlString?Perfil Twitter/X
specialtiesString[]NoEspecialidades del creador
yearsExperienceInt?Años de experiencia
isPublicBooleanNofalse hasta que NappAI certifique al creador
certifiedAtDateTime?Timestamp de certificación por NappAI
certifiedByString?userId del staff de NappAI que aprobó
totalStudentsIntNoAlumnos totales (denormalizado, job nocturno)
avgRatingFloatNoRating medio (denormalizado)
completionRateFloatNoTasa de completado (denormalizado)
totalPathsIntNoNúmero de rutas publicadas (denormalizado)
suspendedAtDateTime?Timestamp de suspensión del creador
paymentDataNoteString?Nota sobre datos de pago del creador
earningsTermsAcceptedAtDateTime?Timestamp de aceptación de los términos de ganancias
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: user → User, paths → Path[], resources → Resource[], badges → CreatorBadgeGrant[], reviews → CreatorReview[], monthlyEarnings → CreatorMonthlyEarning[], earningPreview → CreatorEarningPreview?

Índices: @@index([username]), @@index([isPublic])


Tabla: CreatorReview | Propósito: Valoración de un usuario sobre un creador

CampoTipoNullableDescripción
idString (uuid)NoPK
creatorIdStringNoFK a CreatorProfile
userIdStringNoFK a User (quien valoró)
ratingIntNoPuntuación 1-5
commentString?Comentario textual
createdAtDateTimeNoTimestamp de creación

Relaciones: creator → CreatorProfile, user → User

Índices: @@unique([creatorId, userId]), @@index([creatorId])


Tabla: InstructorApplication | Propósito: Solicitud para convertirse en instructor / creador de contenido

CampoTipoNullableDescripción
idString (uuid)NoPK
fullNameStringNoNombre completo del solicitante
emailStringNoEmail del solicitante
linkedinUrlStringNoPerfil LinkedIn
currentRoleStringNoCargo actual
courseProposalStringNoPropuesta de curso a crear
statusApplicationStatusNoPENDING / IN_REVIEW / APPROVED / REJECTED / WITHDRAWN
reviewNotesString?Notas internas de revisión
reviewedByString?ID del admin que revisó
reviewedAtDateTime?Timestamp de revisión
statusHistoryJsonNoHistorial de cambios de estado: ApplicationEvent[]
userIdString?FK a User (si el solicitante ya tiene cuenta)
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: user → User?

Índices: @@index([status]), @@index([email])


Tabla: StudentProfile | Propósito: Perfil público del estudiante con configuración de privacidad

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdString (unique)NoFK a User (cascade delete) — 1:1
usernameString (unique)NoNombre de usuario único para el perfil público
isPublicBooleanNoSi el perfil es visible públicamente
bioString?Biografía del estudiante
locationString?Ubicación
websiteUrlString?Sitio web personal
twitterUrlString?Perfil Twitter/X
githubUrlString?Perfil GitHub
showCertificatesBooleanNoMuestra certificados en el perfil (default true)
showBadgesBooleanNoMuestra badges en el perfil (default true)
showPathsBooleanNoMuestra rutas completadas (default true)
showTimelineBooleanNoMuestra timeline de actividad (default true)
showXPBooleanNoMuestra puntos XP (default true)
showScoreBooleanNoMuestra fluency score (default true)
usernameChangedAtDateTime?Timestamp del último cambio de username
viewCountIntNoNúmero de visitas al perfil
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: user → User

Índices: @@index([username])


Tabla: StudentBadgeDefinition | Propósito: Definición de un badge para estudiantes con criterios de concesión

CampoTipoNullableDescripción
idString (cuid)NoPK
slugString (unique)NoIdentificador único del badge
nameStringNoNombre del badge
descriptionStringNoDescripción del logro (default "")
categoryStudentBadgeCategoryNoLEVEL / ROUTES / FLUENCY / STREAK / QUIZ / SPECIALIZATION / SPEED / HONOR
iconKeyStringNoClave del icono para renderizado
tierIntNoNivel del badge (1=base, 2=silver, 3=gold, 4=diamond)
criteriaJsonNoCriterios de concesión del badge
isActiveBooleanNoSi el badge está activo
sortOrderIntNoOrden de visualización (default 0)
createdAtDateTimeNoTimestamp de creación

Relaciones: grants → StudentBadgeGrant[]


Tabla: StudentBadgeGrant | Propósito: Concesión de un badge a un estudiante

CampoTipoNullableDescripción
idString (cuid)NoPK
studentIdStringNoFK a User
badgeIdStringNoFK a StudentBadgeDefinition
grantedByString?’system’ o userId del admin que concedió
grantedAtDateTimeNoTimestamp de concesión
revokedAtDateTime?Timestamp de revocación (null = vigente)
supersededAtDateTime?Timestamp en que fue superado por un tier superior
metadataJson?Metadatos adicionales de la concesión
assertionIdString (unique)?ID único Open Badges (cuid por defecto)
badgeUrlString?URL pública del badge assertion

Relaciones: student → User, badge → StudentBadgeDefinition

Índices: @@index([studentId, badgeId])


Tabla: StudentXPEvent | Propósito: Evento de puntos de experiencia (XP) ganado por un estudiante

CampoTipoNullableDescripción
idString (cuid)NoPK
userIdStringNoFK a User
eventTypeStringNoTipo de evento que generó el XP
xpIntNoPuntos XP otorgados
metadataJson?Contexto adicional del evento
createdAtDateTimeNoTimestamp del evento

Relaciones: user → User

Índices: @@index([userId])


Tabla: CreatorBadgeDefinition | Propósito: Definición de un badge para creadores con criterios de concesión

CampoTipoNullableDescripción
idString (uuid)NoPK
slugString (unique)NoIdentificador único del badge
nameStringNoNombre del badge
descriptionStringNoDescripción del logro
iconEmojiStringNoEmoji del badge (deprecated — usar iconKey)
iconKeyStringNoClave del icono (default “rising-star”)
tierIntNoNivel: 1=base, 2=silver, 3=gold, 4=diamond
criteriaJsonNoCriterios de concesión: {minStudents, minRating, ...}
isActiveBooleanNoSi el badge está activo

Relaciones: grants → CreatorBadgeGrant[]


Tabla: CreatorBadgeGrant | Propósito: Concesión de un badge a un creador

CampoTipoNullableDescripción
idString (uuid)NoPK
creatorIdStringNoFK a CreatorProfile
badgeIdStringNoFK a CreatorBadgeDefinition
grantedAtDateTimeNoTimestamp de concesión
revokedAtDateTime?Timestamp de revocación (null = vigente)
grantedByString?’system’ o userId del admin que concedió

Relaciones: creator → CreatorProfile, badge → CreatorBadgeDefinition

Índices: @@unique([creatorId, badgeId]), @@index([creatorId])


Tabla: CreatorEarningLine | Propósito: Desglose por ruta de las ganancias mensuales de un creador

CampoTipoNullableDescripción
idString (uuid)NoPK
earningIdStringNoFK a CreatorMonthlyEarning (cascade delete)
pathIdStringNoID de la ruta (referencia desnormalizada)
pathTitleStringNoTítulo de la ruta (desnormalizado)
pathSlugStringNoSlug de la ruta (desnormalizado)
completionsCountIntNoCompletados de la ruta en el período
minutesConsumedIntNoMinutos consumidos en la ruta
avgRatingFloat?Rating medio de la ruta en el período
reviewsCountIntNoNúmero de reviews en el período
contributionPctFloatNoPorcentaje de contribución al pool del creador
grossAmountEurFloatNoImporte bruto en euros para esta ruta

Relaciones: earning → CreatorMonthlyEarning

Índices: @@index([earningId]), @@index([pathId])


Tabla: CreatorEarningPreview | Propósito: Vista previa en tiempo real de las ganancias estimadas del mes en curso (1:1 con CreatorProfile)

CampoTipoNullableDescripción
idString (uuid)NoPK
creatorIdString (unique)NoFK a CreatorProfile — 1:1
periodMonthStringNoMes en formato YYYY-MM
completionsCountIntNoCompletados acumulados en el mes
minutesConsumedIntNoMinutos consumidos acumulados
weightedRatingFloatNoRating ponderado acumulado
reviewsCountIntNoReviews acumuladas
estimatedGrossEurFloatNoEstimación de ganancia bruta en euros
estimatedPoolAmountFloatNoEstimación del pool total del mes
estimatedParticipationPctFloatNoEstimación del porcentaje de participación
isEligibleSoFarBooleanNoSi cumple criterios de elegibilidad hasta ahora
ineligibilityReasonString?Razón de inelegibilidad si aplica
lastCalculatedAtDateTimeNoTimestamp del último cálculo
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: creator → CreatorProfile

Índices: @@index([creatorId]), @@index([periodMonth])


Tabla: CreatorMonthlyEarning | Propósito: Liquidación mensual definitiva de ganancias de un creador

CampoTipoNullableDescripción
idString (uuid)NoPK
creatorIdStringNoFK a CreatorProfile
configIdStringNoFK a PlatformRevenueConfig
periodMonthStringNoMes en formato YYYY-MM
completionsCountIntNoCompletados en el período
minutesConsumedIntNoMinutos consumidos en el período
weightedRatingFloatNoRating ponderado del creador
reviewsCountIntNoReviews recibidas en el período
rawScoreFloatNoPuntuación bruta calculada
participationPctFloatNoPorcentaje de participación en el pool
grossAmountEurFloatNoImporte bruto en euros
isEligibleBooleanNoSi el creador es elegible para pago
ineligibilityReasonString?Razón de inelegibilidad si aplica
paymentStatusCreatorPaymentStatusNoPENDING_REVIEW / IN_REVIEW / APPROVED / PAID / ON_HOLD / REJECTED
paymentNotesString?Notas del proceso de pago
paymentStatusUpdatedAtDateTime?Timestamp del último cambio de estado de pago
paymentStatusUpdatedByString?ID del admin que cambió el estado de pago
exportedAtDateTime?Timestamp de exportación para pago
exportedByString?ID del admin que exportó
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: creator → CreatorProfile, config → PlatformRevenueConfig, lines → CreatorEarningLine[]

Índices: @@unique([creatorId, periodMonth]), @@index([periodMonth]), @@index([creatorId]), @@index([paymentStatus])


Tabla: PlatformRevenueConfig | Propósito: Configuración mensual del pool de ingresos para creadores (ADR-011)

CampoTipoNullableDescripción
idString (uuid)NoPK
periodMonthString (unique)NoMes en formato YYYY-MM
poolPercentageFloatNoPorcentaje del ingreso neto para el pool (default 0.25)
minCompletionsForEligibilityIntNoCompletados mínimos para ser elegible (default 5)
minReviewsForRatingIntNoReviews mínimas para contar el rating (default 3)
grossRevenueFloat?Ingreso bruto del mes
stripeFeesTotalFloat?Total de comisiones de Stripe
refundsTotalFloat?Total de reembolsos
netRevenueFloat?Ingreso neto (grossRevenue - fees - refunds)
creatorPoolAmountFloat?Importe total del pool para creadores
isLockedBooleanNoSi el período está cerrado para modificaciones
lockedAtDateTime?Timestamp de cierre del período
lockedByString?ID del admin que cerró el período
adminNotesString?Notas internas del admin
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: earnings → CreatorMonthlyEarning[]

Índices: @@index([periodMonth])


Tabla: Notification | Propósito: Notificación in-app para un usuario (ADR-012)

CampoTipoNullableDescripción
idString (cuid)NoPK
userIdStringNoFK a User (cascade delete)
typeNotificationTypeNoTipo de notificación (101 valores)
categoryNotificationCategoryNoLEARNING / ACHIEVEMENT / CONTENT / AI_TASK / TEAM / CREATOR / SYSTEM
titleStringNoTítulo de la notificación
bodyStringNoCuerpo del mensaje
actionUrlStringNoURL de acción principal
actionLabelString?Etiqueta del botón de acción
iconStringNoIcono (default ”🔔“)
iconBgStringNoColor de fondo del icono
severityStringNo’info’ / ‘warning’ / ‘critical’ (default ‘info’)
isReadBooleanNoSi ha sido leída por el usuario
readAtDateTime?Timestamp de lectura
channelsString[]NoCanales de entrega (‘in_app’, ‘email’, ‘push’)
metadataJson?Metadatos adicionales para deduplicación y contexto
emailSentAtDateTime?Timestamp de envío por email
pushSentAtDateTime?Timestamp de envío por push
createdAtDateTimeNoTimestamp de creación

Relaciones: user → User

Índices: @@index([userId, isRead, createdAt]), @@index([userId, category, createdAt]), @@index([createdAt])


Tabla: AICredential | Propósito: API keys de proveedores LLM cifradas con AES-256-GCM

CampoTipoNullableDescripción
idString (uuid)NoPK
providerString (unique)No’anthropic’ / ‘openai’ / ‘gemini’ / ‘qdrant’
labelStringNoNombre amigable (ej. “Anthropic Claude”)
encryptedValueStringNoAPI key cifrada: formato iv:tag:ciphertext (AES-256-GCM)
maskedPreviewStringNoÚltimos 4 chars del valor real: ”…X4kp”
isActiveBooleanNofalse = no se usa aunque exista
lastTestedAtDateTime?Timestamp del último test de conectividad
lastTestStatusString?’ok’ / ‘error’ / ‘timeout’
updatedByString?userId del SUPER_ADMIN que hizo el último cambio
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Tabla: AIRouteConfig | Propósito: Tabla de enrutamiento de casos de uso IA a proveedores LLM

CampoTipoNullableDescripción
idString (uuid)NoPK
useCaseString (unique)NoValor del enum AIUseCase
labelStringNoNombre legible (ej. “Tutor conversacional”)
providerStringNoProveedor primario (AIProvider enum)
modelStringNoModelo primario
fallbackProviderStringNoProveedor de fallback
fallbackModelStringNoModelo de fallback
temperatureFloatNoTemperatura del modelo (default 0.7)
maxTokensIntNoMáximo de tokens por respuesta (default 2048)
cacheTtlSecondsIntNoTTL de caché en segundos (0 = sin caché)
similarityThresholdFloatNoUmbral de similitud para caché semántica (default 0.85)
isActiveBooleanNoSi la ruta está habilitada
updatedByString?userId del admin que realizó el último cambio
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Tabla: AIUsageMetric | Propósito: Registro de cada llamada al motor IA con coste estimado

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdString?FK a User (null = llamada de sistema)
useCaseStringNoCaso de uso ejecutado
providerStringNoProveedor que respondió
modelStringNoModelo específico usado
latencyMsIntNoLatencia total en ms
tokensInIntNoTokens de entrada (default 0)
tokensOutIntNoTokens de salida (default 0)
estimatedCostUsdFloatNoCoste estimado en USD
fromCacheBooleanNoSi la respuesta vino de caché
hadFallbackBooleanNoSi se usó el proveedor de fallback
errorCodeString?Código de error (null = éxito)
createdAtDateTimeNoTimestamp de la llamada

Índices: @@index([userId]), @@index([useCase]), @@index([provider]), @@index([createdAt]), @@index([hadFallback])


Tabla: AIAlertConfig | Propósito: Configuración global de alertas IA (singleton — siempre id=‘singleton’)

CampoTipoNullableDescripción
idStringNoPK — siempre “singleton”
dailyThresholdUsdFloat?Umbral de coste diario en USD para alertas
monthlyThresholdUsdFloat?Umbral de coste mensual en USD para alertas
notificationEmailString?Email para notificaciones de alertas
webhookUrlString?URL de webhook para notificaciones externas
updatedAtDateTimeNoTimestamp de última modificación

Tabla: AIAlert | Propósito: Alerta disparada por el sistema de monitorización de IA

CampoTipoNullableDescripción
idString (uuid)NoPK
typeAIAlertTypeNoPROVIDER_DOWN / HIGH_ERROR_RATE / COST_THRESHOLD_EXCEEDED / LATENCY_DEGRADED / QDRANT_UNAVAILABLE / CREDENTIAL_EXPIRING / FALLBACK_ACTIVATED
severityStringNo’info’ / ‘warning’ / ‘critical’
messageStringNoMensaje descriptivo de la alerta
metadataJson?Datos adicionales del contexto de la alerta
isResolvedBooleanNoSi la alerta ha sido resuelta
resolvedByString?userId del admin que resolvió la alerta
resolvedAtDateTime?Timestamp de resolución
createdAtDateTimeNoTimestamp de creación

Índices: @@index([isResolved]), @@index([createdAt])


Tabla: AITask | Propósito: Tarea asíncrona de generación o evaluación IA con ciclo de revisión

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User
typeAITaskTypeNoQUIZ_GENERATION / LESSON_GENERATION / SUMMARY_GENERATION / CASE_STUDY_GENERATION / OPEN_ANSWER_EVALUATION / FLUENCY_TEST_SCORING / QDRANT_REINDEX_FULL / QDRANT_REINDEX_LESSON / CHURN_ANALYSIS / ADAPTIVE_ENGINE / TAG_SUGGESTION
statusAITaskStatusNoPENDING / RUNNING / COMPLETED / FAILED / CANCELLED
labelStringNoEtiqueta descriptiva para el usuario
contextIdString?ID del módulo/lección/ruta asociado
contextTypeString?’module’ / ‘lesson’ / ‘path’
progressPctIntNoProgreso en porcentaje (default 0)
progressMsgString?Mensaje de progreso para el usuario
errorMsgString?Mensaje de error si falló
providerString?Proveedor IA que ejecutó la tarea
modelString?Modelo IA que ejecutó la tarea
tokensUsedIntNoTokens consumidos (default 0)
costUsdFloatNoCoste estimado en USD (default 0)
bullJobIdString?ID del job en BullMQ
seenByUserBooleanNoSi el usuario ha visto la notificación de resultado
dismissedAtDateTime?Timestamp en que el usuario descartó la tarea
startedAtDateTime?Timestamp de inicio de ejecución
completedAtDateTime?Timestamp de finalización
launchPayloadJson?Payload de lanzamiento persistido
resultPayloadJson?Output del modelo para el diálogo de revisión
reviewStatusAITaskReviewStatusNoPENDING_REVIEW / IN_REVIEW / ACCEPTED / REJECTED / REFINED
reviewedAtDateTime?Timestamp de revisión por el usuario
reviewedByString?userId del usuario que revisó
parentTaskIdString?FK a AITask padre (para refinamientos)
refinementCountIntNoNúmero de refinamientos realizados
refinementNotesString?Notas del refinamiento
importedEntityIdString?ID de la entidad creada al importar el resultado
importedEntityTypeString?’quiz’ / ‘lesson’ / ‘summary’
importedAtDateTime?Timestamp de importación a BD
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: user → User, parentTask → AITask?, refinements → AITask[], quizGenerationResult → QuizGenerationResult?, lessonGenerationResult → LessonGenerationResult?, summaryGenerationResult → SummaryGenerationResult?

Índices: @@index([userId, status]), @@index([userId, type, status]), @@index([userId, createdAt]), @@index([parentTaskId]), @@index([contextId])


Tabla: LessonGenerationResult | Propósito: Resultado tipado de una tarea de generación de lección IA

CampoTipoNullableDescripción
idString (uuid)NoPK
taskIdString (unique)NoFK a AITask — 1:1
lessonIdStringNoID de la lección de destino
moduleIdStringNoID del módulo de contexto
pathIdStringNoID de la ruta de contexto
contentMdStringNoContenido Markdown generado
suggestedTitleStringNoTítulo sugerido por la IA (default "")
estimatedMinIntNoDuración estimada sugerida en minutos (default 10)
keyTopicsJsonNoLista de temas clave identificados (default [])
resourcesReferencedJsonNoRecursos referenciados en el contenido (default [])
insertedAtDateTime?Timestamp en que se insertó el contenido en la lección
createdAtDateTimeNoTimestamp de creación

Relaciones: task → AITask

Índices: @@index([lessonId]), @@index([moduleId])


Tabla: QuizGenerationResult | Propósito: Resultado tipado de una tarea de generación de quiz IA

CampoTipoNullableDescripción
idString (uuid)NoPK
taskIdString (unique)NoFK a AITask — 1:1
moduleIdStringNoID del módulo de contexto
pathIdStringNoID de la ruta de contexto
questionsJsonNoArray de GeneratedQuestion[] serializado
totalGeneratedIntNoTotal de preguntas generadas
importedCountIntNoPreguntas importadas al quiz definitivo (default 0)
importedAtDateTime?Timestamp de importación
createdAtDateTimeNoTimestamp de creación

Relaciones: task → AITask

Índices: @@index([moduleId])


Tabla: SummaryGenerationResult | Propósito: Resultado tipado de una tarea de generación de resumen IA

CampoTipoNullableDescripción
idString (uuid)NoPK
taskIdString (unique)NoFK a AITask — 1:1
lessonIdStringNoID de la lección de destino
objectivesTitleString?Título de la sección de objetivos generada
objectivesBulletsJson?Bullets de objetivos generados
summaryTitleString?Título de la sección de resumen generada
summaryBulletsJson?Bullets del resumen generado
fromCacheBooleanNoSi el resultado vino de caché
insertedSectionsJson?Secciones efectivamente insertadas en la lección
insertedAtDateTime?Timestamp de inserción en la lección
createdAtDateTimeNoTimestamp de creación

Relaciones: task → AITask


Tabla: GuideVersion | Propósito: Versión publicada de la Guía de Calidad del Creador (snapshot completo)

CampoTipoNullableDescripción
idString (uuid)NoPK
versionStringNoNúmero de versión (ej. “2.1”)
publishedAtDateTimeNoTimestamp de publicación
publishedByStringNouserId del admin que publicó
snapshotJsonNoSnapshot completo del contenido de la guía
noteString?Notas del cambio de versión

Tabla: GuideSection | Propósito: Sección de la Guía de Calidad del Creador

CampoTipoNullableDescripción
idString (uuid)NoPK
slugString (unique)NoIdentificador URL de la sección
emojiStringNoEmoji representativo de la sección
titleStringNoTítulo de la sección
eyebrowString?Texto supratítulo
subtitleString?Subtítulo descriptivo
orderIntNoPosición en la guía
isActiveBooleanNoSi la sección está visible
updatedByString?userId del último editor
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: blocks → GuideBlock[]

Índices: @@index([order]), @@index([isActive])


Tabla: GuideBlock | Propósito: Bloque de contenido dentro de una sección de la guía

CampoTipoNullableDescripción
idString (uuid)NoPK
sectionIdStringNoFK a GuideSection (cascade delete)
typeGuideBlockTypeNoCALLOUT / RULE_PAIR / STEP / SPEC_TABLE / EXAMPLE_PAIR / CHECKLIST / TEXT
payloadJsonNoContenido del bloque (estructura varía por tipo)
orderIntNoPosición dentro de la sección
isActiveBooleanNoSi el bloque está visible
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: section → GuideSection

Índices: @@index([sectionId, order])


Tabla: AuditLog | Propósito: Registro centralizado de trazabilidad de todas las acciones relevantes del sistema (ADR-013)

CampoTipoNullableDescripción
idString (cuid)NoPK
actorIdString?ID del usuario que ejecutó la acción
actorRoleUserRole?Rol del actor en el momento de la acción
actorEmailString?Email del actor
actorIpString?IP del actor
actorUserAgentString?User-Agent del actor
actionAuditActionNoAcción realizada (97 valores en el enum)
categoryAuditCategoryNoAUTH / USER_MANAGEMENT / CONTENT / ORGANIZATION / BILLING / AI_SYSTEM / SECURITY / STUDENT_PROGRESS / SYSTEM_CONFIG
severityAuditSeverityNoINFO / WARNING / CRITICAL (default INFO)
entityTypeAuditEntityType?Tipo de entidad afectada
entityIdString?ID de la entidad afectada
entityLabelString?Label legible de la entidad afectada
beforeJson?Estado anterior de la entidad
afterJson?Estado posterior de la entidad
metadataJson?Metadatos adicionales del contexto
successBooleanNoSi la acción fue exitosa (default true)
errorCodeString?Código de error si falló
errorMessageString?Mensaje de error si falló
sessionIdString?ID de la sesión del usuario
requestIdString?ID de la request HTTP
orgIdString?FK a Organization (si aplica)
createdAtDateTimeNoTimestamp del evento

Relaciones: organization → Organization?

Índices: @@index([actorId]), @@index([action]), @@index([category]), @@index([severity]), @@index([entityType, entityId]), @@index([orgId]), @@index([createdAt]), @@index([actorRole])


Tabla: ScheduledJob | Propósito: Configuración y estado de los cron jobs del sistema, con panel de control para SUPER_ADMIN

CampoTipoNullableDescripción
idString (uuid)NoPK
slugString (unique)NoIdentificador único del job (ej. “sync-creator-metrics”)
nameStringNoNombre legible del job
descriptionString?Descripción de qué hace el job
categoryStringNoCategoría del job (ej. “earnings”, “metrics”)
iconString?Icono para el panel de control
cronExprStringNoExpresión cron activa
cronDefaultStringNoExpresión cron por defecto (para reset)
timezoneStringNoTimezone del job (default “UTC”)
isEnabledBooleanNoSi el job está habilitado
retentionDaysIntNoDías de retención de ejecuciones históricas (default 30)
configJson?Configuración específica del job
notesString?Notas internas
lastRunAtDateTime?Timestamp de la última ejecución
lastStatusJobStatus?Estado de la última ejecución
lastDurationMsInt?Duración de la última ejecución en ms
lastItemsCountInt?Ítems procesados en la última ejecución
nextRunAtDateTime?Timestamp de la próxima ejecución prevista
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: executions → JobExecution[]

Índices: @@index([category]), @@index([isEnabled])


Tabla: JobExecution | Propósito: Registro histórico de cada ejecución de un cron job

CampoTipoNullableDescripción
idString (uuid)NoPK
jobIdStringNoFK a ScheduledJob (cascade delete)
statusJobStatusNoRUNNING / COMPLETED / FAILED / SKIPPED
startedAtDateTimeNoTimestamp de inicio
finishedAtDateTime?Timestamp de finalización
durationMsInt?Duración en ms
itemsProcessedInt?Ítems procesados en esta ejecución
itemsSummaryString?Resumen legible de ítems procesados
errorMessageString?Mensaje de error si falló
triggeredByStringNo’cron’ / ‘manual’ / userId del admin que lanzó manualmente
createdAtDateTimeNoTimestamp de creación

Relaciones: job → ScheduledJob

Índices: @@index([jobId, startedAt(sort: Desc)]), @@index([status])


Tabla: TutorConversation | Propósito: Conversación del tutor IA — agrupa mensajes de un contexto específico

CampoTipoNullableDescripción
idString (uuid)NoPK
userIdStringNoFK a User
moduleIdString?ID del módulo de contexto
lessonIdString?ID de la lección de contexto
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: messages → TutorMessage[]

Índices: @@index([userId]), @@index([userId, lessonId])


Tabla: TutorMessage | Propósito: Mensaje individual dentro de una conversación del tutor IA

CampoTipoNullableDescripción
idString (uuid)NoPK
conversationIdStringNoFK a TutorConversation
userIdStringNoFK a User
roleStringNo”user” / “assistant”
contentStringNoContenido del mensaje
ratingString?Valoración del usuario: ‘up’ / ‘down’ / null
providerString?Proveedor IA (ej. ‘anthropic’)
modelString?Modelo IA (ej. ‘claude-sonnet-4-5’)
latencyMsInt?Tiempo de respuesta en ms
createdAtDateTimeNoTimestamp de creación

Relaciones: conversation → TutorConversation, user → User

Índices: @@index([conversationId])


Tabla: DemoRequest | Propósito: Lead B2B — solicitud de demo corporativa

CampoTipoNullableDescripción
idString (uuid)NoPK
firstNameStringNoNombre del solicitante
lastNameStringNoApellidos del solicitante
emailStringNoEmail de contacto
phoneStringNoTeléfono de contacto
companyStringNoEmpresa
roleStringNoCargo del solicitante
sectorStringNoSector de la empresa
teamSizeStringNoTamaño del equipo
messageString?Mensaje adicional
sourceString?Fuente de captación
privacyAcceptedBooleanNoAceptación de política de privacidad
marketingConsentBooleanNoConsentimiento de marketing (default false)
utmSourceString?UTM source del tráfico
utmMediumString?UTM medium del tráfico
utmCampaignString?UTM campaign del tráfico
referrerString?Referrer HTTP
originPageString?Página de origen del formulario
statusDemoRequestStatusNoPENDING / CONTACTED / DEMO_SCHEDULED / DEMO_DONE / CONVERTED / DISCARDED
priorityDemoRequestPriorityNoNORMAL / HIGH_PRIORITY
internalNotesString?Notas internas del equipo de ventas
assignedToString?ID del commercial asignado
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: organization → Organization? (si se convirtió en org activa)

Índices: @@index([email]), @@index([status])


Tabla: PathReview | Propósito: Valoración de un estudiante sobre una ruta de aprendizaje

CampoTipoNullableDescripción
idString (uuid)NoPK
pathIdStringNoFK a Path
userIdStringNoFK a User (quien valoró)
ratingIntNoPuntuación 1-5
commentString?Comentario textual
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: path → Path, user → User

Índices: @@unique([pathId, userId]), @@index([pathId])