Skip to main content

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 security model has three layers working together: Firebase Auth manages user identity and issues session tokens, reCAPTCHA (verified server-side via a Vercel Edge Function) protects the registration form from automated abuse, and Firestore Security Rules enforce exactly which documents each authenticated user can read or write. No layer is optional — removing any one of them creates a gap.

Firebase Auth

Nexu uses Firebase Auth’s email/password provider exclusively. There is no OAuth (Google, GitHub, etc.) and no phone auth.

AuthContext

src/contexts/AuthContext.tsx wraps the entire application in an AuthProvider and exposes authentication state via the useAuth() hook. The context value shape is:
interface AuthContextType {
  user: User | null        // Nexu User profile (fullName, documentType, etc.)
  isAuthenticated: boolean // true when user != null
  isLoading: boolean       // true while onAuthStateChanged hasn't resolved yet
  login: (email: string, password: string) => Promise<boolean>
  register: (userData: RegisterData) => Promise<boolean>
  logout: () => void
}

Auth Lifecycle

1

App mounts

AuthProvider calls onAuthStateChanged(auth, ...). isLoading is true.
2

Firebase resolves the session

If a valid session exists, isLoading becomes false and isAuthenticated becomes true with a partial user object (from Firebase Auth). A Firestore read then enriches the user with profile fields (documentType, documentNumber, etc.) from users/{uid}.
3

No session

user is set to null, isAuthenticated is false, isLoading is false. ProtectedRoute redirects to /login.
Setting a partial user object before the Firestore read completes ensures that ProtectedRoute does not flash-redirect authenticated users to /login on a hard page refresh.

Registration

AuthContext.register() performs three operations in sequence:
  1. createUserWithEmailAndPassword — creates the Firebase Auth account.
  2. updateProfile — sets displayName to the user’s full name.
  3. setDoc(doc(db, 'users', uid), { ... }) — writes the profile document to Firestore, including documentType, documentNumber, and a server timestamp.
const register = async (userData: RegisterData): Promise<boolean> => {
  const credentials = await createUserWithEmailAndPassword(
    auth, userData.email, userData.password
  )
  await updateProfile(credentials.user, { displayName: userData.fullName })
  await setDoc(doc(db, 'users', credentials.user.uid), {
    fullName: userData.fullName,
    email: userData.email,
    documentType: normalizeDocumentType(userData.documentType),
    documentNumber: userData.documentNumber,
    createdAt: serverTimestamp(),
  })
  return true
}

reCAPTCHA Bot Protection

The registration form integrates Google reCAPTCHA v3 (score-based, invisible to users). Because the reCAPTCHA secret key must never appear in frontend code, verification is delegated to the Vercel Edge Function at api/verify-recaptcha.ts.

How it works

  1. The browser calls the reCAPTCHA JS API to obtain a one-time token.
  2. The frontend POSTs { token } to /api/verify-recaptcha.
  3. The Edge Function reads RECAPTCHA_SECRET_KEY from the Vercel environment and calls https://www.google.com/recaptcha/api/siteverify.
  4. The function returns { success: boolean, score: number | null, errors: string[] | null } to the browser.
The full Edge Function (api/verify-recaptcha.ts):
export const config = { runtime: 'edge' }

export default async function handler(request: Request): Promise<Response> {
  if (request.method !== 'POST') {
    return json({ success: false, error: 'Method not allowed' }, 405)
  }

  let token = ''
  try {
    const body = (await request.json()) as { token?: string }
    token = typeof body.token === 'string' ? body.token : ''
  } catch {
    return json({ success: false }, 400)
  }

  if (!token) {
    return json({ success: false }, 400)
  }

  const secret = process.env.RECAPTCHA_SECRET_KEY
  if (!secret) {
    return json({ success: false, error: 'RECAPTCHA_SECRET_KEY no configurada' }, 500)
  }

  const params = new URLSearchParams()
  params.set('secret', secret)
  params.set('response', token)

  const verifyRes = await fetch('https://www.google.com/recaptcha/api/siteverify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString(),
  })

  const data = (await verifyRes.json()) as {
    success: boolean
    score?: number
    challenge_ts?: string
    hostname?: string
    'error-codes'?: string[]
  }

  return json({
    success: data.success,
    score: data.score ?? null,
    errors: data['error-codes'] ?? null,
  })
}

