How the Custom MapLibre Protocol for Local MBTiles Interacts with Tauri Commands in GeoLibre
GeoLibre registers a custom geolibre-mbtiles:// protocol handler that intercepts MapLibre tile requests and forwards them to Rust-backed Tauri commands, enabling secure, sandboxed access to local SQLite MBTiles databases.
The GeoLibre desktop application solves a critical problem for offline mapping: rendering local MBTiles files in a MapLibre-GL JS frontend without direct filesystem access. By bridging MapLibre's protocol system with Tauri's command interface, the architecture keeps all file I/O in the trusted Rust backend while the web UI remains sandboxed.
Architecture Overview: Custom Protocol to Tauri Command Flow
The interaction follows a six-step pipeline from user selection to tile rendering:
- User selects MBTiles file → UI triggers file dialog
- Metadata extraction → Tauri command queries SQLite headers
- Protocol registration → Custom handler installed in MapLibre
- Tile request interception → URL parsed, parameters extracted
- Backend tile retrieval → Rust command fetches binary blob
- MapLibre rendering → Raw bytes decoded as raster or vector
This design ensures that browser security restrictions never block local file access—the desktop shell handles all database operations.
UI Layer: Selecting Files and Initiating the Protocol
The process begins in [MbtilesSource.tsx](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/components/layout/add-data/sources/MbtilesSource.tsx), which renders the Add Data interface for local MBTiles layers.
The component orchestrates two critical operations before any tiles load:
// Open native file picker with fallback
const selectedPath = await openLocalDataFileWithFallback({
name: 'MBTiles',
extensions: ['mbtiles']
});
// Fetch metadata to determine layer properties
const metadata = await invoke<MbtilesMetadata>('read_mbtiles_metadata', {
path: selectedPath
});
The metadata response includes name, format (png, webp, pbf), tile type (raster/vector), bounds, minzoom, and maxzoom—all required to configure the MapLibre source correctly.
Registering the Custom MapLibre Protocol
Once metadata confirms the file is valid, [mbtiles.ts](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/mbtiles.ts) installs the protocol handler. This module defines three essential exports:
registerMbtilesProtocol()— installs the handler viamaplibregl.addProtocol()mbtilesTileUrl(path)— generates templated URLs for the sourceparseMbtilesTileRequest(request)— decodes incoming tile requests
The registration happens once per application lifecycle:
import { registerMbtilesProtocol } from '@/lib/mbtiles';
// Called when first MBTiles layer is added
registerMbtilesProtocol();
The handler itself follows MapLibre's protocol signature, accepting a Request and returning a Promise<Response> with tile binary data.
Protocol Handler: Bridging MapLibre Requests to Tauri
When MapLibre needs a tile, it constructs URLs using the template from mbtilesTileUrl():
// Generated URL format
const tileUrl = mbtilesTileUrl('/path/to/data.mbtiles');
// → "geolibre-mbtiles://tile/{z}/{x}/{y}?path=%2Fpath%2Fto%2Fdata.mbtiles"
The registered handler in [mbtiles.ts](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/mbtiles.ts) (lines 36-43) processes these requests:
// Inside registerMbtilesProtocol()
maplibregl.addProtocol('geolibre-mbtiles', async (request) => {
const { path, z, x, y } = parseMbtilesTileRequest(request);
// Bridge to Tauri backend
const bytes = await invoke<number[] | Uint8Array>('read_mbtiles_tile', {
path,
z,
x,
y
});
return new Response(new Uint8Array(bytes));
});
Key parsing logic extracts coordinates and the encoded file path from the URL query string, then invokes the Rust command with strongly-typed parameters.
Tauri Backend: Rust Commands for SQLite Access
The desktop side implements two primary commands in [src-tauri/src/lib.rs](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src-tauri/src/lib.rs):
read_mbtiles_metadata
Queries the metadata table for layer configuration:
#[tauri::command]
fn read_mbtiles_metadata(path: String) -> Result<MbtilesMetadata, String> {
let conn = open_mbtiles(&path)?;
// Extract name, format, bounds, zoom levels...
}
read_mbtiles_tile
The core tile retrieval command (lines 20-51) handles:
- Database connection — read-only SQLite access via
open_mbtiles() - Coordinate scheme conversion —
xyzvstmsrow calculation:row = (2^z - 1) - yfor TMS - SQL query execution —
SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ? - Optional decompression —
decompress_tile_data()for gzip-compressed tiles - Empty tile handling — returns
Vec::new()for missing tiles to prevent 404 errors
#[tauri::command]
fn read_mbtiles_tile(path: String, z: u8, x: u32, y: u32) -> Result<Vec<u8>, String> {
let mut conn = open_mbtiles(&path)?;
let scheme = detect_scheme(&conn)?; // "xyz" or "tms"
let row = if scheme == "tms" { flip_y(z, y) } else { y };
let tile_data: Option<Vec<u8>> = conn.prepare(
"SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?"
)?.query_row([z, x, row], |row| row.get(0)).ok();
match tile_data {
Some(data) => decompress_tile_data(&data),
None => Ok(Vec::new()),
}
}
All database operations use parameterized queries to prevent SQL injection, and the connection pool is managed per-request for thread safety.
Integrating as a MapLibre Source
The UI completes the cycle by creating a MapLibre source using the custom protocol URL:
// After metadata retrieval and protocol registration
map.addSource('local-mbtiles', {
type: 'raster', // or 'vector' based on metadata.tileType
tiles: [mbtilesTileUrl(selectedPath)],
tileSize: 256,
minzoom: metadata.minzoom,
maxzoom: metadata.maxzoom,
bounds: metadata.bounds
});
MapLibre's tile scheduler automatically requests geolibre-mbtiles:// URLs as the user pans and zooms, triggering the protocol handler → Tauri command → SQLite query pipeline for each visible tile.
Security and Performance Considerations
Sandbox preservation — The web frontend never receives filesystem paths in a usable form; paths are URL-encoded and processed only in Rust.
Read-only access — The open_mbtiles() helper explicitly opens connections with OpenFlags::SQLITE_OPEN_READ_ONLY.
Efficient binary transfer — Tauri's invoke serializes Vec<u8> directly to JavaScript Uint8Array without base64 encoding overhead.
Missing tile tolerance — Empty byte arrays prevent MapLibre from retrying failed requests, improving performance over sparse datasets.
Key Files Reference
| Purpose | File Path | Link |
|---|---|---|
| UI component for MBTiles selection and layer creation | apps/geolibre-desktop/src/components/layout/add-data/sources/MbtilesSource.tsx |
Source |
| Custom protocol registration, URL builder, and request parser | apps/geolibre-desktop/src/lib/mbtiles.ts |
Source |
| Tauri commands for metadata and tile retrieval | apps/geolibre-desktop/src-tauri/src/lib.rs |
Source |
| Tauri environment detection helper | apps/geolibre-desktop/src/lib/is-tauri.ts |
Source |
Summary
- Custom MapLibre protocol (
geolibre-mbtiles://) intercepts tile requests in the frontend - Protocol handler parses coordinates and file path, then calls
invoke('read_mbtiles_tile') - Tauri Rust command executes SQLite queries against the MBTiles database with proper coordinate scheme handling
- Binary response flows back through the protocol handler as a
Responseobject for MapLibre to decode - All filesystem access remains confined to the trusted Tauri backend, satisfying browser security models while enabling rich offline mapping
Frequently Asked Questions
How does GeoLibre handle TMS vs XYZ tile schemes?
The read_mbtiles_tile command detects the scheme from the MBTiles metadata table. For TMS, it flips the Y coordinate using row = (2^z - 1) - y before querying. XYZ coordinates pass through unchanged.
Can multiple MBTiles files be open simultaneously?
Yes. Each tile request includes the full file path in the URL query string, and the Rust backend opens independent SQLite connections per request. No global connection state is maintained between tiles.
What happens when a requested tile doesn't exist in the database?
The read_mbtiles_tile command returns an empty Vec<u8> instead of an error. This signals MapLibre to render transparent/background pixels rather than displaying broken tile indicators.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →