Skip to content

Backend

PostGIS in Supabase — How Velocity X Runs Geospatial Queries Without an Extra Database

ST_Within, ST_DWithin, ST_Distance—all in one Postgres database

🗺️ 📍

Most teams building location-based SaaS reach for a second database: MongoDB with geospatial indexes, Elasticsearch, or a dedicated geospatial service. It's the default move when you need "find all leads within 5km of this rep" or "lasso-select jobs inside a polygon." But if you're already on Supabase, you're already paying for PostGIS. You just haven't flipped the switch.

Velocity X uses PostGIS to run lasso-select queries (ST_Within), proximity searches (ST_DWithin), ETA distance hints (ST_Distance), and territory midpoints (ST_Centroid)—all inside the same Postgres database as your SaaS. No extra infrastructure. No data sync lag. One schema. One migration. One performance profile to tune.

What PostGIS Is (And Why Supabase Includes It)

PostGIS is a Postgres extension that teaches the database how to understand geography. Points, polygons, line strings. Distance calculations. Containment checks. Supabase ships Postgres with PostGIS pre-installed (disabled by default)—you enable it with a single SQL command and get 30+ spatial operators for free.

{`-- In the Supabase SQL editor, run:
create extension if not exists postgis;
create extension if not exists postgis_topology;`}

That's it. Your Postgres instance now understands coordinates. No second database to run, no sync layer, no eventual consistency gap.

Five Core Operators (With Production Patterns)

ST_Within: Lasso-Select and Polygon Containment

ST_Within(point, polygon) returns true if a point sits inside a polygon. Velocity X uses this for lasso-select: a rep draws a boundary on the map, and the app queries all jobs inside that boundary in one call.

{`-- A job is a point; a lasso is a polygon drawn by the user
select id, address, status
from jobs
where org_id = $1
  and st_within(
    st_point(latitude, longitude)::geography,
    st_polygon($2)::geography  -- GeoJSON from the frontend
  );`}

Store the lasso as GeoJSON and cast it server-side. The query planner will use a GIST index (more on that below) to skip most rows without calculating containment on every single job.

ST_DWithin: Proximity ("Within 5km")

ST_DWithin(point1, point2, distance) returns true if two points are within N meters of each other. Great for "find leads near this rep" or "which jobs are closest to the next scheduled stop?"

{`-- Find all unassigned jobs within 5km of a rep's current position
select id, address, distance_to_rep
from jobs
where org_id = $1
  and st_dwithin(
    st_point(latitude, longitude)::geography,
    st_point($2, $3)::geography,  -- rep's lat/lon
    5000  -- 5000 meters = 5km
  )
order by st_distance(
  st_point(latitude, longitude)::geography,
  st_point($2, $3)::geography
) asc;`}

Pair ST_DWithin with ST_Distance to sort by actual distance. The database calculates both in one pass.

ST_Distance: ETA Hints and Sorting

ST_Distance(point1, point2) returns distance in meters (or degrees, depending on geometry vs geography). Use it to show distance in the UI or rank results by proximity.

{`-- Add a calculated column for display
select
  id,
  address,
  round(
    st_distance(
      st_point(latitude, longitude)::geography,
      st_point($2, $3)::geography
    ) / 1000, 1
  )::text || ' km' as distance_label
from jobs
where org_id = $1
order by st_distance(...) asc;`}

ST_Intersects: Multi-Polygon and Route Overlap

ST_Intersects(geom1, geom2) returns true if two geometries overlap or touch. Use this to find jobs that intersect a rep's planned route or multiple service areas.

{`-- Find jobs that overlap with a rep's assigned territory (polygon)
select id, address
from jobs
where org_id = $1
  and st_intersects(
    st_point(latitude, longitude)::geography,
    territory_boundary  -- stored as geography column
  );`}

ST_Centroid: Territory Midpoints and Map Centers

ST_Centroid(polygon) calculates the center of a polygon. Useful for placing a map marker at the geographic center of a service area or sales territory.

{`-- Get the center of a territory to auto-pan the map
select st_astext(st_centroid(territory_boundary)) as center_point
from territories
where org_id = $1 and id = $2;`}

Indexing: GIST for Speed

Without an index, every containment or proximity query scans the entire table. Add a GIST (Generalized Search Tree) spatial index and Postgres can skip 99% of rows before calculating distances:

{`-- Create a spatial index on lat/lon
create index jobs_location_gist
on jobs using gist(st_point(latitude, longitude)::geography);

-- Or, if you pre-store a geometry column:
create index jobs_geom_gist on jobs using gist(geom);`}

GIST indexes add ~2–3% overhead to inserts and updates. Worth it once you have 10k+ rows and geography queries are hitting the route or dashboard.

Frequently Asked Questions

Geography vs Geometry—which one do I use?

Geography (lat/lon on Earth) is slower but accurate for real-world distances. Geometry (abstract 2D plane) is faster but wrong for kilometers. Always cast to geography for queries involving real locations.

Can I store polygons and points in the same column?

No. Create separate columns: job_location (point) and territory (polygon). Or store everything in a single JSON/JSONB column and cast on query—but that's slower because Postgres can't index it.

How do I handle edge cases like the dateline or poles?

PostGIS handles most edge cases correctly if you use geography (not geometry) and let Postgres do the math. If you're working near the poles or crossing the dateline, test heavily and read the PostGIS docs. This is rare in most SaaS.

Does PostGIS work with Edge Functions?

Yes. Use the Supabase JS client or run raw SQL—PostGIS operators work in both. Avoid expensive `ST_Within` / `ST_Intersects` queries inside frequently-called Edge Functions unless you've cached the result or indexed aggressively.

What about real-time location tracking?

PostGIS handles snapshots. For real-time "follow this rep on the map," use Supabase Realtime on a positions table (updated every 10–30 seconds) and calculate proximity server-side on update. Don't query geography for every pixel of movement.

Can I use PostGIS with RLS?

Yes. RLS policies apply before spatial operators run. A query like st_dwithin(...) where org_id = $1 will filter by org first (RLS policy) and then calculate distance. The spatial index helps both layers.

The Bottom Line

PostGIS is production-ready, included with Supabase, and eliminates a whole class of infrastructure decisions. Enable the extension, create a GIST index, and ship geospatial features at the speed of a SQL query. Velocity X uses it every day—lasso-select, proximity, territory mapping—all in one database. If you're building location-aware SaaS, stop reaching for a second database and start reaching for ST_Within.

Ready to build location features that scale? Check out pricing or dig deeper into lasso-select patterns.

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.