function json(body: Record<string, unknown>, status = 200) {
  return new Response(JSON.stringify(body), {
    status,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
  })
}
The RECAPTCHA_SECRET_KEY environment variable must be configured in the Vercel project settings, not in .env.local. It must not use the VITE_ prefix — any variable prefixed with VITE_ is bundled into the JavaScript served to browsers.

Protected Routes

ProtectedRoute in src/App.tsx guards all authenticated pages. It reads isAuthenticated and isLoading from AuthContext:
function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading } = useAuth()

  if (isLoading) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-surface">
        <div className="animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent" />
      </div>
    )
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />
  }

  return <>{children}</>
}
While isLoading is true, a spinner is rendered instead of redirecting. Once Firebase resolves the session, the component either renders the protected content or redirects to /login.

Firestore Security Rules

Security Rules are the server-side enforcement layer for all Firestore reads and writes. They are deployed separately from the application and take effect immediately after deployment — a Vercel build does not update rules. The complete firestore.rules file:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;

      match /lessonProgress/{lessonId} {
        allow read, write: if request.auth != null && request.auth.uid == userId;
      }
    }

    match /lessons/{lessonId} {
      allow read: if request.auth != null;

      match /blocks/{blockId} {
        allow read: if request.auth != null;
      }

      match /questions/{questionId} {
        allow read: if request.auth != null;
      }
    }

    match /certificates/{certificateId} {
      allow read: if request.auth != null && resource.data.userId == request.auth.uid;
      allow create: if request.auth != null
        && request.resource.data.userId == request.auth.uid
        && request.resource.data.certificateId == certificateId
        && request.resource.data.isValid == true;
      allow update, delete: if false;
    }

    match /certificatePublic/{verifyCode} {
      allow read: if true;
      allow create: if request.auth != null
        && request.resource.data.userId == request.auth.uid;
      allow update, delete: if false;
    }

    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Rule-by-Rule Explanation

users/{userId} and users/{userId}/lessonProgress/{lessonId}

allow read, write: if request.auth != null && request.auth.uid == userId;
Both the user profile document and the entire lessonProgress subcollection are owner-only. An authenticated user can only read and write documents where the path userId segment matches their own Firebase Auth UID. No user can access another user’s profile or progress.

lessons/{lessonId}, blocks/{blockId}, questions/{questionId}

allow read: if request.auth != null;
Lesson content is read-only for all authenticated users. Any signed-in user can read any lesson, block, or question. There is no write rule — the application never writes to these collections, and no client-side write is possible.

certificates/{certificateId}

allow read: if request.auth != null && resource.data.userId == request.auth.uid;
allow create: if request.auth != null
  && request.resource.data.userId == request.auth.uid
  && request.resource.data.certificateId == certificateId
  && request.resource.data.isValid == true;
allow update, delete: if false;
The private certificate document has three distinct rules:
  • Read: owner only (resource.data.userId == request.auth.uid).
  • Create: the authenticated user can only create a certificate for themselves, the certificateId field in the document must match the Firestore document ID, and isValid must be true on creation.
  • Update / Delete: permanently denied (if false). Certificates are immutable once issued. Revocation (a future feature) will require a Cloud Function with admin privileges.

certificatePublic/{verifyCode}

allow read: if true;
allow create: if request.auth != null
  && request.resource.data.userId == request.auth.uid;
allow update, delete: if false;
The public verification record is readable by anyone — no authentication required. This is intentional: the QR code at /verificar/:code must work for employers and inspectors who are not registered in Nexu. Creation is restricted to the authenticated user who owns the certificate. Update and delete are denied.

Catch-all deny

match /{document=**} {
  allow read, write: if false;
}
Any collection or document not matched by an explicit rule above is denied for all operations. This is the secure default that prevents accidental data exposure if new collections are added to Firestore without corresponding rules.

Deploying Updated Rules

Changes to firestore.rules only take effect after they are explicitly deployed to Firebase. Merging or deploying the React app to Vercel does not update Firestore rules. You must run the deploy command separately.
To deploy updated security rules:
npm run firebase:deploy:rules
This runs firebase deploy --only firestore:rules against the nexu-156ce Firebase project. After deployment, the new rules are active within seconds for all users in production.
After editing firestore.rules, always test the new rules locally with the Firebase Emulator Suite before deploying to production. The emulator lets you run the full rule set against simulated reads and writes without touching the shared Firebase project.

Build docs developers (and LLMs) love