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?Sínull si solo OAuth
fullNameStringNoNombre completo
avatarUrlString?SíURL del avatar
avatarBytes?SíBytes del avatar (almacenamiento inline)
roleUserRoleNoFREE por defecto
professionalRoleString?Sí”sales”, “developer”, “hr”, etc.
industryString?SíSector profesional
organizationIdString?SíFK a Organization (null = usuario individual)
departmentIdString?SíFK a Department
stripeCustomerIdString (unique)?SíCustomer Stripe para pagos
stripeSubIdString (unique)?SíSubscription activa en Stripe
subPeriodEndDateTime?SíVencimiento de la suscripción
fluidityScoreFloat?SíScore 0-100 calculado por NappAI
localeStringNo”es-ES” por defecto
isEmailVerifiedBooleanNoVerificación de email completada
isSuspendedBooleanNoFlag de suspensión por SUPER_ADMIN
lastActiveAtDateTime?SíÚltima actividad registrada
jobTitleString?SíCargo profesional
companyString?SíEmpresa actual
linkedinUrlString?SíURL del perfil LinkedIn
aiLevelString?Sí’explorer’ / ‘curious’ / ‘practitioner’ / ‘advanced’
goalsString[]NoObjetivos de aprendizaje del usuario
onboardingCompletedBooleanNoIndica si completó el onboarding
notificationPrefsJson?Sí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?SíUser-Agent del dispositivo
expiresAtDateTimeNoFecha de expiración del token
ipAddressString?SíIP de la sesión
cityString?SíCiudad detectada por geolocalización
countryString?SíPaís detectado por geolocalización
lastActiveAtDateTime?SíÚ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?SíDescripción larga para la página de detalle
thumbnailUrlString?SíImagen de portada
accessLevelAccessLevelNoNivel mínimo requerido para acceder
targetRolesString[]NoPúblico objetivo ([“sales”, “developer”])
targetIndustriesString[]NoIndustrias objetivo
estimatedHoursInt?SíDuración estimada en horas
isPublishedBooleanNoVisible en el catálogo
isFeaturedBooleanNoDestacado en portada
organizationIdString?Sínull = ruta global; no null = privada corporativa
difficultyLevelStringNo”Principiante” por defecto
accentColorString?Sí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?Sí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?SíDescripción opcional
accessLevelAccessLevelNoHereda del Path si no se especifica explícitamente
orderIntNoPosición dentro del Path
isPublishedBooleanNoVisible para usuarios
estimatedMinInt?Sí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?SíContenido Markdown (para MARKDOWN o MIXED)
videoUrlString?SíURL Bunny.net (para VIDEO o MIXED)
videoDurationSecInt?Sí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?Sí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?SíDescripción del recurso
fileDataBytes?SíContenido binario del archivo (≤10MB en PostgreSQL)
mimeTypeString?SíTipo MIME del archivo
fileSizeInt?SíTamaño en bytes
fileNameString?SíNombre original del archivo
externalUrlString?Sí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?SíFK a Lesson (si se adjunta a lección)
moduleIdString?SíFK a Module (si se adjunta a módulo)
pathIdString?Sí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?SíTimestamp en que se completó
reviewRequestedAtDateTime?Sí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?Sí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?SíURL del logo para co-branding
brandColorString?SíHex para co-branding de certificados
planOrgPlanNoTEAM / BUSINESS / ENTERPRISE
stripeCustomerIdString (unique)?SíCustomer de Stripe
stripeSubIdString (unique)?SíSubscription de Stripe
maxSeatsIntNoLímite de usuarios (default 5)
isActiveBooleanNoEstado activo de la cuenta
statusOrgStatusNoTRIAL / ACTIVE / SUSPENDED / CANCELLED / CHURNED
trialEndsAtDateTime?SíFecha de fin del período de prueba
contractStartAtDateTime?SíInicio del contrato
contractEndAtDateTime?SíFin del contrato
accountManagerIdString?SíID del account manager asignado
salesNotesString?SíNotas internas del equipo de ventas
sourceOrgSourceNoDEMO_REQUEST / DIRECT_SALE / PARTNER / EVENT
demoRequestIdString (unique)?SíFK a DemoRequest de origen
customPricePerSeatDecimal?SíPrecio por asiento negociado (10,2)
billingCycleBillingCycleNoMONTHLY / ANNUAL
discountPercentInt?SíDescuento en porcentaje (default 0)
industryString?SíSector de la empresa
websiteString?SíSitio web
countryString?SíPaís
companySizeString?SíTamaño de la empresa
descriptionString?SíDescripción de la organización
contactNameString?SíNombre del contacto principal
contactEmailString?SíEmail del contacto principal
contactPhoneString?SíTeléfono del contacto principal
brandPrimaryColorString?SíColor primario de marca (default “#7C3AED”)
brandSecondaryColorString?Sí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?SíDominio permitido si restrictDomain es true
sessionTimeoutHoursInt?SíTimeout de sesión en horas
autoDeactivateDaysInt?SíDías de inactividad antes de desactivar usuario
seatAutoReclaimBooleanNoReclama asientos de usuarios inactivos automáticamente
seatAlertAt90BooleanNoAlerta cuando se alcanza el 90% de asientos
ssoEntityIdString?SíEntity ID del SSO (Enterprise only)
ssoUrlString?SíURL del SSO
ssoX509CertificateString?SíCertificado X.509 para SSO
ssoDefaultRoleString?SíRol por defecto en auto-provisioning SSO
ssoAutoProvisioningBooleanNoCrea usuarios automáticamente vía SSO
notifPreferencesJson?Sí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?SíMáximo de asientos (null = ilimitado)
featuresJsonNoFeatures incluidas en el plan
stripePriceIdMonthlyString?SíPrice ID de Stripe para facturación mensual
stripePriceIdAnnualString?Sí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?Sí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?Sí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?SíÚ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?SíID del usuario que realizó la acción
actorRoleStringNoRol del actor en el momento de la acción
actionStringNoAcción realizada
targetTypeString?SíTipo de entidad afectada
targetIdString?SíID de la entidad afectada
oldValueJson?SíValor anterior
newValueJson?SíValor nuevo
ipAddressString?Sí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?Sí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?Sí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?SíFK a Lesson (si es progreso de lección)
moduleIdString?SíFK a Module (si es progreso de módulo)
pathIdString?SíFK a Path (si es progreso de ruta) — sin FK explícita
statusProgressStatusNoNOT_STARTED / IN_PROGRESS / COMPLETED
timeSpentSecIntNoTiempo total dedicado en segundos
lastPositionInt?SíPosición en vídeo (segundos) para reanudar
startedAtDateTime?SíTimestamp de inicio
completedAtDateTime?Sí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)?SíFK a Module (null = evaluación de ruta)
pathIdString (unique)?Sí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?Sí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?SíTimestamp de publicación
quizTypeStringNo’EVALUATION’ / ‘CASE_STUDY’ (default ‘EVALUATION’)
instructionsString?Sí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?SíOpciones para MULTIPLE_CHOICE: [{id, text, isCorrect, order}]
correctAnswerString?SíRespuesta correcta para TRUE_FALSE: ‘true’ / ‘false’
explanationString?SíExplicación de la respuesta correcta
orderIntNoPosición en el quiz
pointsIntNoPuntos que vale la pregunta (default 1)
gradingCriteriaString?SíCriterios de evaluación para OPEN_ENDED
gradingModeString?Sí’manual’ / ‘ai’ / ‘both’ — solo para OPEN_ENDED
aiGeneratedBooleanNoSi fue generada por IA
aiTaskIdString?SíID del AITask que originó esta pregunta
difficultyString?Sí’easy’ / ‘medium’ / ‘hard’
lessonRefString?Sí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?SíResultado de corrección (null = pendiente evaluación)
aiScoreFloat?SíPuntuación IA para respuestas abiertas (0-1)
aiFeedbackString?Sí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?Sí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?SíFK a User (se asocia si el usuario se registra después)
answersJsonNoRespuestas del test
profileJson?SíResultado del AI Fluency Test con perfil calculado
scoreFloat?SíPuntuación del test
completedAtDateTime?Sí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?SíFK a Path (puede ser nulo para badges de módulo)
moduleIdString?Sí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?Sínull = no expira
linkedInUrlString?SíURL para compartir en LinkedIn
badgeMetadataJson?SíOpen Badges 3.0 assertion JSON
blockchainTxHashString?SíHash de transacción blockchain (Fase 3)
organizationIdString?SíFK a Organization (para certificados corporativos)
pdfStorageKeyString?Sí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?SíDías de validez (null = no expira)
badgeImageUrlString?Sí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?SíURL del avatar
avatarBytes?SíBytes del avatar (almacenamiento inline)
bannerUrlString?SíURL del banner del perfil
linkedinUrlString?SíPerfil LinkedIn
websiteUrlString?SíSitio web personal
twitterUrlString?SíPerfil Twitter/X
specialtiesString[]NoEspecialidades del creador
yearsExperienceInt?SíAños de experiencia
isPublicBooleanNofalse hasta que NappAI certifique al creador
certifiedAtDateTime?SíTimestamp de certificación por NappAI
certifiedByString?Sí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?SíTimestamp de suspensión del creador
paymentDataNoteString?SíNota sobre datos de pago del creador
earningsTermsAcceptedAtDateTime?Sí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?Sí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?SíNotas internas de revisión
reviewedByString?SíID del admin que revisó
reviewedAtDateTime?SíTimestamp de revisión
statusHistoryJsonNoHistorial de cambios de estado: ApplicationEvent[]
userIdString?Sí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?SíBiografía del estudiante
locationString?SíUbicación
websiteUrlString?SíSitio web personal
twitterUrlString?SíPerfil Twitter/X
githubUrlString?Sí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?Sí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?Sí’system’ o userId del admin que concedió
grantedAtDateTimeNoTimestamp de concesión
revokedAtDateTime?SíTimestamp de revocación (null = vigente)
supersededAtDateTime?SíTimestamp en que fue superado por un tier superior
metadataJson?SíMetadatos adicionales de la concesión
assertionIdString (unique)?SíID único Open Badges (cuid por defecto)
badgeUrlString?Sí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?Sí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?SíTimestamp de revocación (null = vigente)
grantedByString?Sí’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?Sí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?Sí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?SíRazón de inelegibilidad si aplica
paymentStatusCreatorPaymentStatusNoPENDING_REVIEW / IN_REVIEW / APPROVED / PAID / ON_HOLD / REJECTED
paymentNotesString?SíNotas del proceso de pago
paymentStatusUpdatedAtDateTime?SíTimestamp del último cambio de estado de pago
paymentStatusUpdatedByString?SíID del admin que cambió el estado de pago
exportedAtDateTime?SíTimestamp de exportación para pago
exportedByString?Sí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?SíIngreso bruto del mes
stripeFeesTotalFloat?SíTotal de comisiones de Stripe
refundsTotalFloat?SíTotal de reembolsos
netRevenueFloat?SíIngreso neto (grossRevenue - fees - refunds)
creatorPoolAmountFloat?SíImporte total del pool para creadores
isLockedBooleanNoSi el período está cerrado para modificaciones
lockedAtDateTime?SíTimestamp de cierre del período
lockedByString?SíID del admin que cerró el período
adminNotesString?Sí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?Sí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?SíTimestamp de lectura
channelsString[]NoCanales de entrega (‘in_app’, ‘email’, ‘push’)
metadataJson?SíMetadatos adicionales para deduplicación y contexto
emailSentAtDateTime?SíTimestamp de envío por email
pushSentAtDateTime?Sí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?SíTimestamp del último test de conectividad
lastTestStatusString?Sí’ok’ / ‘error’ / ‘timeout’
updatedByString?Sí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?Sí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?Sí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?Sí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?SíUmbral de coste diario en USD para alertas
monthlyThresholdUsdFloat?SíUmbral de coste mensual en USD para alertas
notificationEmailString?SíEmail para notificaciones de alertas
webhookUrlString?Sí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?SíDatos adicionales del contexto de la alerta
isResolvedBooleanNoSi la alerta ha sido resuelta
resolvedByString?SíuserId del admin que resolvió la alerta
resolvedAtDateTime?Sí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?SíID del módulo/lección/ruta asociado
contextTypeString?Sí’module’ / ‘lesson’ / ‘path’
progressPctIntNoProgreso en porcentaje (default 0)
progressMsgString?SíMensaje de progreso para el usuario
errorMsgString?SíMensaje de error si falló
providerString?SíProveedor IA que ejecutó la tarea
modelString?SíModelo IA que ejecutó la tarea
tokensUsedIntNoTokens consumidos (default 0)
costUsdFloatNoCoste estimado en USD (default 0)
bullJobIdString?SíID del job en BullMQ
seenByUserBooleanNoSi el usuario ha visto la notificación de resultado
dismissedAtDateTime?SíTimestamp en que el usuario descartó la tarea
startedAtDateTime?SíTimestamp de inicio de ejecución
completedAtDateTime?SíTimestamp de finalización
launchPayloadJson?SíPayload de lanzamiento persistido
resultPayloadJson?SíOutput del modelo para el diálogo de revisión
reviewStatusAITaskReviewStatusNoPENDING_REVIEW / IN_REVIEW / ACCEPTED / REJECTED / REFINED
reviewedAtDateTime?SíTimestamp de revisión por el usuario
reviewedByString?SíuserId del usuario que revisó
parentTaskIdString?SíFK a AITask padre (para refinamientos)
refinementCountIntNoNúmero de refinamientos realizados
refinementNotesString?SíNotas del refinamiento
importedEntityIdString?SíID de la entidad creada al importar el resultado
importedEntityTypeString?Sí’quiz’ / ‘lesson’ / ‘summary’
importedAtDateTime?Sí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?Sí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?Sí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?SíTítulo de la sección de objetivos generada
objectivesBulletsJson?SíBullets de objetivos generados
summaryTitleString?SíTítulo de la sección de resumen generada
summaryBulletsJson?SíBullets del resumen generado
fromCacheBooleanNoSi el resultado vino de caché
insertedSectionsJson?SíSecciones efectivamente insertadas en la lección
insertedAtDateTime?Sí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?Sí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?SíTexto supratítulo
subtitleString?SíSubtítulo descriptivo
orderIntNoPosición en la guía
isActiveBooleanNoSi la sección está visible
updatedByString?Sí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?SíID del usuario que ejecutó la acción
actorRoleUserRole?SíRol del actor en el momento de la acción
actorEmailString?SíEmail del actor
actorIpString?SíIP del actor
actorUserAgentString?Sí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?SíTipo de entidad afectada
entityIdString?SíID de la entidad afectada
entityLabelString?SíLabel legible de la entidad afectada
beforeJson?SíEstado anterior de la entidad
afterJson?SíEstado posterior de la entidad
metadataJson?SíMetadatos adicionales del contexto
successBooleanNoSi la acción fue exitosa (default true)
errorCodeString?SíCódigo de error si falló
errorMessageString?SíMensaje de error si falló
sessionIdString?SíID de la sesión del usuario
requestIdString?SíID de la request HTTP
orgIdString?Sí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?SíDescripción de qué hace el job
categoryStringNoCategoría del job (ej. “earnings”, “metrics”)
iconString?Sí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?SíConfiguración específica del job
notesString?SíNotas internas
lastRunAtDateTime?SíTimestamp de la última ejecución
lastStatusJobStatus?SíEstado de la última ejecución
lastDurationMsInt?SíDuración de la última ejecución en ms
lastItemsCountInt?SíÍtems procesados en la última ejecución
nextRunAtDateTime?Sí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?SíTimestamp de finalización
durationMsInt?SíDuración en ms
itemsProcessedInt?SíÍtems procesados en esta ejecución
itemsSummaryString?SíResumen legible de ítems procesados
errorMessageString?Sí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?SíID del módulo de contexto
lessonIdString?Sí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?SíValoración del usuario: ‘up’ / ‘down’ / null
providerString?SíProveedor IA (ej. ‘anthropic’)
modelString?SíModelo IA (ej. ‘claude-sonnet-4-5’)
latencyMsInt?Sí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?SíMensaje adicional
sourceString?SíFuente de captación
privacyAcceptedBooleanNoAceptación de política de privacidad
marketingConsentBooleanNoConsentimiento de marketing (default false)
utmSourceString?SíUTM source del tráfico
utmMediumString?SíUTM medium del tráfico
utmCampaignString?SíUTM campaign del tráfico
referrerString?SíReferrer HTTP
originPageString?SíPágina de origen del formulario
statusDemoRequestStatusNoPENDING / CONTACTED / DEMO_SCHEDULED / DEMO_DONE / CONVERTED / DISCARDED
priorityDemoRequestPriorityNoNORMAL / HIGH_PRIORITY
internalNotesString?SíNotas internas del equipo de ventas
assignedToString?Sí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?SíComentario textual
createdAtDateTimeNoTimestamp de creación
updatedAtDateTimeNoTimestamp de última modificación

Relaciones: path → Path, user → User

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