[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Geo-Fencing System

Build a Geo-Fencing System

Build a geo-fencing system with polygon zones, point-in-polygon detection, entry/exit events, location-based triggers, real-time tracking, and compliance zone management.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Bruno leads engineering at a 25-person fleet management company tracking 500 delivery vehicles. They need geo-fences for: alerting when a truck enters a customer's delivery zone, detecting if a vehicle leaves its assigned territory, triggering time-clock events when drivers arrive at the warehouse, and compliance zones (restricted areas, school zones with speed limits). They tried radius-based detection but real zones aren't circles — warehouse boundaries, city districts, and delivery areas are irregular polygons. PostGIS queries on every GPS ping (every 10 seconds × 500 vehicles) overloaded the database.

Step 1: Build the Geo-Fencing Engine

typescript
// src/geo/fencing.ts — Geo-fencing with polygon zones, events, and real-time tracking
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

interface GeoFence {
  id: string;
  name: string;
  type: "polygon" | "circle";
  // Polygon: array of [lng, lat] coordinates
  coordinates: number[][];
  // Circle: center + radius
  center?: { lng: number; lat: number };
  radiusMeters?: number;
  category: string;            // "delivery_zone" | "warehouse" | "restricted" | "speed_zone"
  metadata: Record<string, any>;
  triggers: FenceTrigger[];
  status: "active" | "disabled";
  createdAt: string;
}

interface FenceTrigger {
  event: "enter" | "exit" | "dwell";  // dwell = inside for X minutes
  dwellMinutes?: number;
  actions: TriggerAction[];
}

interface TriggerAction {
  type: "webhook" | "notification" | "log" | "speed_limit";
  config: Record<string, any>;
}

interface LocationUpdate {
  vehicleId: string;
  lat: number;
  lng: number;
  speed: number;               // km/h
  heading: number;
  timestamp: number;
}

interface FenceEvent {
  id: string;
  vehicleId: string;
  fenceId: string;
  fenceName: string;
  event: "enter" | "exit" | "dwell";
  location: { lat: number; lng: number };
  speed: number;
  timestamp: string;
}

// Process location update: check all fences
export async function processLocationUpdate(update: LocationUpdate): Promise<FenceEvent[]> {
  const events: FenceEvent[] = [];

  // Get vehicle's previous zone state
  const prevZones = await redis.smembers(`vehicle:zones:${update.vehicleId}`);
  const prevZoneSet = new Set(prevZones);

  // Get nearby fences (spatial index)
  const nearbyFences = await getNearbyFences(update.lat, update.lng);
  const currentZones = new Set<string>();

  for (const fence of nearbyFences) {
    const inside = isInsideFence(update.lat, update.lng, fence);

    if (inside) {
      currentZones.add(fence.id);

      // Check enter event
      if (!prevZoneSet.has(fence.id)) {
        const event = await createFenceEvent(update, fence, "enter");
        events.push(event);
        await executeTriggers(fence, "enter", update, event);
      }

      // Check dwell
      const dwellTrigger = fence.triggers.find((t) => t.event === "dwell");
      if (dwellTrigger) {
        await checkDwell(update.vehicleId, fence, dwellTrigger, update);
      }

      // Check speed limit
      const speedTrigger = fence.triggers.find((t) =>
        t.actions.some((a) => a.type === "speed_limit")
      );
      if (speedTrigger) {
        const speedLimit = speedTrigger.actions.find((a) => a.type === "speed_limit")?.config.maxSpeed;
        if (speedLimit && update.speed > speedLimit) {
          await redis.rpush("notification:queue", JSON.stringify({
            type: "speed_violation",
            vehicleId: update.vehicleId,
            speed: update.speed,
            limit: speedLimit,
            zone: fence.name,
          }));
        }
      }
    }
  }

  // Check exit events
  for (const prevZone of prevZones) {
    if (!currentZones.has(prevZone)) {
      const fence = nearbyFences.find((f) => f.id === prevZone);
      if (fence) {
        const event = await createFenceEvent(update, fence, "exit");
        events.push(event);
        await executeTriggers(fence, "exit", update, event);
      }
      // Clean up dwell timer
      await redis.del(`dwell:${update.vehicleId}:${prevZone}`);
    }
  }

  // Update current zone state
  const pipeline = redis.pipeline();
  pipeline.del(`vehicle:zones:${update.vehicleId}`);
  if (currentZones.size > 0) {
    pipeline.sadd(`vehicle:zones:${update.vehicleId}`, ...currentZones);
  }
  pipeline.expire(`vehicle:zones:${update.vehicleId}`, 3600);

  // Store latest position
  pipeline.geoadd("vehicle:positions", update.lng, update.lat, update.vehicleId);
  pipeline.setex(`vehicle:latest:${update.vehicleId}`, 300, JSON.stringify(update));

  await pipeline.exec();

  return events;
}

// Point-in-polygon test (ray casting algorithm)
function isInsidePolygon(lat: number, lng: number, polygon: number[][]): boolean {
  let inside = false;
  const n = polygon.length;

  for (let i = 0, j = n - 1; i < n; j = i++) {
    const xi = polygon[i][1], yi = polygon[i][0]; // [lng, lat] → lat, lng
    const xj = polygon[j][1], yj = polygon[j][0];

    if (((yi > lng) !== (yj > lng)) && (lat < (xj - xi) * (lng - yi) / (yj - yi) + xi)) {
      inside = !inside;
    }
  }

  return inside;
}

function isInsideCircle(lat: number, lng: number, center: { lat: number; lng: number }, radiusMeters: number): boolean {
  const R = 6371000; // Earth radius in meters
  const dLat = (lat - center.lat) * Math.PI / 180;
  const dLng = (lng - center.lng) * Math.PI / 180;
  const a = Math.sin(dLat / 2) ** 2 +
    Math.cos(center.lat * Math.PI / 180) * Math.cos(lat * Math.PI / 180) * Math.sin(dLng / 2) ** 2;
  const distance = R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return distance <= radiusMeters;
}

function isInsideFence(lat: number, lng: number, fence: GeoFence): boolean {
  if (fence.type === "circle" && fence.center && fence.radiusMeters) {
    return isInsideCircle(lat, lng, fence.center, fence.radiusMeters);
  }
  return isInsidePolygon(lat, lng, fence.coordinates);
}

// Get fences near a location (spatial index)
async function getNearbyFences(lat: number, lng: number): Promise<GeoFence[]> {
  // Check cache for all active fences (updated periodically)
  const cached = await redis.get("fences:active");
  if (cached) {
    const fences: GeoFence[] = JSON.parse(cached);
    // Quick bounding box filter before expensive point-in-polygon
    return fences.filter((f) => isNearBoundingBox(lat, lng, f, 5000)); // 5km buffer
  }

  // Load from DB
  const { rows } = await pool.query(
    `SELECT * FROM geo_fences WHERE status = 'active'`
  );

  const fences = rows.map(parseFence);
  await redis.setex("fences:active", 300, JSON.stringify(fences)); // 5min cache
  return fences.filter((f) => isNearBoundingBox(lat, lng, f, 5000));
}

function isNearBoundingBox(lat: number, lng: number, fence: GeoFence, bufferMeters: number): boolean {
  if (fence.type === "circle" && fence.center) {
    return isInsideCircle(lat, lng, fence.center, (fence.radiusMeters || 0) + bufferMeters);
  }

  const lats = fence.coordinates.map((c) => c[1]);
  const lngs = fence.coordinates.map((c) => c[0]);
  const buffer = bufferMeters / 111000; // rough degrees

  return lat >= Math.min(...lats) - buffer && lat <= Math.max(...lats) + buffer &&
         lng >= Math.min(...lngs) - buffer && lng <= Math.max(...lngs) + buffer;
}

async function checkDwell(vehicleId: string, fence: GeoFence, trigger: FenceTrigger, update: LocationUpdate): Promise<void> {
  const dwellKey = `dwell:${vehicleId}:${fence.id}`;
  const enteredAt = await redis.get(dwellKey);

  if (!enteredAt) {
    await redis.setex(dwellKey, 86400, String(update.timestamp));
    return;
  }

  const dwellMinutes = (update.timestamp - parseInt(enteredAt)) / 60000;
  if (dwellMinutes >= (trigger.dwellMinutes || 0)) {
    const alreadyFired = await redis.get(`dwell:fired:${vehicleId}:${fence.id}`);
    if (!alreadyFired) {
      await redis.setex(`dwell:fired:${vehicleId}:${fence.id}`, 3600, "1");
      await executeTriggers(fence, "dwell", update, await createFenceEvent(update, fence, "dwell"));
    }
  }
}

async function createFenceEvent(update: LocationUpdate, fence: GeoFence, event: FenceEvent["event"]): Promise<FenceEvent> {
  const fenceEvent: FenceEvent = {
    id: `fe-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
    vehicleId: update.vehicleId,
    fenceId: fence.id,
    fenceName: fence.name,
    event,
    location: { lat: update.lat, lng: update.lng },
    speed: update.speed,
    timestamp: new Date(update.timestamp).toISOString(),
  };

  await pool.query(
    `INSERT INTO fence_events (id, vehicle_id, fence_id, event, location, speed, created_at)
     VALUES ($1, $2, $3, $4, point($5, $6), $7, NOW())`,
    [fenceEvent.id, update.vehicleId, fence.id, event, update.lat, update.lng, update.speed]
  );

  return fenceEvent;
}

async function executeTriggers(fence: GeoFence, event: string, update: LocationUpdate, fenceEvent: FenceEvent): Promise<void> {
  const triggers = fence.triggers.filter((t) => t.event === event);
  for (const trigger of triggers) {
    for (const action of trigger.actions) {
      switch (action.type) {
        case "webhook":
          await redis.rpush("webhook:queue", JSON.stringify({ url: action.config.url, payload: fenceEvent }));
          break;
        case "notification":
          await redis.rpush("notification:queue", JSON.stringify({ ...fenceEvent, message: `Vehicle ${update.vehicleId} ${event} ${fence.name}` }));
          break;
        case "log":
          break; // already logged in DB
      }
    }
  }
}

function parseFence(row: any): GeoFence {
  return { ...row, coordinates: JSON.parse(row.coordinates || "[]"), center: row.center ? JSON.parse(row.center) : undefined, triggers: JSON.parse(row.triggers || "[]"), metadata: JSON.parse(row.metadata || "{}") };
}

Results

  • Delivery notifications automated — customer gets "Your driver is 5 minutes away" when the truck enters the delivery zone; customer satisfaction up 30%
  • Territory violations caught — driver leaves assigned zone: dispatcher gets real-time alert; unauthorized detours dropped 90%
  • Time-clock automation — drivers don't clock in/out manually; warehouse geo-fence entry/exit creates accurate time records; payroll disputes eliminated
  • Speed zone compliance — school zone geo-fences with 30 km/h limit; violations logged automatically; fleet safety score improved; insurance premiums reduced 15%
  • 5,000 GPS pings/second handled — Redis spatial index + bounding box pre-filter; only precise polygon checks for nearby fences; database load reduced 95%