Overview
PARKINMX uses Firebase as its backend-as-a-service platform, handling authentication, real-time database, cloud storage, and serverless functions.
Firebase Version : 12.6.0 (client)Firebase Admin : 13.6.0 (server)Node Runtime : 24
Firebase Services Used
Authentication Email/password authentication with username support
Firestore Real-time NoSQL database for all app data
Cloud Functions Serverless backend functions (v2)
Cloud Storage Profile photos and vehicle images
Firebase Initialization
The Firebase client is initialized in the configs directory:
configs/firebaseConfig.ts
import { initializeApp } from 'firebase/app' ;
import { getAuth } from 'firebase/auth' ;
import { getFirestore } from 'firebase/firestore' ;
import { getFunctions } from 'firebase/functions' ;
import { getStorage } from 'firebase/storage' ;
const firebaseConfig = {
apiKey: process . env . EXPO_PUBLIC_FIREBASE_API_KEY ,
authDomain: process . env . EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN ,
projectId: process . env . EXPO_PUBLIC_FIREBASE_PROJECT_ID ,
storageBucket: process . env . EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET ,
messagingSenderId: process . env . EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID ,
appId: process . env . EXPO_PUBLIC_FIREBASE_APP_ID
};
const app = initializeApp ( firebaseConfig );
export const auth = getAuth ( app );
export const db = getFirestore ( app );
export const functions = getFunctions ( app );
export const storage = getStorage ( app );
Store Firebase credentials in environment variables or .env file. Never commit credentials to version control.
Authentication Setup
Email/Password Authentication
PARKINMX supports both email and username login:
import { signInWithEmailAndPassword } from "firebase/auth" ;
import { collection , query , where , getDocs } from "firebase/firestore" ;
const handleLogin = async ( identifier : string , password : string ) => {
let emailToLogin = identifier . trim ();
// If username (no @), look up email in Firestore
if ( ! identifier . includes ( "@" )) {
const q = query (
collection ( db , "users" ),
where ( "username" , "==" , identifier . trim ())
);
const snapshot = await getDocs ( q );
if ( snapshot . empty ) {
throw new Error ( "Username not found" );
}
emailToLogin = snapshot . docs [ 0 ]. data (). email ;
}
// Sign in with Firebase Auth
await signInWithEmailAndPassword ( auth , emailToLogin , password );
};
User Registration
import { createUserWithEmailAndPassword , updateProfile } from "firebase/auth" ;
import { doc , setDoc } from "firebase/firestore" ;
const handleRegister = async ( userData ) => {
// 1. Create Firebase Auth user
const userCredential = await createUserWithEmailAndPassword (
auth ,
userData . email ,
userData . password
);
// 2. Update display name
await updateProfile ( userCredential . user , {
displayName: userData . username
});
// 3. Create Firestore user document
await setDoc ( doc ( db , "users" , userCredential . user . uid ), {
username: userData . username ,
email: userData . email ,
phone: userData . phone ,
name: userData . name ,
credits_balance: 0 ,
xp: 0 ,
level: 1 ,
role: "client" ,
createdAt: new Date (),
photoURL: ""
});
};
Password Recovery
Two-step verification with EmailJS:
// 1. Send verification code via EmailJS
const code = Math . floor ( 1000 + Math . random () * 9000 ). toString ();
await emailjs . send (
'service_ov1txor' ,
'template_tn3fdwk' ,
{ to_email: userEmail , message: code },
{ publicKey: 'uwyrohVqlzvgOj1KT' }
);
// 2. After code verification, send Firebase reset link
await sendPasswordResetEmail ( auth , userEmail );
Firestore Collections
Collection Structure
firestore/
├── users/ # User profiles
│ ├── {userId}/
│ │ ├── fields # User data
│ │ └── transactions/ # Subcollection: credit history
│ │ └── {transactionId}
│ └── ...
├── reservations/ # Parking reservations
│ ├── {reservationId}/
│ └── ...
├── cards/ # Payment cards
│ ├── {cardId}/
│ └── ...
├── vehicles/ # User vehicles
│ ├── {vehicleId}/
│ └── ...
├── friend_requests/ # Friend system
│ ├── {requestId}/
│ └── ...
├── support_chats/ # Support conversations
│ ├── {userId}/
│ │ ├── fields
│ │ └── messages/ # Subcollection
│ │ └── {messageId}
│ └── ...
└── processed_payments/ # Payment idempotency
├── {paymentId}/
└── ...
Users Collection
Path : /users/{userId}
interface User {
username : string ; // Unique username
email : string ; // Email (from Auth)
name : string ; // Display name
phone : string ; // Phone number
photoURL ?: string ; // Profile photo URL
credits_balance : number ; // Available credits
xp : number ; // Experience points
level : number ; // User level (1-100)
role : 'client' | 'admin' ; // User role
createdAt : Timestamp ; // Registration date
memberSince ?: string ; // Formatted date
biometricEnabled ?: boolean ; // Biometric auth setting
pinCode ?: string ; // Hashed PIN
pushToken ?: string ; // Expo push notification token
}
Indexes Required :
username (single field, ascending)
email (single field, ascending)
xp (single field, descending) - for leaderboard
role (single field, ascending)
Example Query :
// Get top 20 users by XP
const q = query (
collection ( db , "users" ),
where ( "role" , "==" , "client" ),
orderBy ( "xp" , "desc" ),
limit ( 20 )
);
const snapshot = await getDocs ( q );
Transactions Subcollection
Path : /users/{userId}/transactions/{transactionId}
interface Transaction {
type : 'recharge' | 'reservation' | 'penalty_cancel' | 'penalty_noshow' ;
amount : number ; // Positive or negative
description : string ;
date : Timestamp ;
reservationId ?: string ; // Related reservation
}
Reservations Collection
Path : /reservations/{reservationId}
interface Reservation {
clientId : string ; // User ID
spotId : string ; // Parking spot (A1-C10)
vehicleId : string ; // Reference to vehicles collection
cardId : string ; // Reference to cards collection
startTime : Timestamp ; // Reservation start
endTime : Timestamp ; // Calculated end time
status : 'pending' | 'active' | 'completed' | 'cancelled_user' | 'inactive' ;
cost : number ; // Credits charged
createdAt : Timestamp ;
verificationCode ?: string ; // QR code data
cancelledAt ?: Timestamp ;
cancelReason ?: string ;
penaltyApplied ?: number ; // Cancellation/no-show penalty
}
Indexes Required :
clientId + status (composite)
status + startTime (composite) - for automated tasks
Real-time Listener Example :
useEffect (() => {
const q = query (
collection ( db , "reservations" ),
where ( "clientId" , "==" , auth . currentUser ?. uid ),
where ( "status" , "in" , [ "active" , "pending" ])
);
const unsubscribe = onSnapshot ( q , ( snapshot ) => {
const reservations = snapshot . docs . map ( doc => ({
id: doc . id ,
... doc . data ()
}));
setActiveReservations ( reservations );
});
return () => unsubscribe ();
}, []);
Cards Collection
Path : /cards/{cardId}
interface Card {
userId : string ; // Owner
cardNumber : string ; // Last 4 digits only
cardHolder : string ;
expiryDate : string ; // MM/YY format
cardType : 'visa' | 'mastercard' | 'amex' ;
isPrimary : boolean ; // Default payment method
addedAt : Timestamp ;
}
Never store full card numbers or CVV codes. Use tokenization services like Stripe or MercadoPago for actual payment processing.
Vehicles Collection
Path : /vehicles/{vehicleId}
interface Vehicle {
userId : string ;
brand : string ; // e.g., "Toyota"
model : string ; // e.g., "Corolla"
color : string ;
plateNumber : string ; // License plate
year : number ;
imageURL ?: string ; // Vehicle photo
addedAt : Timestamp ;
}
Friend Requests Collection
Path : /friend_requests/{requestId}
interface FriendRequest {
fromId : string ; // Sender user ID
toId : string ; // Recipient user ID
status : 'pending' | 'accepted' | 'rejected' ;
createdAt : Timestamp ;
respondedAt ?: Timestamp ;
}
Finding Friends :
// Get accepted friends (either direction)
const q1 = query (
collection ( db , "friend_requests" ),
where ( "fromId" , "==" , currentUserId ),
where ( "status" , "==" , "accepted" )
);
const q2 = query (
collection ( db , "friend_requests" ),
where ( "toId" , "==" , currentUserId ),
where ( "status" , "==" , "accepted" )
);
Support Chats Collection
Path : /support_chats/{userId}
interface SupportChat {
userId : string ;
isOpen : boolean ; // Chat active status
lastMessage : string ;
lastMessageTime : Timestamp ;
unreadCount : number ;
createdAt : Timestamp ;
}
Messages Subcollection : /support_chats/{userId}/messages/{messageId}
interface Message {
text : string ;
sender : 'user' | 'support' | 'ai' ;
timestamp : Timestamp ;
isRead : boolean ;
}
Real-time Chat :
const q = query (
collection ( db , 'support_chats' , userId , 'messages' ),
orderBy ( 'timestamp' , 'asc' )
);
const unsubscribe = onSnapshot ( q , ( snapshot ) => {
const messages = snapshot . docs . map ( doc => ({
id: doc . id ,
... doc . data ()
}));
setMessages ( messages );
});
Cloud Functions
Function Configuration
{
"functions" : [{
"source" : "functions" ,
"codebase" : "default" ,
"disallowLegacyRuntimeConfig" : true ,
"predeploy" : [
"npm --prefix \" $RESOURCE_DIR \" run lint"
]
}]
}
{
"engines" : {
"node" : "24"
},
"dependencies" : {
"firebase-admin" : "^13.6.0" ,
"firebase-functions" : "^7.0.0" ,
"mercadopago" : "^2.11.0"
}
}
Available Functions
crearPreferenciaMP (Callable)
Purpose : Create MercadoPago payment preference for credit top-upTrigger : HTTP callable from clientexports . crearPreferenciaMP = onCall ( async ( request ) => {
const { monto , uid } = request . data ;
const preference = new Preference ( client );
const result = await preference . create ({
items: [{
id: 'saldo_parkinmx' ,
title: 'Recarga de Saldo - ParkInMX' ,
quantity: 1 ,
unit_price: monto ,
currency_id: 'MXN'
}],
external_reference: uid ,
notification_url: `https://webhookmercadopago- ${ process . env . GCLOUD_PROJECT } .cloudfunctions.net/webhookMercadoPago`
});
return { success: true , url: result . init_point };
});
Client Usage :import { httpsCallable } from 'firebase/functions' ;
const crearPreferencia = httpsCallable ( functions , 'crearPreferenciaMP' );
const result = await crearPreferencia ({ monto: 100 , uid: user . uid });
window . open ( result . data . url ); // Open payment page
webhookMercadoPago (HTTP)
Purpose : Process MercadoPago payment notificationsTrigger : HTTP webhook from MercadoPagoexports . webhookMercadoPago = onRequest ( async ( req , res ) => {
const paymentId = req . query . id || req . query [ 'data.id' ];
const topic = req . query . topic || req . query . type ;
if ( topic === 'payment' && paymentId ) {
const payment = new Payment ( client );
const paymentInfo = await payment . get ({ id: paymentId });
if ( paymentInfo . status === 'approved' ) {
const userId = paymentInfo . external_reference ;
const amount = parseFloat ( paymentInfo . transaction_amount );
// Calculate credits: $1 = 6 credits + bonus
let credits = amount * 6 ;
if ( credits >= 100 ) credits += 20 ;
// Atomic transaction to add credits
await db . runTransaction ( async ( t ) => {
const paymentRef = db . collection ( 'processed_payments' ). doc ( paymentId . toString ());
const docPago = await t . get ( paymentRef );
if ( docPago . exists ) return ; // Idempotency check
// Add credits to user
t . update ( db . collection ( 'users' ). doc ( userId ), {
credits_balance: admin . firestore . FieldValue . increment ( credits )
});
// Mark payment as processed
t . set ( paymentRef , {
processedAt: admin . firestore . FieldValue . serverTimestamp (),
uid: userId ,
amount ,
credits
});
});
}
}
res . status ( 200 ). send ( "OK" );
});
cancelarReservaCliente (Callable)
Purpose : Cancel reservation with late cancellation penaltyLogic :
Free cancellation if more than 1 hour before reservation
100 credit penalty if less than 1 hour before
exports . cancelarReservaCliente = onCall ( async ( request ) => {
const { reservaId } = request . data ;
const uid = request . auth . uid ;
const COSTO_CANCELACION_TARDIA = 100 ;
return db . runTransaction ( async ( t ) => {
const reservaRef = db . collection ( 'reservations' ). doc ( reservaId );
const reserva = ( await t . get ( reservaRef )). data ();
// Check ownership and status
if ( reserva . clientId !== uid ) {
throw new HttpsError ( 'permission-denied' , 'Not your reservation' );
}
if ( reserva . status !== 'pending' ) {
throw new HttpsError ( 'failed-precondition' , 'Cannot cancel' );
}
// Calculate penalty
const now = Date . now ();
const startTime = reserva . startTime . toDate (). getTime ();
const hoursUntil = ( startTime - now ) / ( 1000 * 60 * 60 );
const penalty = hoursUntil < 1 ? COSTO_CANCELACION_TARDIA : 0 ;
// Update reservation
t . update ( reservaRef , {
status: 'cancelled_user' ,
penaltyApplied: penalty ,
cancelledAt: admin . firestore . Timestamp . now ()
});
// Deduct penalty if applicable
if ( penalty > 0 ) {
t . update ( db . collection ( 'users' ). doc ( uid ), {
credits_balance: admin . firestore . FieldValue . increment ( - penalty )
});
// Log transaction
const transRef = db . collection ( 'users' ). doc ( uid ). collection ( 'transactions' ). doc ();
t . set ( transRef , {
type: 'penalty_cancel' ,
amount: - penalty ,
description: 'Multa por cancelación tardía (< 1 hora)' ,
date: admin . firestore . Timestamp . now (),
reservationId: reservaId
});
}
return { success: true , penalty };
});
});
vigilarNoShow (Scheduled)
Purpose : Automatically cancel no-show reservations and charge penaltySchedule : Every 10 minutesLogic :
Find reservations with status=“pending” and startTime >15 minutes ago
Change status to “inactive”
Charge 100 credit penalty
exports . vigilarNoShow = onSchedule ({
schedule: "every 10 minutes" ,
timeZone: "America/Mexico_City"
}, async ( event ) => {
const MINUTOS_TOLERANCIA = 15 ;
const MULTA = 100 ;
const limitTime = new Date ( Date . now () - MINUTOS_TOLERANCIA * 60 * 1000 );
const timestampLimit = admin . firestore . Timestamp . fromDate ( limitTime );
const snapshot = await db . collection ( 'reservations' )
. where ( 'status' , '==' , 'pending' )
. where ( 'startTime' , '<=' , timestampLimit )
. get ();
if ( snapshot . empty ) return ;
const batch = db . batch ();
snapshot . forEach ( doc => {
const reserva = doc . data ();
const uid = reserva . clientId ;
// Cancel reservation
batch . update ( doc . ref , {
status: 'inactive' ,
cancelReason: `No Show ( ${ MINUTOS_TOLERANCIA } min tolerancia)` ,
penaltyApplied: MULTA ,
cancelledAt: admin . firestore . Timestamp . now ()
});
// Charge penalty
batch . update ( db . collection ( 'users' ). doc ( uid ), {
credits_balance: admin . firestore . FieldValue . increment ( - MULTA )
});
// Log transaction
const transRef = db . collection ( 'users' ). doc ( uid ). collection ( 'transactions' ). doc ();
batch . set ( transRef , {
type: 'penalty_noshow' ,
amount: - MULTA ,
description: 'Multa automática por no llegar' ,
date: admin . firestore . Timestamp . now (),
reservationId: doc . id
});
});
await batch . commit ();
console . log ( `👮 Cancelled ${ snapshot . size } no-show reservations` );
});
Security Rules
Always implement Firestore Security Rules to protect your data:
rules_version = '2' ;
service cloud . firestore {
match / databases / { database } / documents {
// Users can read their own profile, anyone can read public fields
match / users / { userId } {
allow read : if request . auth != null ;
allow write : if request . auth . uid == userId ;
// User's private transactions
match / transactions / { transactionId } {
allow read , write : if request . auth . uid == userId ;
}
}
// Reservations: users can only access their own
match / reservations / { reservationId } {
allow read : if request . auth != null &&
resource . data . clientId == request . auth . uid ;
allow create : if request . auth != null &&
request . resource . data . clientId == request . auth . uid ;
allow update , delete : if request . auth != null &&
resource . data . clientId == request . auth . uid ;
}
// Cards: users can only manage their own
match / cards / { cardId } {
allow read , write : if request . auth != null &&
resource . data . userId == request . auth . uid ;
}
// Vehicles: users can only manage their own
match / vehicles / { vehicleId } {
allow read , write : if request . auth != null &&
resource . data . userId == request . auth . uid ;
}
// Friend requests: visible to sender and recipient
match / friend_requests / { requestId } {
allow read : if request . auth != null && (
resource . data . fromId == request . auth . uid ||
resource . data . toId == request . auth . uid
);
allow create : if request . auth != null &&
request . resource . data . fromId == request . auth . uid ;
allow update : if request . auth != null &&
resource . data . toId == request . auth . uid ;
}
// Support chats: users can only access their own
match / support_chats / { userId } {
allow read , write : if request . auth . uid == userId ;
match / messages / { messageId } {
allow read , write : if request . auth . uid == userId ;
}
}
}
}
Best Practices
Use Transactions Always use Firestore transactions for operations that modify multiple documents or require consistency checks.
Implement Indexes Create composite indexes for complex queries. Firebase will prompt you with a link when needed.
Cleanup Listeners Always return unsubscribe functions from onSnapshot in useEffect cleanup.
Handle Errors Wrap Firebase calls in try-catch and show user-friendly error messages.
Environment Variables
Create a .env file in the project root:
# Firebase Configuration
EXPO_PUBLIC_FIREBASE_API_KEY = your_api_key
EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN = your_project.firebaseapp.com
EXPO_PUBLIC_FIREBASE_PROJECT_ID = your_project_id
EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET = your_project.appspot.com
EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID = your_sender_id
EXPO_PUBLIC_FIREBASE_APP_ID = your_app_id
# EmailJS (for password recovery)
EXPO_PUBLIC_EMAILJS_SERVICE_ID = service_ov1txor
EXPO_PUBLIC_EMAILJS_TEMPLATE_ID = template_tn3fdwk
EXPO_PUBLIC_EMAILJS_PUBLIC_KEY = uwyrohVqlzvgOj1KT
# Google AI (for support chat)
EXPO_PUBLIC_GEMINI_API_KEY = your_gemini_key
Deployment
Install Firebase CLI
npm install -g firebase-tools
Initialize Project
Select Firestore, Functions, and Storage.
Deploy Functions
firebase deploy --only functions
Deploy Rules
firebase deploy --only firestore:rules
Functions deployment can take 2-5 minutes. Monitor deployment status in the Firebase Console.