Files
supabase/apps/docs/content/guides/database/extensions/postgis.mdx
T
608040b8cb chore(docs) Resolve 'simple' style warnings where applicable (#46966)
Contributes to DOCS-1052

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Resolves MDX linting errors related to "simple" where it applies.
There was a couple cases that did not apply. For example, a product with
"Simple" in the name.

These changes are made in context, either by removing or using a more
descriptive synonym like "minimal" or "basic".

## Tophatting

1. Read each of the diffs.
2. See that the text still makes sense in context.

For extra due diligence, you can run `pnpm lint:mdx` locally and see the
'simple' errors that remain and whether they are worth addressing.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **Documentation**
* Updated many guide, tutorial, and troubleshooting pages with clearer
“basic”/“minimal” wording across setup steps, local testing
instructions, security cautions, and RLS guidance.
* Refined headings, example descriptions, and inline comments for
consistency (including deployment, MCP, metrics API, and search/function
phrasing).
* Improved readability with small snippet formatting tweaks (whitespace
plus import/comment ordering) and added a self-hosting debugging note
for Envoy admin endpoints via a short-lived `curl` container.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Chris Chinchilla <chris.ward@supabase.io>
Co-authored-by: Nik Richers <nrichers@gmail.com>
2026-06-16 21:45:55 +00:00

483 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: 'postgis'
title: 'PostGIS: Geo queries'
description: 'Working with geo-spatial data in Postgres'
tocVideo: 'agFsGDJxjwA'
---
[PostGIS](https://postgis.net/) is a Postgres extension that allows you to interact with Geo data within Postgres. You can sort your data by geographic location, get data within certain geographic boundaries, and do much more with it.
## Overview
While you may be able to store lat/long geographic coordinates as a set of decimals, it does not scale very well when you try to query through a large data set. PostGIS comes with special data types that are efficient, and indexable for high scalability.
The additional data types that PostGIS provides include [Point](https://postgis.net/docs/using_postgis_dbmanagement.html#Point), [Polygon](https://postgis.net/docs/using_postgis_dbmanagement.html#Polygon), [LineString](https://postgis.net/docs/using_postgis_dbmanagement.html#LineString), and many more to represent different types of geographical data. In this guide, we will mainly focus on how to interact with `Point` type, which represents a single set of latitude and longitude. If you are interested in digging deeper, you can learn more about different data types on the [data management section of PostGIS docs](https://postgis.net/docs/using_postgis_dbmanagement.html).
## Enable the extension
You can get started with PostGIS by enabling the PostGIS extension in your Supabase dashboard.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="database-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for `postgis` and enable the extension.
4. In the confirmation prompt select "Create a new schema" and name it `gis` for example.
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
-- Example: enable the "postgis" extension
create extension postgis with schema "extensions";
-- Example: disable the "postgis" extension
drop extension if exists postgis;
```
</TabPanel>
</Tabs>
## Examples
Now that we are ready to get started with PostGIS, let’s create a table and see how we can use PostGIS for some typical use cases. Imagine we are creating a basic restaurant-searching app.
Let’s create our table. Each row represents a restaurant with its location stored in `location` column as a `Point` type.
```sql
create table if not exists public.restaurants (
id int generated by default as identity primary key,
name text not null,
location extensions.geography(POINT) not null
);
```
We can then set a [spatial index](https://postgis.net/docs/using_postgis_dbmanagement.html#build-indexes) on the `location` column of this table.
```sql
create index restaurants_geo_index
on public.restaurants
using GIST (location);
```
### Inserting data
You can insert geographical data through SQL or through our API.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="data"
queryGroup="language"
>
<TabPanel id="data" label="Data">
<h4>Restaurants</h4>
{/* supa-mdx-lint-disable Rule003Spelling */}
| id | name | location |
| --- | ----------- | -------------------------------- |
| 1 | Supa Burger | lat: 40.807416, long: -73.946823 |
| 2 | Supa Pizza | lat: 40.807475, long: -73.94581 |
| 3 | Supa Taco | lat: 40.80629, long: -73.945826 |
{/* supa-mdx-lint-enable Rule003Spelling */}
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
insert into public.restaurants
(name, location)
values
('Supa Burger', extensions.st_point(-73.946823, 40.807416)),
('Supa Pizza', extensions.st_point(-73.94581, 40.807475)),
('Supa Taco', extensions.st_point(-73.945826, 40.80629));
```
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { error } = await supabase.from('restaurants').insert([
{
name: 'Supa Burger',
location: 'POINT(-73.946823 40.807416)',
},
{
name: 'Supa Pizza',
location: 'POINT(-73.94581 40.807475)',
},
{
name: 'Supa Taco',
location: 'POINT(-73.945826 40.80629)',
},
])
```
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
await supabase.from('restaurants').insert([
{
'name': 'Supa Burger',
'location': 'POINT(-73.946823 40.807416)',
},
{
'name': 'Supa Pizza',
'location': 'POINT(-73.94581 40.807475)',
},
{
'name': 'Supa Taco',
'location': 'POINT(-73.945826 40.80629)',
},
]);
```
</TabPanel>
</$Show>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
struct Restaurant: Codable {
let name: String
let location: String // You could also use a custom type with a custom `Encodable` conformance for convenience.
}
try await supabase.from("restaurants")
.insert(
[
Restaurant(name: "Supa Burger", location: "POINT(-73.946823 40.807416)"),
Restaurant(name: "Supa Pizza", location: "POINT(-73.94581 40.807475)"),
Restaurant(name: "Supa Taco", location: "POINT(-73.945826 40.80629)"),
]
)
.execute()
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
@Serializable
data class Restaurant(
val name: String,
val location: String //you could also use a custom type with a custom serializer for more type safety
)
```
```kotlin
val data = supabase.from("restaurants").insert(listOf(
Restaurant("Supa Burger", "POINT(-73.946823 40.807416)"),
Restaurant("Supa Pizza", "POINT(-73.94581 40.807475)"),
Restaurant("Supa Taco", "POINT(-73.945826 40.80629)"),
))
```
</TabPanel>
</$Show>
</Tabs>
Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is when inserting data from the client library, there is no comma between the two values, just a single space.
At this point, if you go into your Supabase dashboard and look at the data, you will notice that the value of the `location` column looks something like this.
```
0101000020E6100000A4DFBE0E9C91614044FAEDEBC0494240
```
We can query the `restaurants` table directly, but it will return the `location` column in the format you see above.
We will create [database functions](/docs/guides/database/functions) so that we can use the [st_y()](https://postgis.net/docs/ST_Y.html) and [st_x()](https://postgis.net/docs/ST_X.html) function to convert it back to lat and long floating values.
### Order by distance
Sorting datasets from closest to farthest, sometimes called nearest-neighbor sort, is a very common use case in Geo-queries. PostGIS can handle it with the use of the [`<->`](https://postgis.net/docs/geometry_distance_knn.html) operator. `<->` operator returns the two-dimensional distance between two geometries and will utilize the spatial index when used within `order by` clause. You can create the following database function to sort the restaurants from closest to farthest by passing the current locations as parameters.
```sql
create or replace function nearby_restaurants(lat float, long float)
returns table (id public.restaurants.id%TYPE, name public.restaurants.name%TYPE, lat float, long float, dist_meters float)
set search_path = ''
language sql
as $$
select id, name, extensions.st_y(location::extensions.geometry) as lat, extensions.st_x(location::extensions.geometry) as long, extensions.st_distance(location, extensions.st_point(long, lat)::extensions.geography) as dist_meters
from public.restaurants
order by location operator(extensions.<->) extensions.st_point(long, lat)::extensions.geography;
$$;
```
Now you can call this function from your client using `rpc()` like this:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.rpc('nearby_restaurants', {
lat: 40.807313,
long: -73.946713,
})
```
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.rpc('nearby_restaurants',params: {
'lat': 40.807313,
'long': -73.946713,
});
```
</TabPanel>
</$Show>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
struct Response: Codable {
let id: Int
let name: String
let lat: Double
let long: Double
let distance: Double
enum CodingKeys: String, CodingKey {
case id, name, lat, long
case distance = "dist_meters"
}
}
let response: Response = try await supabase.rpc(
"nearby_restaurants",
params: [
"lat": 40.807313,
"long": -73.946713
]
)
.execute()
.value
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
val data = supabase.postgrest.rpc(
function = "nearby_restaurants",
parameters = buildJsonObject { //You can put here any serializable object including your own classes
put("lat", 40.807313)
put("lon", -73.946713)
}
)
```
</TabPanel>
</$Show>
<TabPanel id="result" label="Result">
```json
[
{
"id": 1,
"name": "Supa Burger",
"lat": 40.807416,
"long": -73.946823,
"dist_meters": 14.73033739
},
{
"id": 2,
"name": "Supa Pizza",
"lat": 40.807475,
"long": -73.94581,
"dist_meters": 78.28980007
},
{
"id": 3,
"name": "Supa Taco",
"lat": 40.80629,
"long": -73.945826,
"dist_meters": 136.04329002
}
]
```
</TabPanel>
</Tabs>
### Finding all data points within a bounding box
![Searching within a bounding box of a map](/docs/img/guides/database/extensions/postgis/map.png)
When you are working on a map-based application where the user scrolls through your map, you might want to load the data that lies within the bounding box of the map every time your users scroll. PostGIS can return the rows that are within the bounding box just by supplying the bottom left and the top right coordinates. Let’s look at what the function would look like:
```sql
create or replace function restaurants_in_view(min_lat float, min_long float, max_lat float, max_long float)
returns table (id public.restaurants.id%TYPE, name public.restaurants.name%TYPE, lat float, long float)
set search_path to ''
language sql
as $$
select id, name, extensions.st_y(location::extensions.geometry) as lat, extensions.st_x(location::extensions.geometry) as long
from public.restaurants
where location operator(extensions.&&) extensions.ST_SetSRID(extensions.ST_MakeBox2D(extensions.ST_Point(min_long, min_lat), extensions.ST_Point(max_long, max_lat)), 4326)
$$;
```
The [`&&`](https://postgis.net/docs/geometry_overlaps.html) operator used in the `where` statement here returns a boolean of whether the bounding box of the two geometries intersect or not. We are basically creating a bounding box from the two points and finding those points that fall under the bounding box. We are also utilizing a few different PostGIS functions:
- [ST_MakeBox2D](https://postgis.net/docs/ST_MakeBox2D.html): Creates a 2-dimensional box from two points.
- [ST_SetSRID](https://postgis.net/docs/ST_SetSRID.html): Sets the [SRID](https://postgis.net/docs/manual-dev/using_postgis_dbmanagement.html#spatial_ref_sys), which is an identifier of what coordinate system to use for the geometry. 4326 is the standard longitude and latitude coordinate system.
You can call this function from your client using `rpc()` like this:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.rpc('restaurants_in_view', {
min_lat: 40.807,
min_long: -73.946,
max_lat: 40.808,
max_long: -73.945,
})
```
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.rpc('restaurants_in_view', params: {
'min_lat': 40.807,
'min_long': -73.946,
'max_lat': 40.808,
'max_long': -73.945,
});
```
</TabPanel>
</$Show>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
struct Response: Codable {
let id: Int
let name: String
let lat: Double
let long: Double
}
let response: Response = try await supabase.rpc(
"restaurants_in_view",
params: [
"min_lat": 40.807,
"min_long": -73.946,
"max_long": -73.945,
"max_lat": 40.808,
]
)
.execute()
.value
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
val data = supabase.postgrest.rpc(
function = "restaurants_in_view",
parameters = buildJsonObject { //You can put here any serializable object including your own classes
put("min_lat", 40.807)
put("min_lon", -73.946)
put("max_lat", 40.808)
put("max_lon", -73.945)
}
)
```
</TabPanel>
</$Show>
<TabPanel id="result" label="Result">
```json
[
{
"id": 2,
"name": "Supa Pizza",
"lat": 40.807475,
"long": -73.94581
}
]
```
</TabPanel>
</Tabs>
## Troubleshooting
As of PostGIS 2.3 or newer, the PostGIS extension is no longer relocatable from one schema to another. If you need to move it from one schema to another for any reason (e.g. from the public schema to the extensions schema for security reasons), you would normally run a ALTER EXTENSION to relocate the schema. However, you will now to do the following steps:
1. Backup your Database to prevent data loss - You can do this through the [CLI](/docs/reference/cli/supabase-db-dump) or Postgres backup tools such as [pg_dumpall](https://www.postgresql.org/docs/current/backup-dump.html#BACKUP-DUMP-ALL)
2. Drop all dependencies you created and the PostGIS extension - `DROP EXTENSION postgis CASCADE;`
3. Enable PostGIS extension in the new schema - `CREATE EXTENSION postgis SCHEMA extensions;`
4. Restore dropped data via the Backup if necessary from step 1 with your tool of choice.
Alternatively, you can contact the [Supabase Support Team](/dashboard/support/new) and ask them to run the following SQL on your instance:
```sql
BEGIN;
UPDATE pg_extension
SET extrelocatable = true
WHERE extname = 'postgis';
ALTER EXTENSION postgis
SET SCHEMA extensions;
ALTER EXTENSION postgis
UPDATE TO "<POSTGIS_VERSION>next";
ALTER EXTENSION postgis UPDATE;
UPDATE pg_extension
SET extrelocatable = false
WHERE extname = 'postgis';
COMMIT;
```
## Resources
- [Official PostGIS documentation](https://postgis.net/documentation/)