Features

Geospatial Queries

Vextro automatically syncs point field data to a @convex-dev/geospatial spatial index whenever documents are created, updated, or deleted. This enables efficient bounding-box and nearest-neighbor queries without manual index management.

For querying, you use the GeospatialIndex API directly in your own Convex functions — giving you full control over filtering, pagination, and result shaping.

Setup

Before writing spatial queries, ensure you've completed the point field setup:

  1. npm install @convex-dev/geospatial
  2. Register app.use(geospatial) in convex.config.ts
  3. Pass geospatialComponent: components.geospatial to createVextroAdminModule()

Creating a spatial index instance

Use the createSpatialIndex helper from vextro/convex/geo:

import { createSpatialIndex } from "vextro/convex/geo";
import { components } from "./_generated/api";

const geoIndex = createSpatialIndex(components.geospatial);

Or use the GeospatialIndex class directly:

import { GeospatialIndex } from "@convex-dev/geospatial";
import { components } from "./_generated/api";

const geoIndex = new GeospatialIndex(components.geospatial);

Bounding box query

Find all points within a rectangular region:

// convex/stores.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
import { createSpatialIndex } from "vextro/convex/geo";
import { components } from "./_generated/api";

const geoIndex = createSpatialIndex(components.geospatial);

export const storesInArea = query({
  args: {
    west: v.number(),
    east: v.number(),
    south: v.number(),
    north: v.number(),
  },
  handler: async (ctx, { west, east, south, north }) => {
    return await geoIndex.query(ctx, {
      shape: {
        type: "rectangle",
        rectangle: { west, east, south, north },
      },
    });
  },
});

Nearest neighbor query

Find the closest points to a given location:

export const nearestStores = query({
  args: {
    lat: v.number(),
    lng: v.number(),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, { lat, lng, limit }) => {
    return await geoIndex.nearest(ctx, {
      point: { latitude: lat, longitude: lng },
      limit: limit ?? 10,
    });
  },
});

Filtered spatial queries

If your point field defines filterKeys, you can filter spatial results by those metadata values:

// Collection definition:
// location: f.point({ filterKeys: { category: "category", status: "status" } })

export const nearbyRestaurants = query({
  args: {
    lat: v.number(),
    lng: v.number(),
  },
  handler: async (ctx, { lat, lng }) => {
    return await geoIndex.nearest(ctx, {
      point: { latitude: lat, longitude: lng },
      filter: (q) =>
        q.eq("category", "restaurant").eq("status", "published"),
      limit: 20,
    });
  },
});

Haversine distance utility

For display purposes (e.g., showing "5 miles away" in a UI), use haversineDistance:

import { haversineDistance } from "vextro/convex/geo";

const distance = haversineDistance(
  { lat: 40.7128, lng: -74.006 },   // New York
  { lat: 34.0522, lng: -118.2437 }, // Los Angeles
  "mi"
);
// ≈ 2,451 miles
ParameterTypeDefaultDescription
a{ lat: number; lng: number }requiredFirst point
b{ lat: number; lng: number }requiredSecond point
unit"km" | "mi""km"Distance unit

Returns a number — the great-circle distance in the specified unit.

Constants

  • EARTH_RADIUS_KM — 6371 (mean radius in kilometers)
  • EARTH_RADIUS_MI — 3958.8 (mean radius in miles)

Re-exported types

For convenience, pointValidator and the PointValue type are re-exported from vextro/convex/geo so you can use them in Convex query args:

import { pointValidator } from "vextro/convex/geo";

export const nearby = query({
  args: {
    center: pointValidator,
    radiusMiles: v.number(),
  },
  handler: async (ctx, { center, radiusMiles }) => {
    return await geoIndex.nearest(ctx, {
      point: { latitude: center.lat, longitude: center.lng },
      maxDistance: radiusMiles * 1609.34, // convert miles to meters if needed
    });
  },
});

Automatic index sync

You don't need to manually manage the spatial index. Vextro's admin module hooks handle it:

  • Document created — point inserted into spatial index with filter keys and sort key
  • Document updated — if point or any filter/sort key field changed, old entry removed and new one inserted
  • Document deleted — point removed from spatial index
  • Point field cleared — entry removed from spatial index
  • Point field populated (was empty) — entry inserted into spatial index

This happens transparently inside createDocumentForCollection, updateDocumentForCollection, and deleteDocumentForCollection.

Previous
Review Queue