Overview
S-Parking uses 10 Cloud Functions to handle all backend operations. Each function is independently deployable and auto-scales based on demand.
Parking Status 2 functions for real-time parking data
Reservations 2 functions for reservation management
Spot Management 2 functions for CRUD operations
Zone Management 2 functions for zone operations
Analytics 1 function for occupancy history
IoT Ingestion 1 function for sensor data
1. Get Parking Status
Function : getParkingStatus
Endpoint : GET/POST https://[region]-[project].cloudfunctions.net/get-parking-status
Purpose : Fetch all parking spots with real-time status and auto-expire reservations.
Features
Returns all parking spots from parking_spots collection
Auto-expires reservations : Checks reservation_data.expires_at and reverts to status 1 if expired
Uses batch writes for efficient multi-spot updates
CORS-enabled for browser clients
Implementation
functions . http ( 'getParkingStatus' , ( req , res ) => {
cors ( req , res , async () => {
try {
const spotsCollection = firestore . collection ( 'parking_spots' );
const snapshot = await spotsCollection . get ();
const spots = [];
const now = new Date ();
const batch = firestore . batch ();
let hasExpired = false ;
snapshot . forEach ( doc => {
let data = doc . data ();
let status = data . status ;
const spotId = doc . id ;
// Auto-expire logic
if ( status === 2 && data . reservation_data ?. expires_at ) {
const expiresAt = data . reservation_data . expires_at . toDate ();
if ( now > expiresAt ) {
console . log ( `Expired reservation: ${ spotId } ` );
const docRef = spotsCollection . doc ( spotId );
batch . update ( docRef , {
status: 1 ,
last_changed: FieldValue . serverTimestamp (),
reservation_data: FieldValue . delete ()
});
hasExpired = true ;
status = 1 ;
data . status = 1 ;
}
}
spots . push ({ id: spotId , ... data });
});
if ( hasExpired ) {
await batch . commit ();
console . log ( 'Expired reservations cleaned up' );
}
res . status ( 200 ). json ( spots );
} catch ( error ) {
console . error ( "Error in getParkingStatus:" , error );
res . status ( 500 ). send ( 'Internal server error' );
}
});
});
[
{
"id" : "A-01" ,
"lat" : -33.4489 ,
"lng" : -70.6693 ,
"status" : 1 ,
"desc" : "Puesto A-01" ,
"zone_id" : "zone_1764307623391" ,
"last_changed" : { "_seconds" : 1709673600 }
},
{
"id" : "A-02" ,
"status" : 2 ,
"reservation_data" : {
"license_plate" : "ABCD12" ,
"expires_at" : { "_seconds" : 1709677200 },
"duration" : 60
}
}
]
2. Reserve Parking Spot
Function : reserveParkingSpot
Endpoint : POST https://[region]-[project].cloudfunctions.net/reserve-parking-spot
Purpose : Reserve an available parking spot for a specified duration.
Request Body
Parking spot identifier (e.g., A-01).
Vehicle license plate number.
Reservation duration in minutes (e.g., 60).
Implementation
functions . http ( 'reserveParkingSpot' , ( req , res ) => {
cors ( req , res , async () => {
if ( req . method !== 'POST' ) {
return res . status ( 405 ). send ( 'Method Not Allowed' );
}
const { spot_id , license_plate , duration_minutes } = req . body ;
if ( ! spot_id || ! license_plate || ! duration_minutes ) {
return res . status ( 400 ). json ({
error: 'Missing required fields: spot_id, license_plate, duration_minutes'
});
}
const docRef = firestore . collection ( 'parking_spots' ). doc ( spot_id );
try {
// Use transaction to prevent race conditions
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 ();
// Status validation: 0 = Occupied, 2 = Reserved
if ( data . status === 0 ) {
throw new Error ( "Spot is occupied by a vehicle" );
}
if ( data . status === 2 ) {
throw new Error ( "Spot already has an active reservation" );
}
// Calculate expiration time
const expirationTime = new Date ();
expirationTime . setMinutes (
expirationTime . getMinutes () + parseInt ( duration_minutes )
);
// Update to Status 2 (Reserved)
t . update ( docRef , {
status: 2 ,
last_changed: FieldValue . serverTimestamp (),
reservation_data: {
license_plate: license_plate ,
expires_at: expirationTime ,
duration: parseInt ( duration_minutes )
}
});
});
console . log ( `Reservation successful: ${ spot_id } , Plate: ${ license_plate } ` );
res . status ( 200 ). json ({
success: true ,
message: "Reservation created successfully"
});
} catch ( error ) {
console . warn ( "Reservation error:" , error . message );
res . status ( 409 ). json ({ error: error . message });
}
});
});
Success (200)
Conflict (409)
Bad Request (400)
{
"success" : true ,
"message" : "Reservation created successfully"
}
{
"error" : "Spot already has an active reservation"
}
{
"error" : "Missing required fields: spot_id, license_plate, duration_minutes"
}
3. Release Parking Spot
Function : releaseParkingSpot
Endpoint : POST https://[region]-[project].cloudfunctions.net/release-parking-spot
Purpose : Cancel an active reservation and return spot to available status.
Request Body
Parking spot identifier (e.g., A-01).
Implementation
functions . http ( 'releaseParkingSpot' , ( req , res ) => {
cors ( req , res , async () => {
if ( req . method !== 'POST' ) return res . status ( 405 ). send ( 'Method Not Allowed' );
const { spot_id } = req . body ;
if ( ! spot_id ) return res . status ( 400 ). json ({ error: 'Missing spot_id' });
const docRef = firestore . collection ( 'parking_spots' ). doc ( spot_id );
try {
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 ();
// Only release if status is 2 (Reserved)
if ( data . status !== 2 ) {
throw new Error ( "Spot does not have an active reservation to cancel" );
}
// Revert to Available (1)
t . update ( docRef , {
status: 1 ,
last_changed: FieldValue . serverTimestamp (),
reservation_data: FieldValue . delete ()
});
});
console . log ( `Reservation cancelled: ${ spot_id } ` );
res . status ( 200 ). json ({ success: true });
} catch ( e ) {
console . warn ( "Release error:" , e . message );
res . status ( 409 ). json ({ error: e . message });
}
});
});
4. Create Parking Spot
Function : createParkingSpot
Endpoint : POST https://[region]-[project].cloudfunctions.net/create-parking-spot
Purpose : Create or update a parking spot in the system.
Request Body
Spot identifier (will be uppercased, e.g., a-01 → A-01).
Description (defaults to Puesto [ID]).
Zone identifier (optional).
Initial status (defaults to 1 - Available).
Implementation Highlights
const idTrimmed = id . trim (). toUpperCase ();
const latNum = parseFloat ( lat );
const lngNum = parseFloat ( lng );
const statusNum = status !== undefined ? parseInt ( status ) : 1 ;
await firestore . collection ( 'parking_spots' ). doc ( idTrimmed ). set ({
id: idTrimmed ,
lat: latNum ,
lng: lngNum ,
desc: desc || `Puesto ${ idTrimmed } ` ,
status: statusNum ,
zone_id: zone_id || null ,
created_at: new Date (),
updated_at: new Date ()
}, { merge: true }); // merge: true = update if exists, create if not
5. Delete Parking Spot
Function : deleteParkingSpot
Endpoint : POST https://[region]-[project].cloudfunctions.net/delete-parking-spot
Purpose : Permanently delete a parking spot from the database.
Request Body
Spot identifier to delete.
Implementation
const idTrimmed = id . trim (). toUpperCase ();
console . log ( `🗑️ Deleting spot: ${ idTrimmed } ` );
await firestore . collection ( 'parking_spots' ). doc ( idTrimmed ). delete ();
res . status ( 200 ). json ({
success: true ,
message: `Spot ${ idTrimmed } deleted successfully`
});
6. Get Zones
Function : getZones
Endpoint : GET https://[region]-[project].cloudfunctions.net/get-zones
Purpose : Retrieve all parking zones ordered by order field.
Implementation
const snapshot = await firestore
. collection ( 'parking_zones' )
. orderBy ( 'order' , 'asc' )
. get ();
const zones = [];
snapshot . forEach ( doc => zones . push ( doc . data ()));
// Return default zone if empty
if ( zones . length === 0 ) {
zones . push ({
id: 'General' ,
name: 'General' ,
order: 1 ,
desc: 'Default general zone' ,
color: 'blue' ,
created_at: new Date (),
updated_at: new Date ()
});
}
res . status ( 200 ). json ( zones );
[
{
"id" : "zone_1764307623391" ,
"name" : "Federico Froebel" ,
"order" : 1 ,
"desc" : "Exterior parking on Federico Froebel street" ,
"color" : "blue" ,
"created_at" : { "_seconds" : 1764307623 },
"updated_at" : { "_seconds" : 1764307623 }
}
]
7. Manage Zones
Function : manageZones
Endpoint : POST https://[region]-[project].cloudfunctions.net/manage-zones
Purpose : Create, update, or delete parking zones.
Request Body
Operation type: create, update, or delete.
Zone ID (required for update and delete, auto-generated for create).
Zone name (required for create and update).
Sort order (optional, defaults to 999).
Zone description (optional).
Color identifier (optional, defaults to blue).
Operations
const zoneId = id || `zone_ ${ Date . now () } ` ;
const newOrder = order || 999 ;
await firestore . collection ( 'parking_zones' ). doc ( zoneId ). set ({
id: zoneId ,
name: name . trim (),
order: newOrder ,
desc: desc || '' ,
color: color || 'blue' ,
created_at: new Date (),
updated_at: new Date ()
});
const updateData = {
name: name . trim (),
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 ( id ). update ( updateData );
await firestore . collection ( 'parking_zones' ). doc ( id ). delete ();
res . status ( 200 ). json ({
success: true ,
message: 'Zone deleted successfully'
});
8. Get Occupancy History
Function : getOccupancyHistory
Endpoint : GET https://[region]-[project].cloudfunctions.net/get-occupancy-history
Purpose : Retrieve historical occupancy data for analytics.
Query Parameters
Number of days to retrieve (1-30, defaults to 1).
Optional zone ID to filter results.
Implementation
const daysParam = parseInt ( req . query . days || '1' , 10 );
const days = Math . min ( Math . max ( daysParam , 1 ), 30 ); // Clamp 1-30
const zoneId = req . query . zoneId || null ;
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 => {
const data = doc . data ();
const entry = {
hour_key: data . hour_key ,
ts: data . ts ,
global: data . global ,
};
if ( zoneId ) {
// Include only requested zone
entry . zone = data . zones ?.[ zoneId ] || null ;
} else {
// Include zone summaries
entry . zones_summary = Object . keys ( data . zones || {}). map ( zId => ({
id: zId ,
occupancyPct: data . zones [ zId ]. occupancyPct ,
free: data . zones [ zId ]. free ,
occupied: data . zones [ zId ]. occupied ,
reserved: data . zones [ zId ]. reserved ,
total: data . zones [ zId ]. total
}));
}
samples . push ( entry );
});
{
"success" : true ,
"days" : 7 ,
"zoneId" : null ,
"count" : 168 ,
"from" : "2026-02-26T14:00:00.000Z" ,
"to" : "2026-03-05T14:00:00.000Z" ,
"samples" : [
{
"hour_key" : "2026-02-26-14" ,
"ts" : 1709038800000 ,
"global" : {
"free" : 10 ,
"occupied" : 22 ,
"reserved" : 4 ,
"total" : 36 ,
"occupancyPct" : 72
},
"zones_summary" : [ /* ... */ ]
}
]
}
9. Save Hourly Snapshot
Function : saveHourlySnapshot
Endpoint : GET/POST https://[region]-[project].cloudfunctions.net/save-hourly-snapshot
Purpose : Generate hourly occupancy snapshot for analytics. Triggered by Cloud Scheduler.
Features
Production Mode : Reads actual data from parking_spots collection
Development Mode : Generates realistic mock data for testing (POST with hoursAgo param)
Data Retention : Auto-deletes snapshots older than 30 days
Idempotent : Uses hour_key as document ID to prevent duplicates
Mock Data Generation
The function includes sophisticated mock data generation that simulates realistic university parking patterns:
// POST with { hoursAgo: 24, mockData: true } generates historical data
const hourOfDay = targetDate . getUTCHours ();
const dayOfWeek = targetDate . getUTCDay ();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6 ;
// Peak hours: 11:00-15:00 Chile time (14:00-18:00 UTC)
if ( ! isWeekend && hourOfDay >= 14 && hourOfDay <= 18 ) {
froebelOccupancy = 0.85 + Math . random () * 0.10 ; // 85-95%
interiorOccupancy = 0.88 + Math . random () * 0.10 ; // 88-98%
}
{
"success" : true ,
"message" : "Hourly snapshot generated" ,
"hour_key" : "2026-03-05-14" ,
"timestamp" : "2026-03-05T14:00:00.000Z" ,
"retention_deleted" : 0 ,
"global" : {
"free" : 8 ,
"occupied" : 25 ,
"reserved" : 3 ,
"total" : 36 ,
"occupancyPct" : 78
},
"zones_count" : 2 ,
"development_mode" : false
}
10. Ingest Parking Data (IoT)
Function : ingestParkingData
Endpoint : POST https://[region]-[project].cloudfunctions.net/ingest-parking-data
Purpose : Receive sensor data from IoT devices and update spot status.
Request Body
Sensor status: 0 (Occupied) or 1 (Available).
Reservation Protection Logic
This function implements intelligent logic to protect active reservations:
Scenario : Spot is reserved, but sensor says available (user hasn’t arrived yet)Action : Ignore sensor, keep reservation activeif ( currentStatus === 2 && sensorStatus === 1 ) {
console . log ( `Protecting reservation: ${ spotId } ` );
return res . status ( 200 ). send ( 'Spot reserved, waiting for arrival' );
}
Scenario : Spot is reserved, sensor detects vehicle (reserved user arrived)Action : Confirm occupancy, clear reservation dataif ( currentStatus === 2 && sensorStatus === 0 ) {
console . log ( `Reservation fulfilled: ${ spotId } ` );
await docRef . update ({
status: 0 ,
last_changed: FieldValue . serverTimestamp (),
reservation_data: FieldValue . delete ()
});
return res . status ( 200 ). send ( 'Reservation completed' );
}
Scenario : No reservation involved, normal sensor updatesAction : Update status directlyif ( currentStatus !== sensorStatus ) {
await docRef . update ({
status: sensorStatus ,
last_changed: FieldValue . serverTimestamp ()
});
console . log ( `Normal update: ${ spotId } → ${ sensorStatus } ` );
}
Auto-create New Spots
If a sensor sends data for a non-existent spot, it’s automatically created:
if ( ! doc . exists ) {
await docRef . set ({
status: sensorStatus ,
last_changed: FieldValue . serverTimestamp ()
});
return res . status ( 200 ). send ( 'New spot created' );
}
CORS Configuration
All functions use the cors middleware to enable browser access:
const cors = require ( 'cors' )({ origin: true });
functions . http ( 'functionName' , ( req , res ) => {
cors ( req , res , async () => {
// Function logic here
});
});
Alternatively, some functions set CORS headers manually:
res . set ( 'Access-Control-Allow-Origin' , '*' );
res . set ( 'Access-Control-Allow-Methods' , 'POST, OPTIONS' );
res . set ( 'Access-Control-Allow-Headers' , 'Content-Type' );
if ( req . method === 'OPTIONS' ) {
res . status ( 204 ). send ( '' );
return ;
}
Error Handling
All functions follow consistent error handling patterns:
400 Bad Request Missing or invalid parameters
405 Method Not Allowed Wrong HTTP method used
409 Conflict Business logic violation (e.g., spot already reserved)
500 Internal Error Unexpected errors with stack traces
Deployment
All functions are deployed via gcloud CLI:
# Deploy individual function
gcloud functions deploy reserve-parking-spot \
--gen2 \
--runtime=nodejs20 \
--region=us-central1 \
--source=./reserve-parking-spot_function-source \
--entry-point=reserveParkingSpot \
--trigger-http \
--allow-unauthenticated
# Deploy all functions
for dir in gcp-functions/*_function-source ; do
FUNC_NAME = $( basename " $dir " _function-source )
gcloud functions deploy " $FUNC_NAME " --gen2 --source= " $dir "
done
Function Avg Latency Cold Start Memory Invocations/day getParkingStatus 180ms 450ms 256MB 4,320 reserveParkingSpot 120ms 420ms 256MB 150 releaseParkingSpot 110ms 400ms 256MB 140 getOccupancyHistory 320ms 480ms 256MB 200 saveHourlySnapshot 850ms 520ms 512MB 24 ingestParkingData 95ms 380ms 256MB 2,000
Best Practices
Use Transactions Always use transactions for operations that check-then-modify state.
Validate Early Validate all inputs before performing database operations.
Log Strategically Use structured logging with context for debugging.
Handle OPTIONS Always handle preflight OPTIONS requests for CORS.