Welcome to Lynker Spatial Tiles
Access geospatial tile data for watershed analysis, flood modeling, land cover classification, and more.
Get Started Now
Service Overview
The Lynker Spatial Tile Service provides cloud-hosted vector and raster tile datasets optimized for web and desktop applications. All data is served in Protocol Buffer (PBF) format with JWT Bearer token authentication.
Service Base URL: https://tiles.lynker-spatial.com
Available Datasets
- Modeling Hydrofabric (
lynker-spatial-modeling-fabric) — Next-generation hydrographic reference network for distributed hydrologic modeling
- Continental Rivers (
rivers) — National river network optimized for visualization and spatial analysis
- National Wetlands (
wetlands) — Wetland ecosystems and habitats across the continent (USFWS)
- WOTUS Flowlines (
wotus-flowlines) — Clean Water Act regulated flowlines for environmental compliance
- Protected Lands Database (
padus) — Protected Areas Database of the United States (USGS)
- US Reservoirs (
reservoirs) — Major reservoir locations, polygons, and metadata (USGS)
- Flow Divergences (
divergences) — Continental-scale flow divergence patterns for hydrologic routing
- 3D Hydronet (
3dhp-non-river) — Three-dimensional hydrographic features including non-river waterways
- Mystic Flood Mapping (
mystic) — High-resolution flood inundation extent from Hurricane Mystic
- Mystic Live Polygons (
mystic-live) — Real-time flood extent polygons from active Hurricane Mystic monitoring
- Helene Flood Extents (
helene) — Flood inundation mapping from Hurricane Helene optical remote sensing
- HUC 10190007 Flood Polygons (
huc-10190007) — Fort Collins HUC-8 staged flood polygons for inundation mapping
- FloodFabric FoCo Flood Polygons (
floodfabric-foco) — Fort Collins staged flood polygons for FloodFabric live/hindcast rendering
Key Features
- Vector tiles in PMTiles/MVT format for efficient delivery
- Zoom level support from 0 to 28 for detailed mapping
- JWT Bearer token authentication for secure access
- Monthly data quota per token (3 GB) with production request throttling handled by CDN/API layer
- RESTful API with catalog discovery
- Support for all major Web GIS platforms
- Low-latency delivery via AWS CloudFront CDN
Getting Started
Start in 5 Minutes (Recommended)
Use this exact endpoint and layer as your default first integration target.
Step 1 — Get a token (Python)
# pip install boto3
import boto3, os
def get_token(username, password):
client = boto3.client('cognito-idp', region_name='us-west-2')
resp = client.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={'USERNAME': username, 'PASSWORD': password},
ClientId='73r3fn6gi8f3qhha2m5n0197v4'
)
return resp['AuthenticationResult']['AccessToken']
TOKEN = get_token(
os.environ['LYNKER_USERNAME'],
os.environ['LYNKER_PASSWORD']
)
print(TOKEN) # copy into LYNKER_TOKEN, or use directly below
Step 2 — Verify token + service
curl -H "Authorization: Bearer $LYNKER_TOKEN" \
https://tiles.lynker-spatial.com/catalog
Step 3 — Pull a tile
curl -H "Authorization: Bearer $LYNKER_TOKEN" \
https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11 \
-o tile.pbf
Step 4 — MapLibre source config
tiles: ['https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}']
'source-layer': 'flowpath'
Token refresh: Access tokens expire after 1 day. Call get_token() again or use the RefreshToken from AuthenticationResult with AuthFlow='REFRESH_TOKEN_AUTH' to avoid re-entering credentials.
The Lynker Spatial Tile Service consists of:
- Base URL: https://tiles.lynker-spatial.com
- Tile Format: PBF (Protocol Buffers) / PMTiles
- Authentication: JWT Bearer Token (expires per Cognito app client; this deployment uses 1 day)
- Default Projection: Web Mercator (EPSG:3857)
- Tile Naming: {source}/{z}/{x}/{y} (omit .pbf extension)
Quick Start: Five Steps
Quick Setup Checklist
Verify your email (if new account)
Retrieve your token via the Python snippet above (boto3) or copy it from the portal after signing in
Export it: export LYNKER_TOKEN=<your token>
Verify access:
curl -H "Authorization: Bearer $LYNKER_TOKEN" \
https://tiles.lynker-spatial.com/catalog
System Requirements
- Internet Connection: HTTPS connections required
- HTTP/2 Support: Recommended for efficient tile streaming
- TLS 1.2+: For secure authentication
- Standard GIS Software: Works with MapLibre, QGIS, ArcGIS, Leaflet, etc.
Sign Up & Account Setup
📧 Step 1: Create or Sign In
Visit the Lynker Spatial authentication portal to create an account or sign in.
Go to Portal
✓ Step 2: Verify Email
Check your email inbox for a verification link and confirm your account (if new).
🔑 Step 3: Get Your Token
Use the Python boto3 snippet in the Quick Start above, or sign in at the portal and copy your token from the session.
🚀 Step 4: Start Using Tiles
Use your token in the Authorization header to access all tile endpoints.
Token Management
Retrieve your token programmatically with boto3 (see Quick Start) or copy it from the portal after signing in. Tokens are user-specific — one token per user account, not shared across an organization.
- Access tokens expire after 1 day; refresh tokens expire after 30 days
- Read the
expires_in field and refresh proactively — do not poll until 401
- Refresh without re-entering credentials using
AuthFlow='REFRESH_TOKEN_AUTH' and the RefreshToken from your last auth response
- Use in request header:
Authorization: Bearer <token>
Authentication & Authorization
All requests to the Lynker Spatial Tile Service require JWT Bearer token authentication. Tokens are issued through AWS Cognito; token lifetimes vary by Cognito app client. This deployment uses 1 day access/ID tokens and 30 day refresh tokens — configure your client to refresh tokens proactively based on the returned `expires_in`.
Getting Your Token
Programmatic (recommended for scripts and pipelines):
import boto3, os
def get_token(username, password):
client = boto3.client('cognito-idp', region_name='us-west-2')
resp = client.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={'USERNAME': username, 'PASSWORD': password},
ClientId='73r3fn6gi8f3qhha2m5n0197v4'
)
auth = resp['AuthenticationResult']
return auth['AccessToken'], auth['RefreshToken']
token, refresh = get_token(
os.environ['LYNKER_USERNAME'],
os.environ['LYNKER_PASSWORD']
)
# Later, refresh without re-entering credentials:
def refresh_token(refresh_tok):
client = boto3.client('cognito-idp', region_name='us-west-2')
resp = client.initiate_auth(
AuthFlow='REFRESH_TOKEN_AUTH',
AuthParameters={'REFRESH_TOKEN': refresh_tok},
ClientId='73r3fn6gi8f3qhha2m5n0197v4'
)
return resp['AuthenticationResult']['AccessToken']
Tokens expire after 1 day. Store the RefreshToken securely (it lasts 30 days) and call refresh_token() before making tile requests rather than re-authenticating from scratch each time.
Using Your Token
Include the token in the Authorization header for all tile service requests:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11
Security Note: Access tokens expire after 1 day; refresh tokens expire after 30 days. Read the token's expires_in field and implement proactive refresh logic a few minutes before expiry. Never commit tokens to version control.
Token Refresh Pattern
Implement automatic token refresh to handle expiration:
// Example: Node.js token refresh
async function getValidToken() {
const cached = getCachedToken();
if (cached && !isExpired(cached)) {
return cached;
}
const newToken = await requestNewToken();
cacheToken(newToken);
return newToken;
}
// Use in requests
const token = await getValidToken();
// For catalog discovery the Authorization header is optional; include it when available
fetch('https://tiles.lynker-spatial.com/catalog', {
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
});
Testing Authentication
Verify your setup works:
# Test 1: Get service catalog (requires valid token)
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://tiles.lynker-spatial.com/catalog
# Test 2: Request a specific tile (note: no .pbf extension in URL)
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11 \
-o tile.pbf
# Verify the file is valid
file tile.pbf
# Should output: tile.pbf: Protocol Buffer data
Access Control
Access is controlled by token validity and monthly quota, not by fine-grained scopes. A valid JWT grants read access to all tile sources. Quota consumption is tracked per token by JWT ID (jti claim) and resets monthly. Enterprise API keys (X-Api-Key / X-Enterprise-Key) bypass JWT validation and quota tracking.
MapLibre GL JS Integration
MapLibre GL JS is a free, open-source JavaScript library for interactive maps. It provides excellent support for vector tiles and is ideal for web applications.
Installation
# Via npm
npm install maplibre-gl
# Via CDN (for simple projects)
<script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />
Basic Map with Tiles
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title>Lynker Tiles with MapLibre</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id='map'></div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-100, 40],
zoom: 6
});
map.on('load', () => {
map.addSource('hydrofabric', {
type: 'vector',
tiles: ['https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}'],
minzoom: 0,
maxzoom: 14
});
map.addLayer({
id: 'flowpaths',
type: 'line',
source: 'hydrofabric',
'source-layer': 'flowpath',
paint: {
'line-color': '#0084ff',
'line-width': 1.5,
'line-opacity': 0.8
}
});
});
</script>
</body>
</html>
Authentication in MapLibre
Since MapLibre runs in the browser, handle authentication through a proxy server:
// Option 1: Proxy tile requests through your backend
const tileUrl = '/api/proxy/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}';
map.addSource('hydrofabric', {
type: 'vector',
tiles: [tileUrl],
minzoom: 0,
maxzoom: 14
});
// Your backend proxy adds the Authorization header
// Example Express.js proxy:
app.get('/api/proxy/tiles/lynker-spatial-modeling-fabric/:z/:x/:y', (req, res) => {
const { z, x, y } = req.params;
const tileUrl = `https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/${z}/${x}/${y}`;
fetch(tileUrl, {
headers: { 'Authorization': `Bearer ${process.env.LYNKER_TOKEN}` }
})
.then(r => r.buffer())
.then(buffer => {
res.set('Content-Type', 'application/octet-stream');
res.send(buffer);
})
.catch(err => res.status(500).json({ error: err.message }));
});
Advanced Styling
Use data-driven styling to create thematic maps — for example, scaling line width by stream order:
map.addLayer({
id: 'flowpaths-styled',
type: 'line',
source: 'hydrofabric',
'source-layer': 'flowpath',
paint: {
'line-color': '#0084ff',
'line-width': [
'interpolate', ['linear'], ['get', 'order_'],
1, 0.5,
4, 2,
7, 5
],
'line-opacity': [
'case',
['boolean', ['feature-state', 'hover'], false],
1.0,
0.7
]
}
});
// Highlight on hover
map.on('mousemove', 'flowpaths-styled', (e) => {
if (e.features.length > 0) {
map.setFeatureState(
{ source: 'hydrofabric', id: e.features[0].id },
{ hover: true }
);
}
});
Interactivity
// Click handler for feature information
map.on('click', 'flowpaths', (e) => {
const properties = e.features[0].properties;
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(`
<h3>Flowpath</h3>
<p>ID: ${properties.id}</p>
<p>Length: ${properties.lengthkm} km</p>
`)
.addTo(map);
});
// Change cursor on hover
map.on('mouseenter', 'flowpaths', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'flowpaths', () => {
map.getCanvas().style.cursor = '';
});
QGIS Integration
QGIS 3.x has native support for vector tiles. Here are three methods to add Lynker Spatial tiles to your QGIS projects.
Method 1: Vector Tile Layer (GUI)
Step-by-Step Instructions
Open QGIS and go to Layer → Add Layer → Add Vector Tile Layer
Click the New button to create a new connection
Enter connection details:
Name: Lynker Hydrofabric
URL: https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y} (omit .pbf extension)
Go to the HTTP Headers tab
Add custom header:
Header Name: Authorization
Header Value: Bearer YOUR_ACCESS_TOKEN
Click OK and save the connection
Select the connection from the list and add the layer
Configure symbology by right-clicking the layer → Properties → Symbology
Method 2: Python Console
from qgis.core import QgsVectorTileLayer, QgsProject
from qgis.utils import iface
# Create vector tile layer (note: no .pbf extension in URL)
url = 'https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}'
layer = QgsVectorTileLayer(url, 'Modeling Hydrofabric')
# Add to project
QgsProject.instance().addMapLayer(layer)
print(f"Layer added: {layer.name()}")
Method 3: Custom Python Plugin
For more control, create a custom plugin:
import requests
from qgis.core import QgsVectorTileLayer, QgsProject
class LynkerTileManager:
def __init__(self, token=None):
self.token = token
self.base_url = 'https://tiles.lynker-spatial.com'
def get_catalog(self):
"""Fetch available tile sources (catalog is public; Authorization optional)"""
headers = {'Authorization': f'Bearer {self.token}'} if self.token else {}
response = requests.get(f'{self.base_url}/catalog', headers=headers)
return response.json()
def add_tile_layer(self, source_name, display_name=None):
"""Add a tile layer to QGIS"""
if display_name is None:
display_name = source_name
url = f'{self.base_url}/api/tiles/{source_name}/{{z}}/{{x}}/{{y}}' # no .pbf extension
layer = QgsVectorTileLayer(url, display_name)
QgsProject.instance().addMapLayer(layer)
return layer
# Usage
manager = LynkerTileManager('YOUR_TOKEN')
manager.add_tile_layer('lynker-spatial-modeling-fabric', 'Modeling Hydrofabric')
manager.add_tile_layer('wetlands', 'Wetlands')
Styling Vector Tiles in QGIS
- Right-click the layer → Properties
- Go to the Symbology tab
- Select Vector Tile Rendering style
- Configure colors, opacity, and outlines for each feature type
- Use Rule-based rendering for conditional styling
Troubleshooting
- No tiles appearing: Check Authorization header is correct, verify token hasn't expired
- Layer visible at all zoom levels: Check minzoom/maxzoom settings in layer properties
- Styling issues: Ensure layer names in symbology match actual feature types in tileset
ArcGIS Integration
Add Lynker Spatial tiles to ArcGIS Pro, ArcGIS Online, and ArcMap using multiple connection methods.
ArcGIS Pro Setup
Adding a Vector Tile Service
In ArcGIS Pro, go to Insert → Connections → New Vector Tile Service Connection
Enter the service URL and configure authentication
Add the connection to your project
Drag the connection into your map to add layers
ArcGIS Online
In ArcGIS Online Map Viewer:
- Click "Add" → "Add Layer from Web"
- Select "Vector Tile Service"
- URL:
https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}
- Add Authorization header:
Bearer YOUR_TOKEN
Note: The tile service does not expose a WMS or WMTS endpoint. All datasets are served as vector tiles (MVT/PBF) only. ArcGIS Pro 3.x and ArcGIS Online support vector tile connections natively.
Configuring Proxy for Enterprise
For enterprise deployments, configure a proxy server:
# proxy.ashx or similar proxy configuration
# Note: Use Bearer token in Authorization header instead of ClientId/Secret
QueryableLayerIds: 0,1,2,3,4,5
LogFile: C:\logs\proxy.log
LogLevel: All
ServerUrl: https://tiles.lynker-spatial.com
AllowedReferrers: yourdomain.com
AllowedPorts: 443
HasToken: true
# Get this token from https://proxy.lynker-spatial.com/token
BearerToken: YOUR_JWT_TOKEN_FROM_PORTAL
Leaflet.js Integration
Leaflet is a lightweight, open-source JavaScript library perfect for simple to moderately complex maps.
Installation
# Via npm
npm install leaflet
# Via CDN (easiest for simple projects)
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
Basic Map with Tiles
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
#map { height: 600px; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([40, -100], 6);
// Base layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
// Lynker tile overlay through proxy
const tileLayer = L.tileLayer('/api/tiles/{z}/{x}/{y}', { // proxy handles auth
attribution: '© Lynker Spatial',
minZoom: 0,
maxZoom: 14,
opacity: 0.7
}).addTo(map);
</script>
</body>
</html>
Proxy Setup for Leaflet
Implement a backend proxy to handle authentication:
const token = process.env.LYNKER_TOKEN;
app.get('/api/tiles/:z/:x/:y', async (req, res) => {
const { z, x, y } = req.params;
const url = `https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/${z}/${x}/${y}`;
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
res.set('Content-Type', 'application/octet-stream');
res.send(Buffer.from(await response.arrayBuffer()));
});
Leaflet vs MapLibre Comparison
| Aspect |
Leaflet |
MapLibre GL JS |
| Bundle Size |
~39 KB gzipped |
~70 KB gzipped |
| Learning Curve |
Easy |
Moderate |
| 3D Support |
No |
Yes |
| Vector Tile Support |
Basic (via proxy) |
Native & Advanced |
| Best Use Case |
Simple web maps |
Complex styling & effects |
Folium & Python Jupyter
Folium bridges Python and Leaflet.js, making it easy to create interactive maps in Jupyter notebooks.
Installation
pip install folium
# Optional: for enhanced functionality
pip install folium geopandas shapely
Basic Map with Tiles
import folium
# Create base map
m = folium.Map(location=[40, -100], zoom_start=6)
# Add tile layer via proxy (backend handles token)
folium.TileLayer(
tiles='/api/tiles/{z}/{x}/{y}',
attr='© Lynker Spatial',
name='Protected Areas'
).add_to(m)
folium.LayerControl().add_to(m)
m.save('map.html')
Working with GeoPandas
import folium
import geopandas as gpd
from shapely.geometry import Point, box
# Load vector data
gdf = gpd.read_file('protected_areas.geojson')
# Create map centered on data
bounds = gdf.total_bounds
center = [(bounds[1] + bounds[3])/2, (bounds[0] + bounds[2])/2]
m = folium.Map(location=center, zoom_start=6)
# Add GeoDataFrame as GeoJson layer
folium.GeoJson(
data=gdf,
style_function=lambda x: {
'fillColor': '#088',
'color': '000',
'weight': 1,
'fillOpacity': 0.3
}
).add_to(m)
# Add Lynker tiles as base layer
# (via proxy endpoint)
m.save('geopandas_map.html')
Spatial Data Analysis
import folium
import geopandas as gpd
# Load and analyze spatial data
gdf = gpd.read_file('data.geojson')
center = [gdf.total_bounds[1], gdf.total_bounds[0]]
m = folium.Map(location=center, zoom_start=6)
folium.GeoJson(data=gdf).add_to(m)
m.save('map.html')
OpenLayers Integration
OpenLayers is a powerful, enterprise-grade mapping library suitable for complex GIS web applications.
Installation
# Via npm
npm install ol
# Via CDN
<script src="https://cdn.jsdelivr.net/npm/ol@latest/dist/ol.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@latest/ol.css" />
Basic Map Setup
import Map from 'ol/Map';
import OSM from 'ol/source/OSM';
import TileLayer from 'ol/layer/Tile';
import VectorTileLayer from 'ol/layer/VectorTile';
import MVTFormat from 'ol/format/MVT';
import View from 'ol/View';
import { fromLonLat } from 'ol/proj';
// Create a map instance
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OSM()
})
],
view: new View({
center: fromLonLat([-100, 40]),
zoom: 6
})
});
// Add vector tile layer
const vectorTileLayer = new VectorTileLayer({
source: new VectorTile({
format: new MVTFormat(),
// Canonical tile URL: /{source}/{z}/{x}/{y} (omit .pbf extension)
url: 'https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}',
minZoom: 0,
maxZoom: 14
}),
style: {
'fill-color': '#088',
'stroke-color': '#000',
'stroke-width': 1,
'fill-opacity': 0.3
}
});
map.addLayer(vectorTileLayer);
Advanced Styling
import { Style, Fill, Stroke, Icon } from 'ol/style';
const styleFunction = (feature) => {
const protectionLevel = feature.get('protection_level');
let fillColor;
switch(protectionLevel) {
case 3:
fillColor = '#cb181d';
break;
case 2:
fillColor = '#fb6a4a';
break;
case 1:
fillColor = '#fcae91';
break;
default:
fillColor = '#fee5d9';
}
return new Style({
fill: new Fill({
color: fillColor
}),
stroke: new Stroke({
color: '#000',
width: 1
})
});
};
vectorTileLayer.setStyle(styleFunction);
R Language Integration
R users can access Lynker Spatial tiles through several packages for geospatial analysis.
Installation
# Install required packages
install.packages('leaflet')
install.packages('sf')
install.packages('ggplot2')
Basic Map with Leaflet
library(leaflet)
m <- leaflet() %>%
setView(lng = -100, lat = 40, zoom = 6) %>%
addTiles() %>%
addTiles(
# Canonical direct tile URL (omit .pbf). For browser use, prefer a proxy
# that injects the Authorization header. Example proxy URL shown elsewhere.
urlTemplate = 'https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}',
attribution = '© Lynker Spatial'
)
m
Working with Spatial Data
library(sf)
library(leaflet)
# Load and display spatial data
counties <- st_read('counties.geojson')
leaflet(counties) %>%
addTiles() %>%
addPolygons(fillColor = ~colorQuantile("viridis", POPULATION)(POPULATION))
Static Maps
library(ggplot2)
library(sf)
states <- st_as_sf(maps::map('state', fill=TRUE, plot=FALSE))
ggplot(states) + geom_sf(aes(fill = NAME), alpha = 0.7) + theme_minimal()
Working with Raster Data
library(leaflet)
# Display raster tiles with Leaflet
leaflet() %>%
addTiles() %>%
addRasterImage(rasterData, opacity = 0.7)
OAuth2 Authentication Setup
For production tile access with secure authentication, Lynker Spatial provides an OAuth2-enabled tile proxy. This allows browsers and desktop applications to authenticate once and access tiles without manual token handling.
Why OAuth2?
- User-Friendly: Sign in once via browser OAuth flow
- Secure: Tokens stored in HttpOnly cookies (not accessible to JavaScript)
- Automatic Refresh: Tokens refresh automatically before expiration
- No Manual Headers: Authentication handled server-side by the proxy
- PKCE Protected: RFC 7636 compliant protection against authorization code interception
Architecture Overview
Browser Application
↓
OAuth2 Tile Proxy (https://tiles.lynker-spatial.com)
• Handles OAuth2 login/logout
• Manages secure token storage
• Enforces PKCE authentication
• Injects tokens into tile requests
↓
Upstream Token Service (Cognito, Azure AD, or OIDC provider)
• Issues & validates JWT tokens
↓
Lynker Spatial Tile CDN
• Serves tiles with verified tokens
Quick Start: Browser-Based OAuth2 Flow
1. Check Status: GET /auth/session
2. Login: GET /auth/login?redirectTo=/home (in browser)
3. OAuth Provider: User signs in at OAuth provider
4. Redirect Back: Returns to /home with session cookie
5. Fetch Tiles: Browser now has authentication
6. Logout: POST /auth/logout to clear session
Client-Side Implementation (JavaScript/MapLibre)
const PROXY_URL = 'https://tiles.lynker-spatial.com';
// Check auth status
async function checkAuth() {
const response = await fetch(`${PROXY_URL}/auth/session`, {
credentials: 'include' // Include cookies
});
return await response.json();
}
// Login
function login() {
// Redirect to OAuth provider
window.location.href = `${PROXY_URL}/auth/login?redirectTo=${encodeURIComponent(window.location.pathname)}`;
}
// Logout
async function logout() {
await fetch(`${PROXY_URL}/auth/logout`, {
method: 'POST',
credentials: 'include'
});
// Refresh auth status
window.location.reload();
}
// Add tiles after authentication
async function initMap() {
const session = await checkAuth();
if (!session.authenticated) {
showLoginButton();
return;
}
// User is authenticated - add tile layers
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-100, 40],
zoom: 4
});
map.on('load', () => {
map.addSource('lynker-tiles', {
type: 'vector',
tiles: [`${PROXY_URL}/api/tiles/lynker-spatial-modeling-fabric/{z}/{x}/{y}`],
minzoom: 0,
maxzoom: 14
});
map.addLayer({
id: 'flowpaths',
type: 'line',
source: 'lynker-tiles',
'source-layer': 'flowpath',
paint: {
'line-color': '#0084ff',
'line-width': 1.5
}
});
});
}
initMap();
API Endpoints
| Method |
Endpoint |
Auth Required |
Description |
| GET |
/health |
❌ No |
Service health check |
| GET |
/catalog |
❌ No |
List all available datasets with metadata |
| GET |
/:dataset |
❌ No |
Get TileJSON metadata for specific dataset (e.g., /padus, /wetlands) |
| GET |
/auth/session |
❌ No |
Check authentication status and session info |
| GET |
/auth/login?redirectTo=<path> |
❌ No |
Start OAuth2 authentication flow (browser) |
| GET |
/auth/callback?code=<code>&state=<state> |
❌ No |
OAuth provider redirect callback (automatic) |
| POST |
/auth/logout |
✅ Yes |
Clear session and logout user |
| GET |
/api/tiles/:dataset/:z/:x/:y |
✅ Yes |
Fetch tile (requires session authentication) |
Configuration & Deployment
For API endpoint reference and quick-start curl commands, see QUICK_REFERENCE.md in the tiles-proxy directory.
Security Features
- PKCE (RFC 7636): Code challenge/verifier prevents authorization code interception
- CSRF Protection: State and nonce validation prevents cross-site attacks
- HttpOnly Cookies: Tokens never accessible to JavaScript (prevents XSS)
- Automatic Token Refresh: Refreshes tokens before expiration with backoff retry
- Token Redaction: Tokens automatically redacted in logs
- Secure Session Storage: Redis-ready abstraction for scaling
Advanced Usage Patterns
Direct HTTP Access
Access tiles with curl for testing:
# Download a single tile
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11 \
-o tile.pbf
Simple Python Tile Request
import requests
token = 'YOUR_TOKEN'
url = 'https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11'
response = requests.get(url, headers={'Authorization': f'Bearer {token}'})
if response.status_code == 200:
with open('tile.pbf', 'wb') as f:
f.write(response.content)
API Reference
Base URL
https://tiles.lynker-spatial.com
Core Endpoints
GET /catalog
List all available tile sources and metadata. No authentication required.
curl https://tiles.lynker-spatial.com/catalog
Response (200 OK):
{
"tiles": {
"lynker-spatial-modeling-fabric": {
"name": "Modeling Hydrofabric",
"description": "Next-generation hydrographic reference network designed for distributed hydrologic modeling and water resources analysis.",
"category": "Hydrology",
"attribution": "Lynker Spatial"
},
"rivers": {
"name": "Continental Rivers",
"description": "Comprehensive national river network optimized for visualization and spatial analysis of hydrographic data.",
"category": "Hydrology",
"attribution": "USGS"
},
"wetlands": {
"name": "National Wetlands",
"description": "Comprehensive mapping of wetland ecosystems and habitats across the continent for conservation planning.",
"category": "Environment",
"attribution": "USFWS"
}
// ... all 13 sources
}
}
GET /api/tiles/{source}/{z}/{x}/{y}
Retrieve a vector tile
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://tiles.lynker-spatial.com/api/tiles/lynker-spatial-modeling-fabric/5/9/11 \
-o tile.pbf
Parameters:
- source: Tile dataset identifier (lynker-spatial-modeling-fabric, rivers, wetlands, etc.)
- z: Zoom level (0-14 depending on source)
- x: Tile column index
- y: Tile row index
Response: Binary PBF/MVT data
Status Codes:
- 200: Tile found and returned
- 204: Authorized but no tile data at this coordinate (valid empty response)
- 401: Unauthorized (missing or invalid token)
- 429: Monthly quota exceeded
HTTP Headers
| Header |
Direction |
Required |
Description |
Authorization |
Request |
Yes |
Bearer token: Bearer YOUR_TOKEN |
User-Agent |
Request |
Recommended |
Identify your application |
Accept-Encoding |
Request |
Recommended |
Use gzip for compression |
Content-Type |
Response |
Yes |
application/octet-stream |
X-Quota-Remaining-Bytes |
Response |
Yes |
Remaining bytes in the current monthly token quota |
X-Quota-Total-Bytes |
Response |
Yes |
Total monthly quota in bytes for the token (e.g. 3221225472 = 3 GB) |
Rate Limiting
Enforced limits & behavior:
- Monthly data quota: 3 GB per token (default:
3221225472 bytes). The service returns X-Quota-Remaining-Bytes and X-Quota-Total-Bytes headers on tile responses.
- Request throttling: The application does not enforce a fixed requests-per-minute per token. Request-level throttling and global protections (CDN, API Gateway, WAF) are used in production and may vary by deployment.
- Quota enforcement: When a token exceeds its monthly quota the service returns
HTTP 429 Too Many Requests. Implement local caching and exponential backoff to avoid hitting quotas.
Error Responses
# 401 Unauthorized
{
"error": "Unauthorized",
"message": "Invalid or expired token",
"status": 401
}
# 404 Not Found
{
"error": "Not Found",
"message": "Tile not found at coordinates z/x/y",
"status": 404
}
# 429 Too Many Requests
{
"error": "Rate Limited",
"message": "Too many requests. Retry after 60 seconds.",
"status": 429,
"retry_after": 60
}
Best Practices for API Calls
- Implement connection pooling to reuse HTTP connections
- Use HTTP/2 for multiplexing multiple tile requests
- Enable gzip compression in Accept-Encoding
- Cache tiles locally to reduce API calls
- Implement exponential backoff for rate limit retries
- Monitor X-Quota-Remaining-Bytes header to avoid hitting quota limits
- Set appropriate timeout values (30 seconds recommended)
- Include meaningful User-Agent for analytics
FAQs
How do I get a token?
Use the boto3 snippet in the Quick Start section — pass your portal username and password and it returns your AccessToken and RefreshToken directly. No browser required for scripts.
What URL format should I use?
Tile URLs use the pattern https://tiles.lynker-spatial.com/api/tiles/{source}/{z}/{x}/{y}. Do not include the .pbf extension in MapLibre or client configs.
What happens when I hit quota limits?
API responses will return 429 Too Many Requests and include X-Quota-Remaining-Bytes and X-Quota-Total-Bytes headers. Implement exponential backoff and caching to avoid exhausting your monthly quota.