Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/VasquezRivero92/HabboCafe/llms.txt

Use this file to discover all available pages before exploring further.

HADOS stores all tournament state in exactly two Firestore top-level collections: users and dados. Both collections use real-time onSnapshot listeners so every connected client — leaderboard viewers, intermediaries, and admins — sees updates the moment a write is committed. This page documents the document paths, listener patterns, key queries, and batch-write operations used by the application.

users collection

Document path: /users/{userId} Each document in users corresponds to one HabboUser record (see Data Models). The {userId} segment is the Firebase Authentication UID for self-registered accounts, or a Firestore auto-generated ID for players added manually by staff.

Real-time listener — single user profile

When a user signs in, their own profile is loaded and kept in sync via a targeted document listener:
import { doc, onSnapshot } from 'firebase/firestore';
import { db } from './firebase';

const userProfileRef = doc(db, 'users', user.uid);

const unsubscribe = onSnapshot(userProfileRef, (docSnap) => {
  if (docSnap.exists()) {
    const profile = { ...docSnap.data(), id: docSnap.id };
    setUserProfile(profile);
  }
});

// Call unsubscribe() when the component unmounts

Real-time listener — all users (Global Admin only)

Global Admins subscribe to the entire users collection to power the Dado assignment dropdown:
import { collection, onSnapshot } from 'firebase/firestore';

const unsubscribe = onSnapshot(collection(db, 'users'), (snapshot) => {
  const list = snapshot.docs.map((docSnap) => ({
    id: docSnap.id,
    ...docSnap.data(),
  }));
  setAllUsers(list);
});
This full-collection listener is only activated when userProfile.role === 'global_admin'. All other roles retrieve a narrower, dadoId-scoped subset (see below).

Key query — leaderboard by Dado

The leaderboard for the active Dado is fetched with a where filter on dadoId. Results are sorted client-side by points descending immediately after the snapshot fires:
import { collection, query, where, onSnapshot } from 'firebase/firestore';

const q = query(
  collection(db, 'users'),
  where('dadoId', '==', targetDadoId)
);

const unsubscribe = onSnapshot(q, (snapshot) => {
  const list = snapshot.docs.map((docSnap) => ({
    id: docSnap.id,
    ...docSnap.data(),
  }));

  // Client-side sort — highest points first
  list.sort((a, b) => b.points - a.points);
  setUsers(list);
});

Key query — participations by username

Non-admin users can participate in multiple Dados under the same Habbo nickname. The app discovers all their participation records by querying on username:
import { collection, query, where, onSnapshot } from 'firebase/firestore';

const q = query(
  collection(db, 'users'),
  where('username', '==', userProfile.username)
);

const unsubscribe = onSnapshot(q, (snapshot) => {
  const participations = snapshot.docs.map((docSnap) => ({
    id: docSnap.id,
    ...docSnap.data(),
  }));
  setMyParticipations(participations);
});
This query result feeds getActiveRole() — it finds the participation record matching the currently selected Dado and reads its role field to determine the user’s effective permissions.

Indexes

No custom composite indexes are required for the queries above. Both where('dadoId', '==', ...) and where('username', '==', ...) are single-field equality filters that Firestore resolves with auto-generated indexes.

dados collection

Document path: /dados/{dadoId} Each document in dados represents one tournament environment. The {dadoId} is always a Firestore auto-generated ID created when a Global Admin calls setDoc on a new doc(collection(db, 'dados')) reference.

Real-time listener — full collection

All authenticated users subscribe to the entire dados collection. Because the number of Dados is expected to remain small (tens, not thousands), no filtering is applied at the query level:
import { collection, onSnapshot } from 'firebase/firestore';

const unsubscribe = onSnapshot(collection(db, 'dados'), (snapshot) => {
  const dadosList = snapshot.docs.map((docSnap) => ({
    id: docSnap.id,
    ...docSnap.data(),
  }));
  setDados(dadosList);
});
After the snapshot fires, the app derives each user’s available Dados by filtering:
  • Global Admin: all Dados.
  • Others: Dados where d.adminId === currentUser.uid OR where a matching myParticipations record exists with p.dadoId === d.id.

Creating a new Dado

import { collection, doc, setDoc } from 'firebase/firestore';

const dadoDocRef = doc(collection(db, 'dados')); // auto-generated ID

await setDoc(dadoDocRef, {
  id: dadoDocRef.id,
  name: 'Dado VIP',
  createdAt: new Date().toISOString(),
});

