Skip to Content
Tile Server

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:

  1. Set the appropriate layer type (point, line, fill)
  2. Use the correct source-layer (defaults to ‘default’)
  3. 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:

#PurposePseudocodeActual Code
1Define inputs and outputsDEFINE FUNCTION parks_name(tile_params, optional_filter) RETURNS vector_tileCREATE OR REPLACE FUNCTION public.parks_name(z integer, x integer, y integer, name_prefix text default 'B') RETURNS bytea AS $$
2Calculate tile boundsbounds = COMPUTE_TILE_BOUNDS(tile_params)WITH bounds AS (SELECT ST_TileEnvelope(z, x, y) AS geom),
3Filter & 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) || '%')),
4Generate the vector tilevector_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:

  1. Intercept tile requests
  2. Add the Authorization header with your access token
  3. 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
Last updated on