Tile Server
Geobase automatically generates vector tile endpoints for your spatial data, making it easy to visualize your geographic data on interactive web maps.
What are Vector Tiles?
Vector tiles are packets of geographic data, delivered to a map in small chunks. Unlike raster tiles (regular map images), vector tiles contain the raw geometric data, enabling:
- Dynamic styling
- Smooth zoom levels
- Smaller file sizes
- Interactive features
Learn about the supported geometry types in Geobase.
Automatic Tile Server Generation
Geobase creates tile endpoints automatically in two scenarios:
1. Tables with Geometry Columns
Any table containing a geometry
column will automatically get a tile endpoint.
2. Custom Tile Generation Functions
You can create dynamic tile endpoints by defining PostgreSQL functions. These functions must have the following signature:
CREATE OR REPLACE FUNCTION public.get_tiles(
z integer, -- zoom level
x integer, -- tile x coordinate
y integer -- tile y coordinate
)
RETURNS bytea -- returns tile in MVT format
LANGUAGE plpgsql
Function Parameters
Any additional function parameters beyond the required z
, x
, and y
will be exposed as URL query parameters in the tile endpoint. For example:
CREATE OR REPLACE FUNCTION public.get_routing_tiles(
z integer,
x integer,
y integer,
start_point geometry = NULL,
end_point geometry = NULL
)
RETURNS bytea
This function would be accessible at:
https://[project_id].geobase.app/tileserver/v1/rpc.get_routing_tiles/{z}/{x}/{y}.pbf?start_point=POINT(...)&end_point=POINT(...)
Dynamic Return Types
Function-based tile endpoints can return different types of geometries. When implementing the client-side map, make sure to:
- Set the appropriate layer type (
point
,line
,fill
) - Use the correct
source-layer
(defaults to ‘default’) - Adjust the map bounds and center to match your data
Example MapLibre implementation:
const map = new maplibregl.Map({
container: "map",
style: BASE_MAPS.light,
bounds: [-180, -90, 180, 90], // Adjust based on your data extent
});
map.on("load", () => {
map.addSource("routing_tiles", {
type: "vector",
tiles: [
`https://[project_id].geobase.app/tileserver/v1/rpc.get_routing_tiles/{z}/{x}/{y}.pbf?apikey=${apiKey}&start_point=POINT(0 0)`,
],
});
map.addLayer({
id: "routing-layer",
type: "line", // Adjust based on your geometry type
source: "routing_tiles",
"source-layer": "default",
paint: {
"line-color": "#FF0000",
"line-width": 2,
},
});
});
Example function to query parks:
CREATE OR REPLACE
FUNCTION public.parks_name(
z integer, x integer, y integer, -- tile parameters needed for tile server to know which tiles to request
name_prefix text default 'B') -- optional parameter to filter the data by the name prefix
RETURNS bytea -- returns the vector tiles as a bytea
AS $$
WITH
bounds AS (
SELECT ST_TileEnvelope(z, x, y) AS geom
),
mvtgeom AS (
SELECT ST_AsMVTGeom(ST_Transform(t.location, 3857), bounds.geom) AS geom,
t.park
FROM parks t, bounds
WHERE ST_Intersects(t.location, ST_Transform(bounds.geom, 4326))
AND upper(t.park) LIKE (upper(name_prefix) || '%')
)
SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom;
$$
LANGUAGE 'sql'
SET search_path = extensions, public
STABLE PARALLEL SAFE;
COMMENT ON FUNCTION public.parks_name IS 'Filters the parks table by the initial letters of the name using the "name_prefix" parameter.';
Table: Anatomy of a Tile Server Function
A tile server function is a four-step process. So using the example above, we can break it down into the following steps:
# | Purpose | Pseudocode | Actual Code |
---|---|---|---|
1 | Define inputs and outputs | DEFINE FUNCTION parks_name(tile_params, optional_filter) RETURNS vector_tile | CREATE OR REPLACE FUNCTION public.parks_name(z integer, x integer, y integer, name_prefix text default 'B') RETURNS bytea AS $$ |
2 | Calculate tile bounds | bounds = COMPUTE_TILE_BOUNDS(tile_params) | WITH bounds AS (SELECT ST_TileEnvelope(z, x, y) AS geom), |
3 | Filter & transform data (business logic) | filtered_data = APPLY_FILTERS_AND_TRANSFORM(data, bounds, optional_filter) | mvtgeom AS (SELECT ST_AsMVTGeom(ST_Transform(t.location, 3857), bounds.geom) AS geom, t.park FROM parks t, bounds WHERE ST_Intersects(t.location, ST_Transform(bounds.geom, 4326)) AND upper(t.park) LIKE (upper(name_prefix) || '%')), |
4 | Generate the vector tile | vector_tile = GENERATE_VECTOR_TILE(filtered_data) | SELECT ST_AsMVT(mvtgeom, 'default') FROM mvtgeom; |
Using Tile Endpoints
Basic URL Structure
For uncached tiles:
https://[project_id].geobase.app/tileserver/v1/public.[table_name]/{z}/{x}/{y}.pbf?apikey=[ANON_KEY]
For cached tiles:
https://[project_id].geobase.app/tileserver/v1/cached/public.[table_name]/{z}/{x}/{y}.pbf?apikey=[ANON_KEY]
Query Parameters
Properties
Comma-separated list of columns to include in the tiles (helps reduce tile size):
?properties=name,population,area
Filters
Geobase supports Common Query Language (CQL) filters to narrow down the features returned in tiles. Filters are passed via the filter
parameter:
?filter=population > 1000000
Common filter examples:
-- Basic comparisons
population >= 1000000
name = 'Finland'
-- Between ranges
population BETWEEN 1000000 AND 9000000
-- Lists
country IN ('Chile', 'Kenya', 'Denmark')
-- Text matching (% is wildcard)
name LIKE 'San%'
name ILIKE '%america' -- case-insensitive
-- Null checks
description IS NOT NULL
-- Boolean combinations
(continent = 'Europe' OR continent = 'Africa') AND population < 1000000
-- Spatial filters
INTERSECTS(geometry, ENVELOPE(-100, 49, -90, 50))
DWITHIN(geometry, POINT(-100 49), 0.1)
-- Temporal filters
created_at > 2001-01-01T00:00
updated_at BETWEEN '2001-01-01' AND '2001-12-31'
URL Encoding Filters
When using filters in your application, use JavaScript’s built-in encodeURIComponent()
to properly encode the filter parameter:
const filter = "population > 1000000 AND name LIKE 'San%'";
const encodedFilter = encodeURIComponent(filter);
const tileUrl = `https://[project_id].geobase.app/tileserver/v1/public.cities/{z}/{x}/{y}.pbf?apikey=${ANON_KEY}&filter=${encodedFilter}`;
// Use in MapLibre
map.addSource("cities", {
type: "vector",
tiles: [tileUrl],
});
Or when using multiple parameters:
const baseUrl = "https://[project_id].geobase.app/tileserver/v1/public.cities/{z}/{x}/{y}.pbf";
const params = new URLSearchParams({
apikey: ANON_KEY,
filter: "population > 1000000 AND name LIKE 'San%'",
properties: "name,population,geometry",
});
const tileUrl = `${baseUrl}?${params.toString()}`;
Caching
Toggle caching by including /cached/
in the URL path:
https://[project_id].geobase.app/tileserver/v1/cached/public.[table_name]/{z}/{x}/{y}.pbf
This is recommended for static datasets that don’t change frequently.
Authentication and Row Level Security (RLS)
When using tables with Row Level Security (RLS) enabled or accessing private datasets, you’ll need to include authentication headers in your tile requests. This requires special configuration in your map library.
MapLibre GL JS
const transformRequest = (url, resourceType) => {
// Only transform tile requests to your Geobase project
if (
resourceType === "Tile" &&
url.startsWith(geobase.baseUrl) &&
user.auth.token
) {
return {
url,
headers: {
// Include the auth token in the request
Authorization: `Bearer ${user.auth.token}`,
},
};
}
};
// Add the transformRequest function to your map configuration
const map = new maplibregl.Map({
container: "map",
style: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
center: [11.96464, 57.70561],
zoom: 10,
transformRequest: transformRequest, // Add this line
});
Using with Geobase Client
If you’re using the Geobase client library:
import { createClient } from '@geobase/client'
const geobase = createClient(
'https://[project_id].geobase.app',
'[ANON_KEY]'
)
// Sign in user
await geobase.auth.signIn({
email: 'user@example.com',
password: 'password'
})
// The transformRequest function will now include the current session token
const transformRequest = (url, resourceType) => {
if (
resourceType === "Tile" &&
url.startsWith(geobase.baseUrl) &&
geobase.auth.session()
) {
return {
url,
headers: {
Authorization: `Bearer ${geobase.auth.session().access_token}`,
},
};
}
};
Other Map Libraries
For other mapping libraries, you’ll need to find their equivalent method for transforming requests. The key is to:
- Intercept tile requests
- Add the Authorization header with your access token
- Only transform requests to your Geobase project URLs
Integration Examples
MapLibre GL JS
const map = new maplibregl.Map({
container: "map",
style: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
center: [11.96464, 57.70561],
zoom: 10,
});
map.on("load", () => {
map.addSource("buildings", {
type: "vector",
tiles: [`https://[project_id].geobase.app/tileserver/v1/public.buildings/{z}/{x}/{y}.pbf?apikey=${ANON_KEY}`],
});
map.addLayer({
id: "buildings-layer",
type: "fill",
source: "buildings",
"source-layer": "public.buildings",
paint: {
"fill-color": "rgba(123, 168, 234, 0.5)",
"fill-outline-color": "rgba(255, 255, 255, 1)",
},
filter: ["==", "$type", "Polygon"],
});
});
Mapbox GL JS
[Example implementation with Mapbox GL JS]
OpenLayers
[Example implementation with OpenLayers]
Performance Tips
- Make sure you have spatial indices on geometry columns (
gist
index) - Use the
properties
parameter to limit returned attributes - Enable caching for static datasets
- Consider using different zoom level strategies for different data densities
Studio Interface
[Screenshot of the Geobase Studio tile server interface]
The Studio provides a visual interface to:
- View tile endpoint URLs
- Test tile generation
- Configure tile properties
- Monitor usage statistics