local_mbtiles_server 0.1.0 copy "local_mbtiles_server: ^0.1.0" to clipboard
local_mbtiles_server: ^0.1.0 copied to clipboard

Serve SQLCipher-encrypted and plain MBTiles over localhost HTTP so any map SDK (MapLibre, Mapbox, etc.) can load offline tiles via standard URL templates.

local_mbtiles_server #

Serve SQLCipher-encrypted MBTiles from a Flutter app over localhost HTTP, so any map SDK (MapLibre, Mapbox GL, etc.) can load protected offline tiles via standard URL templates — without decrypting the whole file or building map-SDK-specific integrations.

Why this package? #

Offline map apps often ship encrypted .mbtiles files to protect tile data at rest. SQLCipher stores tiles inside an encrypted SQLite database — but map SDKs do not read MBTiles directly. They expect HTTP tile URLs like http://127.0.0.1:8080/basemap/{z}/{x}/{y}.pbf.

This package is built for that gap: decrypt and serve tiles on demand from encrypted MBTiles over loopback HTTP, so your map SDK works unchanged.

  1. Open an encrypted .mbtiles file with the correct SQLCipher password.
  2. Start a loopback HTTP server inside your app.
  3. Point MapLibre, Mapbox, or any tile-URL-capable SDK at http://127.0.0.1:<port>/{sourceId}/{z}/{x}/{y}.{ext}.

Tiles are decrypted per request at the SQLite page level — the file stays encrypted on disk and is never fully decrypted into a plain copy. Plain (unencrypted) MBTiles are also supported via the same API when you omit the password.

The server is map-SDK-agnostic: one HTTP endpoint works with any client that accepts custom tile URL templates.

Features #

Encrypted MBTiles (primary)

  • SQLCipher 4 support via sqflite_sqlcipher — passphrase or raw-key (x'…hex…') passwords
  • On-demand decryption — only the database pages needed for each tile request are decrypted; no full-file decrypt step
  • Single code path for encrypted and plain files — pass password when the MBTiles file is encrypted, omit it otherwise
  • Loopback-only by default (127.0.0.1) — decrypted tile bytes are served only to the app on the same device, not exposed on the local network

Tile serving

  • Raster formats: PNG, JPEG, WebP
  • Vector formats: PBF / MVT
  • Automatic gzip detection (Content-Encoding: gzip when tile bytes are gzipped)
  • Multi-source registry — serve several MBTiles files (encrypted or plain) from one server
  • Dart APIhealth and metadata for app setup; HTTP serves tiles only

Platform support #

MBTiles access uses sqflite_sqlcipher, which runs on Android and iOS. Desktop and web hosts are not supported for opening real MBTiles files (unit tests on desktop skip encrypted fixtures when the native plugin is unavailable).

Getting started #

Add the dependency:

dependencies:
  local_mbtiles_server: ^0.1.0

See the example/ app for a full runnable demo with encrypted raster tiles and a MapLibre preview screen.

Quick start #

Encrypted MBTiles (SQLCipher) #

Pass the exact string SQLCipher expects for PRAGMA key, then start the server the same way as plain files:

import 'package:local_mbtiles_server/local_mbtiles_server.dart';

// Passphrase-protected file:
final source = await MbtilesSource.open(
  path: '/path/to/encrypted.mbtiles',
  id: 'offline',
  password: 'my-passphrase',
);

// Raw-key file (hex PRAGMA form):
// final source = await MbtilesSource.open(
//   path: '/path/to/encrypted.mbtiles',
//   id: 'offline',
//   password: "x'3c1c3e9b5819aa6855842f9e4e24477b8e90b89547a79d45120ef8da0f533676'",
// );

final server = LocalMbtilesServer(
  config: MbtilesServerConfig(port: 0), // 0 = OS-assigned free port
);

await server.register(source);
await server.start();

final url = '${server.baseUrl}/offline/{z}/{x}/{y}.pbf';
// Pass `url` to your map SDK — tiles are decrypted per request.

For SQLCipher 4 files, codec settings (page size, KDF, cipher) are read from the file header automatically. You do not pass those parameters to this package.

Verify passwords with the sqlcipher CLI before wiring them into your app:

sqlcipher your.mbtiles
PRAGMA key = "your-password-or-x'hex'";
SELECT count(*) FROM metadata;

