Database Structure
S-Parking uses Firestore in Native Mode with three primary collections:
parking_spots Real-time status of all parking spots
parking_zones Zone definitions and metadata
occupancy_history Hourly snapshots for analytics
Collection: parking_spots
Stores real-time state of each parking spot. Document ID is the spot identifier (e.g., A-01, I-16).
Document Schema
Uppercase spot identifier (e.g., A-01, I-16). Same as document ID.
Latitude coordinate (e.g., -33.4489).
Longitude coordinate (e.g., -70.6693).
Current status:
0 = Occupied (vehicle detected)
1 = Available (free)
2 = Reserved (user reservation active)
Human-readable description (e.g., Puesto A-01).
Foreign key to parking_zones collection (e.g., zone_1764307623391).
Server timestamp of last status change. Updated via FieldValue.serverTimestamp().
Present only when status = 2. Contains: Vehicle license plate number.
Reservation expiration time.
Reservation duration in minutes.
Document creation timestamp.
Example Document
{
"id" : "A-01" ,
"lat" : -33.4489 ,
"lng" : -70.6693 ,
"status" : 2 ,
"desc" : "Puesto A-01" ,
"zone_id" : "zone_1764307623391" ,
"last_changed" : { "_seconds" : 1709673600 , "_nanoseconds" : 0 },
"reservation_data" : {
"license_plate" : "ABCD12" ,
"expires_at" : { "_seconds" : 1709677200 , "_nanoseconds" : 0 },
"duration" : 60
},
"created_at" : { "_seconds" : 1709670000 , "_nanoseconds" : 0 },
"updated_at" : { "_seconds" : 1709673600 , "_nanoseconds" : 0 }
}
Status State Machine
Firestore Operations
Create Spot
Reserve Spot
Query by Zone
await firestore . collection ( 'parking_spots' ). doc ( 'A-01' ). set ({
id: 'A-01' ,
lat: - 33.4489 ,
lng: - 70.6693 ,
desc: 'Puesto A-01' ,
status: 1 ,
zone_id: 'zone_1764307623391' ,
created_at: new Date (),
updated_at: new Date ()
}, { merge: true });
await firestore . runTransaction ( async ( t ) => {
const doc = await t . get ( docRef );
const data = doc . data ();
if ( data . status === 0 ) {
throw new Error ( "Spot occupied" );
}
if ( data . status === 2 ) {
throw new Error ( "Already reserved" );
}
const expirationTime = new Date ();
expirationTime . setMinutes ( expirationTime . getMinutes () + 60 );
t . update ( docRef , {
status: 2 ,
last_changed: FieldValue . serverTimestamp (),
reservation_data: {
license_plate: 'ABCD12' ,
expires_at: expirationTime ,
duration: 60
}
});
});
const snapshot = await firestore
. collection ( 'parking_spots' )
. where ( 'zone_id' , '==' , 'zone_1764307623391' )
. get ();
snapshot . forEach ( doc => {
console . log ( doc . id , doc . data ());
});
Collection: parking_zones
Stores parking zone definitions. Document ID is the zone identifier (e.g., zone_1764307623391).
Document Schema
Unique zone identifier (e.g., zone_1764307623391).
Display name (e.g., Federico Froebel, Interior DUOC).
Sort order for display (ascending). Used in UI to order zone tabs.
Color identifier for UI (e.g., blue, green, red).
Last modification timestamp.
Example Document
{
"id" : "zone_1764307623391" ,
"name" : "Federico Froebel" ,
"order" : 1 ,
"desc" : "Estacionamiento exterior en calle Federico Froebel" ,
"color" : "blue" ,
"created_at" : { "_seconds" : 1764307623 , "_nanoseconds" : 391000000 },
"updated_at" : { "_seconds" : 1764307623 , "_nanoseconds" : 391000000 }
}
Firestore Operations
Create Zone
Update Zone
Get Zones (Ordered)
const zoneId = `zone_ ${ Date . now () } ` ;
await firestore . collection ( 'parking_zones' ). doc ( zoneId ). set ({
id: zoneId ,
name: 'Nueva Zona' ,
order: 999 ,
desc: 'Descripción de la zona' ,
color: 'blue' ,
created_at: new Date (),
updated_at: new Date ()
});
const updateData = {
name: 'Nombre Actualizado' ,
updated_at: new Date ()
};
if ( order !== undefined ) updateData . order = order ;
if ( desc !== undefined ) updateData . desc = desc ;
if ( color !== undefined ) updateData . color = color ;
await firestore . collection ( 'parking_zones' ). doc ( zoneId ). update ( updateData );
const snapshot = await firestore
. collection ( 'parking_zones' )
. orderBy ( 'order' , 'asc' )
. get ();
const zones = [];
snapshot . forEach ( doc => zones . push ( doc . data ()));
Collection: occupancy_history
Stores hourly snapshots of parking occupancy for analytics. Document ID is the hour key (e.g., 2026-03-05-14).
Document Schema
Unique hour identifier in format YYYY-MM-DD-HH (UTC). Example: 2026-03-05-14.
ISO 8601 timestamp for audit purposes (e.g., 2026-03-05T14:00:00.000Z).
Unix timestamp in milliseconds. Used for efficient range queries.
Global occupancy statistics: Number of available spots.
Number of occupied spots.
Number of reserved spots.
Occupancy percentage: (occupied + reserved) / total * 100, rounded to integer.
Map of zone statistics. Keys are zone IDs, values are objects with: Zone occupancy percentage.
Snapshot creation timestamp.
Example Document
{
"hour_key" : "2026-03-05-14" ,
"timestamp" : "2026-03-05T14:00:00.000Z" ,
"ts" : 1709643600000 ,
"global" : {
"free" : 8 ,
"occupied" : 25 ,
"reserved" : 3 ,
"total" : 36 ,
"occupancyPct" : 78
},
"zones" : {
"zone_1764307623391" : {
"free" : 5 ,
"occupied" : 13 ,
"reserved" : 2 ,
"total" : 20 ,
"occupancyPct" : 75
},
"zone_1764306251630" : {
"free" : 3 ,
"occupied" : 12 ,
"reserved" : 1 ,
"total" : 16 ,
"occupancyPct" : 81
}
},
"created_at" : { "_seconds" : 1709643600 , "_nanoseconds" : 0 }
}
Snapshot Generation Logic
function buildHourKey ( date = new Date ()) {
const y = date . getUTCFullYear ();
const m = String ( date . getUTCMonth () + 1 ). padStart ( 2 , '0' );
const d = String ( date . getUTCDate ()). padStart ( 2 , '0' );
const h = String ( date . getUTCHours ()). padStart ( 2 , '0' );
return ` ${ y } - ${ m } - ${ d } - ${ h } ` ;
}
function addStatusCounts ( acc , status ) {
if ( ! acc . total ) acc . total = 0 ;
acc . total += 1 ;
if ( status === 1 ) acc . free = ( acc . free || 0 ) + 1 ;
else if ( status === 0 ) acc . occupied = ( acc . occupied || 0 ) + 1 ;
else if ( status === 2 ) acc . reserved = ( acc . reserved || 0 ) + 1 ;
}
Firestore Operations
Query Last 7 Days
Save Hourly Snapshot
Data Retention (30 days)
const days = 7 ;
const startTs = Date . now () - days * 24 * 60 * 60 * 1000 ;
const snapshot = await firestore
. collection ( 'occupancy_history' )
. where ( 'ts' , '>=' , startTs )
. orderBy ( 'ts' , 'asc' )
. get ();
const samples = [];
snapshot . forEach ( doc => samples . push ( doc . data ()));
const hourKey = buildHourKey ( new Date ());
const docData = {
hour_key: hourKey ,
timestamp: new Date (). toISOString (),
ts: Date . now (),
global: { /* computed stats */ },
zones: { /* computed per-zone stats */ },
created_at: new Date ()
};
await firestore
. collection ( 'occupancy_history' )
. doc ( hourKey )
. set ( docData , { merge: true });
const cutoffMs = Date . now () - 30 * 24 * 60 * 60 * 1000 ;
const oldQuery = await firestore
. collection ( 'occupancy_history' )
. where ( 'ts' , '<' , cutoffMs )
. get ();
if ( ! oldQuery . empty ) {
const batch = firestore . batch ();
oldQuery . docs . forEach ( doc => batch . delete ( doc . ref ));
await batch . commit ();
console . log ( `Deleted ${ oldQuery . size } old snapshots` );
}
Indexes
Required Composite Indexes
occupancy_history Fields:
ts (Ascending)
zone_id (Ascending)
Used for time-range queries filtered by zone.
parking_spots Fields:
zone_id (Ascending)
status (Ascending)
Used for zone-filtered status queries.
Single-field Indexes (Automatic)
parking_spots.status
parking_spots.zone_id
parking_zones.order
occupancy_history.ts
Transaction Patterns
S-Parking uses Firestore transactions for atomic operations that require read-modify-write consistency.
Reserve Parking Spot Transaction
await firestore . runTransaction ( async ( t ) => {
const doc = await t . get ( docRef );
if ( ! doc . exists ) {
throw new Error ( "Spot does not exist" );
}
const data = doc . data ();
// Validate current state
if ( data . status === 0 ) {
throw new Error ( "Spot occupied by vehicle" );
}
if ( data . status === 2 ) {
throw new Error ( "Spot already reserved" );
}
// Calculate expiration
const expirationTime = new Date ();
expirationTime . setMinutes ( expirationTime . getMinutes () + duration_minutes );
// Atomic update
t . update ( docRef , {
status: 2 ,
last_changed: FieldValue . serverTimestamp (),
reservation_data: {
license_plate ,
expires_at: expirationTime ,
duration: duration_minutes
}
});
});
Release Parking Spot Transaction
await firestore . runTransaction ( async ( t ) => {
const doc = await t . get ( docRef );
if ( ! doc . exists ) throw new Error ( "Spot does not exist" );
const data = doc . data ();
if ( data . status !== 2 ) {
throw new Error ( "No active reservation to cancel" );
}
t . update ( docRef , {
status: 1 ,
last_changed: FieldValue . serverTimestamp (),
reservation_data: FieldValue . delete ()
});
});
Best Practices
Use Transactions Always use transactions for reserve/release operations to prevent race conditions.
Batch Writes Use batch writes when updating multiple documents (e.g., expiring reservations).
Server Timestamps Use FieldValue.serverTimestamp() for consistent timestamps across timezones.
Data Retention Implement automated cleanup for occupancy_history (30-day retention).