Skip to main content

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');
    }
  });
});

Response Format

[
  {
    "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

spot_id
string
required
Parking spot identifier (e.g., A-01).
license_plate
string
required
Vehicle license plate number.
duration_minutes
number
required
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 });
    }
  });
});

Response Format

{
  "success": true,
  "message": "Reservation created successfully"
}

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

spot_id
string
required
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

id
string
required
Spot identifier (will be uppercased, e.g., a-01A-01).
lat
number
required
Latitude coordinate.
lng
number
required
Longitude coordinate.
desc
string
Description (defaults to Puesto [ID]).
zone_id
string
Zone identifier (optional).
status
number
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

id
string
required
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);

Response Format

[
  {
    "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

action
string
required
Operation type: create, update, or delete.
id
string
Zone ID (required for update and delete, auto-generated for create).
name
string
Zone name (required for create and update).
order
number
Sort order (optional, defaults to 999).
desc
string
Zone description (optional).
color
string
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()
});

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

days
number
Number of days to retrieve (1-30, defaults to 1).
zoneId
string
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);
});

Response Format

{
  "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%
}

Response Format

{
  "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

spot_id
string
required
Parking spot identifier.
status
number
required
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 active
if (currentStatus === 2 && sensorStatus === 1) {
  console.log(`Protecting reservation: ${spotId}`);
  return res.status(200).send('Spot reserved, waiting for arrival');
}

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

Performance Metrics

FunctionAvg LatencyCold StartMemoryInvocations/day
getParkingStatus180ms450ms256MB4,320
reserveParkingSpot120ms420ms256MB150
releaseParkingSpot110ms400ms256MB140
getOccupancyHistory320ms480ms256MB200
saveHourlySnapshot850ms520ms512MB24
ingestParkingData95ms380ms256MB2,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.

Build docs developers (and LLMs) love