Skip to main content

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

id
string
required
Uppercase spot identifier (e.g., A-01, I-16). Same as document ID.
lat
number
required
Latitude coordinate (e.g., -33.4489).
lng
number
required
Longitude coordinate (e.g., -70.6693).
status
number
required
Current status:
  • 0 = Occupied (vehicle detected)
  • 1 = Available (free)
  • 2 = Reserved (user reservation active)
desc
string
Human-readable description (e.g., Puesto A-01).
zone_id
string
Foreign key to parking_zones collection (e.g., zone_1764307623391).
last_changed
timestamp
Server timestamp of last status change. Updated via FieldValue.serverTimestamp().
reservation_data
object
Present only when status = 2. Contains:
license_plate
string
Vehicle license plate number.
expires_at
timestamp
Reservation expiration time.
duration
number
Reservation duration in minutes.
created_at
timestamp
Document creation timestamp.
updated_at
timestamp
Last update 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

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

Collection: parking_zones

Stores parking zone definitions. Document ID is the zone identifier (e.g., zone_1764307623391).

Document Schema

id
string
required
Unique zone identifier (e.g., zone_1764307623391).
name
string
required
Display name (e.g., Federico Froebel, Interior DUOC).
order
number
required
Sort order for display (ascending). Used in UI to order zone tabs.
desc
string
Zone description.
color
string
Color identifier for UI (e.g., blue, green, red).
created_at
timestamp
Zone creation timestamp.
updated_at
timestamp
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

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

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

hour_key
string
required
Unique hour identifier in format YYYY-MM-DD-HH (UTC). Example: 2026-03-05-14.
timestamp
string
required
ISO 8601 timestamp for audit purposes (e.g., 2026-03-05T14:00:00.000Z).
ts
number
required
Unix timestamp in milliseconds. Used for efficient range queries.
global
object
required
Global occupancy statistics:
free
number
Number of available spots.
occupied
number
Number of occupied spots.
reserved
number
Number of reserved spots.
total
number
Total spots in system.
occupancyPct
number
Occupancy percentage: (occupied + reserved) / total * 100, rounded to integer.
zones
object
required
Map of zone statistics. Keys are zone IDs, values are objects with:
free
number
Available spots in zone.
occupied
number
Occupied spots in zone.
reserved
number
Reserved spots in zone.
total
number
Total spots in zone.
occupancyPct
number
Zone occupancy percentage.
created_at
timestamp
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

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()));

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).

Build docs developers (and LLMs) love