If your app stores a base64-encoded raw key separately from the .mbtiles file, decode it and format as "x'…hex…'" yourself. The helper in example/lib/sqlcipher_password.dart shows this conversion; the bundled demo itself uses a simple passphrase.

Plain MBTiles #

Same API — omit password when the file is not encrypted:

final source = await MbtilesSource.open(
  path: '/path/to/map.mbtiles',
  id: 'basemap',
);

final server = LocalMbtilesServer(config: MbtilesServerConfig(port: 0));
await server.register(source);
await server.start();

final url = '${server.baseUrl}/basemap/{z}/{x}/{y}.png';

HTTP API (tiles only) #

After server.start(), the server listens on loopback (default 127.0.0.1:8080, or an OS-assigned port when port: 0) and serves tile bytes only:

Method Path Description
GET /{sourceId}/{z}/{x}/{y}[.{ext}] Tile bytes

Tile paths use web-map XYZ coordinates. The optional extension (.png, .pbf, etc.) is ignored for lookup but helps map SDKs and debugging.

Responses

  • 200 — tile found; Content-Type set from MBTiles format (image/png, application/x-protobuf, …)
  • 404 — unknown source or tile not in the file (Tile not found)
  • 400 — invalid coordinates or path

Use the Dart API below for server health and MBTiles metadata.

Server health #

await server.register(source);
await server.start();

final health = server.health;
// health.status, health.sourceCount, health.port, health.isRunning
// health.toJson() for logging or UI

Works before or after start()isRunning and port reflect the current state.

Reading metadata #

MBTiles metadata (name, format, zoom range, bounds, vector layer JSON, etc.) can be read in two ways. Both query the file’s metadata table at call time — nothing is cached on the source after open.

API When to use
source.getMetadata() on an open MbtilesSource You have (or just opened) the source instance — e.g. inspect a file before deciding to register it
server.getMetadata(sourceId) on LocalMbtilesServer The source is already registered; you only track sourceId (typical multi-map apps)

Inspect before register #

Use this when you open a file and want bounds/format before adding it to the server:

final source = await MbtilesSource.open(
  path: '/path/to/map.mbtiles',
  id: 'candidate',
  password: 'my-passphrase',
);

try {
  final meta = await source.getMetadata();
  if (meta.minZoom == null || meta.maxZoom == null) {
    return; // skip unsuitable file
  }

  await server.register(source);
  // safe to drop the `source` variable — server holds the instance
} catch (error) {
  await source.close();
  rethrow;
}

Requires a successful MbtilesSource.open first. Fails if the source has been closed.

After register (by source id) #

Use this when sources are on the server and you only keep ids — no need to hold MbtilesSource references:

await server.register(await MbtilesSource.open(path: pathA, id: 'satellite', password: pw));
await server.register(await MbtilesSource.open(path: pathB, id: 'streets', password: pw));
await server.start();

final satelliteMeta = await server.getMetadata('satellite');
final streetsMeta = await server.getMetadata('streets');

// Build tile URL and zoom range for your map SDK
final url = '${server.baseUrl}/streets/{z}/{x}/{y}.pbf';
final minZoom = streetsMeta.minZoom ?? 0;
final maxZoom = streetsMeta.maxZoom ?? 22;
final bounds = streetsMeta.bounds; // west, south, east, north

Throws SourceNotFoundException if sourceId is not registered. Works before or after server.start() — registration is enough.

Map SDK example #

Use the tile URL template from server.baseUrl, metadata from getMetadata, and your SDK’s custom-tile-source API:

final metadata = await server.getMetadata('basemap');
final tileUrl = '${server.baseUrl}/basemap/{z}/{x}/{y}.pbf';

// Pass tileUrl, metadata.minZoom, metadata.maxZoom (and vector layers if needed)
// to your map SDK — exact API depends on the SDK you use.

For raster MBTiles, use the .png (or .jpg / .webp) extension in the tile URL instead of .pbf.

The bundled example app (example/) loads an encrypted raster overlay (demo_raster.mbtiles, zooms 12–15 over a small San Francisco bbox) via MapLibre — zoom out to see the base map; zoom in to see localhost tiles. See example/README.md.

On Android, map SDKs need cleartext HTTP allowed for 127.0.0.1 — see Diagnostics.

Multi-source registry #

Register several MBTiles files under different ids:

await server.register(await MbtilesSource.open(path: pathA, id: 'satellite'));
await server.register(await MbtilesSource.open(path: pathB, id: 'streets'));

