RideWiseCore Docs

Node.js integration

A complete, production-shaped integration between your backend and RideWiseCore.

This guide builds a small module for your Node.js backend that:

  1. Keeps one authenticated socket to RideWiseCore.
  2. Forwards agent locations from your agent app.
  3. Requests matches for passengers and waits for the result.
  4. Relays offers to agents and their answers back.
  5. Updates agent status around trips.

It uses TypeScript, socket.io-client and the built-in fetch. The pieces that talk to your apps (notifyAgent, notifyPassenger) are stubs to replace with your own push or socket layer.

npm install socket.io-client

1. Typed client

Start with the event types from the Real-time reference. Save them as rwc-events.ts, then create the client:

rwc-client.ts
import { io } from "socket.io-client";
import type { Match, RwcSocket } from "./rwc-events";

const RWC_API_URL = process.env.RWC_API_URL!;
const RWC_API_KEY = process.env.RWC_API_KEY!;

export const rwc: RwcSocket = io(RWC_API_URL, {
  auth: { apiKey: RWC_API_KEY },
  transports: ["websocket"],
});

rwc.on("connect", () => console.info("[rwc] connected"));
rwc.on("disconnect", (reason) => console.warn("[rwc] disconnected:", reason));
rwc.on("connect_error", (err) => console.error("[rwc] connect_error:", err.message));

class RwcError extends Error {
  constructor(
    public statusCode: number,
    message: string,
  ) {
    super(message);
  }
}

async function rwcFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(`${RWC_API_URL}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${RWC_API_KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  if (res.status === 204) return undefined as T;
  const body = await res.json();
  if (!res.ok) throw new RwcError(res.status, body.error?.message ?? res.statusText);
  return body as T;
}

export const rwcApi = {
  createMatch: (input: { pickup: { lat: number; lng: number }; vehicleType: string; radiusMeters?: number }) =>
    rwcFetch<Match>("/matches", { method: "POST", body: JSON.stringify(input) }),
  getMatch: (matchId: string) => rwcFetch<Match>(`/matches/${matchId}`),
  cancelMatch: (matchId: string) => rwcFetch<Match>(`/matches/${matchId}/cancel`, { method: "POST" }),
};

2. Forward agent locations

Your agent app already sends its GPS position to your backend. Forward each update, with the agent's real status:

agents.ts
import { rwc } from "./rwc-client";
import type { AgentStatus } from "./rwc-events";

// Your own source of truth for whether an agent is on a trip.
const agentStatus = new Map<string, AgentStatus>();

export function onAgentGps(agentId: string, gps: { lat: number; lng: number; heading?: number; speed?: number }, vehicleType: string) {
  rwc.emit(
    "agent:location",
    {
      agentId,
      ...gps,
      vehicleType,
      status: agentStatus.get(agentId) ?? "available", // never omit, or it resets to available
    },
    (ack) => {
      if (!ack.ok) console.warn(`[rwc] location rejected for ${agentId}: ${ack.error}`);
    },
  );
}

export function setAgentStatus(agentId: string, status: AgentStatus) {
  agentStatus.set(agentId, status);
}

export function onAgentSignOff(agentId: string, lastGps: { lat: number; lng: number }, vehicleType: string) {
  setAgentStatus(agentId, "offline");
  rwc.emit("agent:location", { agentId, ...lastGps, vehicleType, status: "offline" });
}

3. Request a match and wait for the result

POST /matches returns straight away. The result arrives later on match:update. This helper turns that into a promise:

matching.ts
import { rwc, rwcApi } from "./rwc-client";
import type { Match } from "./rwc-events";

const pending = new Map<string, (match: Match) => void>();

rwc.on("match:update", (match) => {
  pending.get(match.id)?.(match);
  pending.delete(match.id);
});

/** Resolves with the final match: matched, unmatched or cancelled. */
export async function findAgent(pickup: { lat: number; lng: number }, vehicleType: string): Promise<Match> {
  const match = await rwcApi.createMatch({ pickup, vehicleType });

  // No agent at all: the match is already final.
  if (match.status !== "searching") return match;

  return new Promise<Match>((resolve) => {
    pending.set(match.id, resolve);

    // Safety net: if the socket dropped and we missed the update, poll.
    setTimeout(async () => {
      if (!pending.has(match.id)) return;
      const latest = await rwcApi.getMatch(match.id);
      if (latest.status !== "searching") {
        pending.delete(match.id);
        resolve(latest);
      }
    }, 120_000);
  });
}

Use it in your ride-request handler:

const result = await findAgent({ lat: 40.713, lng: -74.0055 }, "sedan");

if (result.status === "matched") {
  setAgentStatus(result.matchedAgentId!, "busy");
  await createTripInYourDb({ agentId: result.matchedAgentId!, rwcMatchId: result.id });
  notifyPassenger("Agent on the way");
} else {
  notifyPassenger("No agents available right now");
}

4. Relay offers and answers

offers.ts
import { rwc } from "./rwc-client";

rwc.on("ride:offer", (offer) => {
  // If you run several backend instances, make sure only one delivers each offer.
  notifyAgent(offer.agentId, {
    type: "ride_offer",
    matchId: offer.matchId,
    pickup: offer.pickup,
    // Leave headroom for network latency.
    respondWithinMs: offer.expiresInMs - 2_000,
  });
});

/** Call this when your agent app sends back Accept/Decline. */
export function onAgentAnswer(agentId: string, matchId: string, response: "accept" | "decline") {
  rwc.emit("ride:respond", { matchId, agentId, response }, (ack) => {
    if (!ack.ok) {
      // Expired, cancelled, or already resolved. Tell the agent the offer is gone.
      notifyAgent(agentId, { type: "offer_withdrawn", matchId });
    }
  });
}

5. Finish the trip

RideWiseCore doesn't know when a trip ends. When your trip completes or is cancelled after matching, make the agent available again:

export function onTripEnded(agentId: string) {
  setAgentStatus(agentId, "available");
  // The next GPS update carries the new status automatically.
}

6. Passenger gives up while searching

export async function onPassengerCancelled(matchId: string) {
  try {
    await rwcApi.cancelMatch(matchId);
  } catch (err) {
    // 409: already finished or mid-transition. Re-read to see what happened.
    const latest = await rwcApi.getMatch(matchId);
    if (latest.status === "matched") {
      // An agent accepted at the last moment. Cancel the trip in your system
      // and release the agent.
      onTripEnded(latest.matchedAgentId!);
    }
  }
}

Production checklist

  • API key is stored in a secret manager and never sent to clients.
  • Every agent:location includes an explicit status.
  • Agent locations are sent every 3–10 s while online, and offline is sent on sign-off.
  • Agent status returns to available after every trip.
  • ride:offer handling doesn't depend on the POST /matches response arriving first.
  • Each offer is delivered to the agent once, even with several backend instances.
  • ride:respond acknowledgements with ok: false are shown to the agent.
  • 401, 402 and 403 responses and socket connect_errors alert your on-call team, because they mean dispatch is down.
  • You have a documented key rotation procedure.

On this page