Batch writes

HADOS uses writeBatch whenever multiple documents must change atomically. A partial write failure would leave the leaderboard in an inconsistent state, so every multi-document operation is wrapped in a batch.

Rank recalculation (points update)

Fired after every points addition or subtraction. The full user list for the active Dado is sorted, then every document is updated with its new currentRank, previousRank, and points in a single commit:
import { writeBatch, doc } from 'firebase/firestore';

const sorted = [...updatedUsers].sort((a, b) => b.points - a.points);
const batch = writeBatch(db);

sorted.forEach((user, index) => {
  const newRank = index + 1;
  const userRef = doc(db, 'users', user.id);
  batch.update(userRef, {
    points: user.points,
    previousRank: user.currentRank ?? user.previousRank,
    currentRank: newRank,
  });
});

await batch.commit();

Tournament reset

When a Dado Admin starts a new tournament, all player scores in that Dado are zeroed and the Dado’s date range is updated — all in one batch:
const batch = writeBatch(db);

// Update the Dado's dates
const dadoRef = doc(db, 'dados', activeDado.id);
batch.update(dadoRef, {
  startDate: tournamentStartDate, // 'YYYY-MM-DD'
  endDate: tournamentEndDate,     // 'YYYY-MM-DD'
});

// Reset every player's score and rank
users.forEach((u) => {
  const userRef = doc(db, 'users', u.id);
  batch.update(userRef, {
    points: 0,
    previousRank: 1,
    currentRank: 1,
  });
});

await batch.commit();

Dado Admin assignment

Assigning a Dado Admin requires updating both the Dado document (to record the admin’s UID, email, and username) and the target HabboUser document (to elevate their role and set their dadoId):
const batch = writeBatch(db);

const dadoRef = doc(db, 'dados', dadoId);
batch.update(dadoRef, {
  adminId: adminUser.id,
  adminEmail: adminUser.email,
  adminName: adminUser.username,
});

const userRef = doc(db, 'users', adminUser.id);
batch.update(userRef, {
  role: 'dado_admin',
  dadoId: dadoId,
});

await batch.commit();

Security rules recommendation

HADOS relies on Firestore Security Rules to enforce that clients cannot read or write data they are not authorised to access. The application itself does not use a server-side admin SDK, so all access control must be implemented at the rules layer. Below is a minimal recommended ruleset that mirrors the application’s permission model. Adapt it to your production requirements before deploying.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Helper: is the requesting user authenticated?
    function isAuth() {
      return request.auth != null;
    }

    // Helper: read the caller's own profile from Firestore
    function myProfile() {
      return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
    }

    // Helper: is the caller a Global Admin?
    function isGlobalAdmin() {
      return isAuth() && myProfile().role == 'global_admin';
    }

    // -------------------------------------------------------
    // /users/{userId}
    // -------------------------------------------------------
    match /users/{userId} {
      // Any authenticated user can read user documents
      // (required for leaderboard and participation queries)
      allow read: if isAuth();

      // A user can create their own profile on first sign-in
      allow create: if isAuth() && request.auth.uid == userId;

      // A user can update their own document;
      // Global Admins and Dado Admins can update any user document
      allow update: if isAuth() && (
        request.auth.uid == userId ||
        isGlobalAdmin() ||
        myProfile().role == 'dado_admin'
      );

      // Only Global Admins can delete user documents
      allow delete: if isGlobalAdmin();
    }

    // -------------------------------------------------------
    // /dados/{dadoId}
    // -------------------------------------------------------
    match /dados/{dadoId} {
      // Any authenticated user can read Dado documents
      allow read: if isAuth();

      // Only Global Admins can create new Dados
      allow create: if isGlobalAdmin();

      // Global Admins and Dado Admins (of this Dado) can update
      allow update: if isAuth() && (
        isGlobalAdmin() ||
        (myProfile().role == 'dado_admin' &&
         myProfile().dadoId == dadoId)
      );

      // Only Global Admins can delete Dados
      allow delete: if isGlobalAdmin();
    }
  }
}
The helper function myProfile() performs an extra Firestore read on every rule evaluation. In high-traffic environments, consider caching role information in Firebase Auth custom claims to reduce rule evaluation latency and cost.
Test your rules using the Firebase Emulator Suite before deploying to production. The Rules Playground in the Firebase console is useful for quick spot-checks but does not replace a full integration test suite.

Build docs developers (and LLMs) love