// satellite: http://127.0.0.1:8080/satellite/{z}/{x}/{y}.jpg
// streets:   http://127.0.0.1:8080/streets/{z}/{x}/{y}.pbf

After register, you only need server and source ids — see Reading metadata.

unregister(sourceId) removes a source and closes it. DuplicateSourceException is thrown if the same id is registered twice.

Lifecycle #

Typical app flow:

// 1. Open sources and start server (e.g. when a map screen opens)
await server.register(source);
await server.start();

// 2. Use server.baseUrl in your map SDK

// 3. Stop when done
await server.unregister('basemap'); // optional per-source cleanup
await server.stop(); // closes all sources when closeSourcesOnStop is true (default)

The server keeps running until you call stop(). It does not automatically pause when the app is backgrounded — if you need that, stop/restart the server from your app's lifecycle observer (WidgetsBindingObserver).

Always call MbtilesSource.close() (or server.stop() with default config) to release the SQLite connection.

Configuration #

MbtilesServerConfig(
  host: InternetAddress.loopbackIPv4, // default — localhost only
  port: 8080,                          // use 0 for a free port
  debugResponses: false,               // include exception details in 500 responses
  closeSourcesOnStop: true,            // close all TileSources on stop()
  onError: (error, stack) { … },       // optional logging hook
)

Supported tile formats #

MBTiles format Kind URL extension Content-Type
png raster .png image/png
jpg, jpeg raster .jpg image/jpeg
webp raster .webp image/webp
pbf, mvt vector .pbf application/x-protobuf

Error handling #

Exception When
MbtilesOpenException File missing, wrong password, corrupt database
UnsupportedTileFormatException Unknown metadata.format
DuplicateSourceException Registering the same source id twice
SourceNotFoundException Tile or metadata request for unknown source
InvalidTileRequestException Bad zoom/x/y in URL

Diagnostics #

Common setup issues when integrating encrypted MBTiles, localhost tile URLs, and map SDKs.

Android cleartext traffic #

Map SDKs fetch tiles over http://127.0.0.1. Android blocks non-HTTPS traffic by default, so allow cleartext for localhost in your app manifest:

<application
    android:usesCleartextTraffic="true"
    …>

iOS allows loopback HTTP for local requests without extra configuration.

SQLCipher 3 (or older) MBTiles files #

This package uses sqflite_sqlcipher, which targets SQLCipher 4. Files encrypted with an older SQLCipher version may fail to open on iOS, or open only after migration.

On Android, the plugin can run PRAGMA cipher_migrate automatically when it detects an older encryption format and upgrade the file in place. On iOS, that automatic migration is not supported — re-encrypt the MBTiles with SQLCipher 4 (or migrate on Android first, then ship the updated file). See the sqflite_sqlcipher readme for background.

Prefer creating new offline packs with SQLCipher 4 up front. Verify any file with the sqlcipher CLI before shipping:

sqlcipher your.mbtiles
PRAGMA key = "your-password-or-x'hex'";
SELECT count(*) FROM metadata;

Wrong password or MbtilesOpenException #

If MbtilesSource.open throws MbtilesOpenException, the file path, password string, or encryption format is wrong. Pass the exact value SQLCipher expects for PRAGMA key — passphrase or raw-key form "x'…hex…'". If your app stores a base64 raw key, decode and format it yourself (see example/lib/sqlcipher_password.dart). A mismatch often looks like a corrupt database even when the file is fine.

Android release builds (R8 / ProGuard) #

Release APKs shrink native code by default. If SQLCipher classes are stripped, encrypted MBTiles may fail at runtime. Add this to android/app/proguard-rules.pro (create the file if needed), as documented by sqflite_sqlcipher:

-keep class net.sqlcipher.** { *; }

Port already in use #

If start() fails because the port is taken, set MbtilesServerConfig(port: 0) to let the OS assign a free port, then read server.baseUrl (or server.health.port) for the actual URL passed to your map SDK.

Additional information #

License #

See LICENSE.

2
likes
160
points
86
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Serve SQLCipher-encrypted and plain MBTiles over localhost HTTP so any map SDK (MapLibre, Mapbox, etc.) can load offline tiles via standard URL templates.

Repository (GitHub)
View/report issues

Topics

#mbtiles #maps #offline #sqlcipher

License

BSD-3-Clause (license)

Dependencies

flutter, meta, shelf, sqflite_sqlcipher

More

Packages that depend on local_mbtiles_server