Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Stivenz3/Nexu/llms.txt
Use this file to discover all available pages before exploring further.
Nexu’s Firestore database is organized into six logical collections. Three of them — lessons, blocks, and questions — are write-once content loaded by seed scripts and read-only for the application. The other three — users, certificates, and certificatePublic — are written by the application as users progress through the course and earn their certification.
Content collections (lessons, lessons/{id}/blocks, lessons/{id}/questions) are populated by seed scripts run from a developer terminal (npm run seed:lesson1). The React application never writes to these collections. Progress and certificate collections are written exclusively by the running application.
Collection Reference
lessons/{lessonId}
Top-level lesson document. Defined by the LessonDoc interface in src/types/lesson.ts. The lessonId is used as the Firestore document ID (e.g., lesson_01_higiene_personal).
| Field | Type | Description |
|---|
lessonId | string | Document ID, e.g. lesson_01_higiene_personal |
title | string | Human-readable lesson title |
order | number | Position in the learning path (1-based) |
normativeBase | string | Normative reference, e.g. "Resolución 2674 de 2013, Cap. III" |
passingScore | number | Minimum passing score for the final exam (e.g. 70) |
estimatedMinutes | number | Estimated time to complete the lesson |
isActive | boolean | Whether the lesson appears in the learning path |
totalBlocks | number | Total number of blocks in this lesson |
Composite index (defined in firestore.indexes.json):
{
"collectionGroup": "lessons",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "isActive", "order": "ASCENDING" },
{ "fieldPath": "order", "order": "ASCENDING" }
]
}
This index is used by fetchActiveLessons() in lessonService.ts, which queries where('isActive', '==', true) + orderBy('order', 'asc').
lessons/{lessonId}/blocks/{blockId}
Subcollection of content blocks within a lesson. Defined by LessonBlock in src/types/lesson.ts. Blocks are fetched in order sequence; the app enforces linear progression — a block is only accessible once the previous block is marked completed.
| Field | Type | Description |
|---|
blockId | string | Document ID, e.g. b01_video, b02_modulo_a |
type | string | One of: video, theory, game, exam |
order | number | Display order within the lesson (1-based) |
title | string | Block title shown in the lesson map |
isRequired | boolean | Whether the block must be completed before the exam unlocks |
imageUrl | string? | Optional header image URL |
imageAlt | string? | Alt text for the header image |
content | object | Type-specific content object (see block types below) |
Block content shapes by type
video — VideoBlockContent
| Field | Type | Description |
|---|
youtubeUrl | string | YouTube embed URL |
minWatchPercent | number | Minimum watch percentage to mark block complete |
durationSeconds | number | Video duration in seconds |
theory — TheoryBlockContent
| Field | Type | Description |
|---|
normativeRef | string | Regulation article referenced by this module |
summary | string | Short summary paragraph |
keyPoints | string[] | Bullet list of key takeaways |
technicalNote | string | null | Optional technical callout |
hasInterludedQuestion | boolean | Whether an inline question interrupts reading |
questionId | string | null | Single inline question ID (legacy) |
questionIds | string[] | null | Multiple inline question IDs |
game — GameBlockContent
| Field | Type | Description |
|---|
sceneName | string | Identifier of the 3-D scene |
scenario | string | Scenario description shown to the user |
objective | string | Game objective text |
totalErrors | number | Number of planted errors to find |
errors | GameError[] | Array of { errorId, description, normativeRef, feedbackText } |
scoring.baseScore | number | Score before penalties |
scoring.penaltyPerHint | number | Points deducted per hint used |
scoring.penaltyPerWrongClick | number | Points deducted per wrong click |
scoring.passingScore | number | Minimum score to pass the game block |
timeLimitSeconds | number | Time limit for the game |
unityGamePath | string? | Optional path to the Unity WebGL build directory |
unityBuildName | string? | Optional Unity build name identifier |
exam — ExamBlockContent
| Field | Type | Description |
|---|
totalQuestions | number | Number of questions drawn from the pool |
passingPercent | number | Minimum percentage correct to pass |
randomize | boolean | Whether question order is shuffled |
showExplanationAfterAnswer | boolean | Show per-question explanation immediately |
unlimitedAttempts | boolean | Whether retakes are allowed |
lessons/{lessonId}/questions/{questionId}
Subcollection of exam and inline questions. Defined by LessonQuestion in src/types/lesson.ts. Questions with questionType === 'evaluacion_final' are drawn for the final exam; those with questionType === 'intercalada' appear inline within theory blocks.
| Field | Type | Description |
|---|
questionId | string | Document ID, e.g. q_p1i, q_e01 |
blockRef | string | blockId of the block this question belongs to |
questionType | string | intercalada (inline) or evaluacion_final (final exam pool) |
order | number | Sort order within its block |
text | string | Question stem |
options | array | Array of { label, text, isCorrect, feedback? } answer choices |
explanation | string | Explanation shown after the user answers |
normativeRef | string | Regulation article this question tests |
difficulty | string | basica, intermedia, or avanzada |
Each option in options has this shape:
| Field | Type | Description |
|---|
label | string | Option letter, e.g. "A" |
text | string | Option text |
isCorrect | boolean | Whether this option is the correct answer |
feedback | string? | Why this option is wrong (falls back to explanation if absent) |
users/{userId}
User profile document created at registration by AuthContext.register(). The document ID is the Firebase Auth UID.
Fields written at registration by AuthContext.tsx:
| Field | Type | Description |
|---|
fullName | string | User’s full name |
email | string | Email address |
documentType | string | cc, ce, or passport |
documentNumber | string | Government ID number |
createdAt | Timestamp | Server timestamp at registration |
users/{userId}/lessonProgress/{lessonId}
One document per lesson the user has started. Written and updated by progressService.ts. The document ID matches the lessonId.
| Field | Type | Description |
|---|
lessonId | string | Lesson identifier |
status | string | locked, in_progress, passed, or failed |
startedAt | Timestamp | Server timestamp when the user first opened the lesson |
completedAt | Timestamp | null | Server timestamp when the user passed; null until then |
lastBlockCompleted | string | blockId of the most recently completed block |
totalTimeSpentSec | number | Cumulative time in seconds across all blocks |
blocksProgress | object | Map of blockId → BlockProgressEntry (see below) |
evalAttempts | number[] | Array of every exam score, in attempt order |
bestEvalScore | number | Highest exam score achieved across all attempts |
blocksProgress entry shapes
Each key in blocksProgress is a blockId. The value shape depends on the block type:
Video block progress
| Field | Type |
|---|
completed | boolean |
videoWatched | boolean |
watchedPct | number |
Theory block progress
| Field | Type |
|---|
completed | boolean |
questionAnswered | boolean? |
answeredCorrect | boolean? |
questionsAnswered | boolean[]? |
Game block progress
| Field | Type |
|---|
completed | boolean |
gameScore | number |
errorsFound | number |
hintsUsed | number |
timeSpentSec | number |
Exam block progress
| Field | Type |
|---|
completed | boolean |
lastScore | number |
attempts | number |
passed | boolean |
certificates/{certificateId}
Private certificate record. Only the certificate owner (userId == request.auth.uid) can read it. Defined by CertificateRecord in src/types/certificate.ts.
The certificateId is deterministic: cert_{userId[0..8]}_nexu_bpm_curso.
| Field | Type | Description |
|---|
certificateId | string | Document ID |
userId | string | Firebase Auth UID of the certificate owner |
lessonId | string? | Deprecated. Legacy per-lesson ID; new certificates use courseId instead |
courseId | string | nexu_bpm_curso — identifies the full course |
companyId | string | null | Reserved for enterprise accounts; currently null |
userName | string | Full name as entered at registration |
userDocument | string | Full government ID — private, never exposed publicly |
lessonTitle | string | Display title shown on the certificate |
courseName | string | "Buenas Prácticas de Manufactura" |
finalScore | number | Average score across all completed lessons |
completedLessons | CompletedLessonSnapshot[]? | Snapshot of each passed lesson: { lessonId, lessonTitle, finalScore } |
verifyCode | string | Public verification code, e.g. NX-AB12CD34EF |
qrVerifyUrl | string | Full URL to /verificar/{verifyCode} |
issuedAt | Timestamp | Issue date |
expiresAt | Timestamp | Expiry date (1 year after issuedAt) |
isValid | boolean | true when valid; false when revoked (future feature) |
Composite index (defined in firestore.indexes.json):
{
"collectionGroup": "certificates",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "userId", "order": "ASCENDING" },
{ "fieldPath": "issuedAt", "order": "DESCENDING" }
]
}
A certificate is issued only when all 8 lessons of the program (NEXU_COURSE_TOTAL_LESSONS = 8) are published (isActive: true) and marked passed in the user’s progress. Passing a single lesson does not issue a certificate — tryIssueCourseCertificateAfterLesson() in certificateService.ts checks this condition after every exam.
certificatePublic/{verifyCode}
Publicly readable record — no authentication required. The document ID is the verifyCode itself (e.g., NX-AB12CD34EF). This collection powers the QR code verification page at /verificar/:code. Defined by CertificatePublicRecord in src/types/certificate.ts.
| Field | Type | Description |
|---|
certificateId | string | Reference back to the private certificates document |
userId | string | Owner UID |
userName | string | Full name (public) |
userDocumentMasked | string | Masked ID, e.g. CC. ***6789 |
lessonTitle | string | Certificate display title |
courseName | string | "Buenas Prácticas de Manufactura" |
finalScore | number | Average score across completed lessons |
completedLessons | CompletedLessonSnapshot[]? | Passed lesson snapshots |
issuedAt | Timestamp | Issue date |
expiresAt | Timestamp | Expiry date |
isValid | boolean | Validity flag |
status | string | valid, expired, or revoked |
The masked document field is generated in certificateService.ts as:
function maskDocument(documentType: string, documentNumber: string): string {
const tail = documentNumber.slice(-4)
return `${documentType.toUpperCase()}. ***${tail}`
}
Write Pattern Summary
| Collection | Written by | Read by |
|---|
lessons | Seed scripts (terminal) | App — all authenticated users |
lessons/.../blocks | Seed scripts (terminal) | App — all authenticated users |
lessons/.../questions | Seed scripts (terminal) | App — all authenticated users |
users/{uid} | AuthContext.register() | Owner only |
users/{uid}/lessonProgress/{id} | progressService.ts | Owner only |
certificates/{id} | certificateService.ts | Owner only |
certificatePublic/{code} | certificateService.ts | Everyone (no auth required) |