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:

  1. User selects MBTiles file → UI triggers file dialog
  2. Metadata extraction → Tauri command queries SQLite headers
  3. Protocol registration → Custom handler installed in MapLibre
  4. Tile request interception → URL parsed, parameters extracted
  5. Backend tile retrieval → Rust command fetches binary blob
  6. 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 via maplibregl.addProtocol()
  • mbtilesTileUrl(path) — generates templated URLs for the source
  • parseMbtilesTileRequest(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 — xyz vs tms row calculation: row = (2^z - 1) - y for 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 Response object 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →