Mobile integration
Patterns for shipping MAP in iOS (Swift) and Android (Kotlin) apps — the SDK, rendering tiles with MapLibre Native, subscribing to live positions via SSE, and forwarding webhook events to FCM/APNS push.
1. Add the SDK
Swift (SPM)
// Package.swift
dependencies: [
.package(url: "https://github.com/map-bd/sdk-swift", from: "0.1.0"),
]Android (Gradle)
// app/build.gradle.kts
dependencies {
implementation("bd.map:mapbd:0.1.0")
implementation("org.maplibre.gl:android-sdk:11.5.0") // for map rendering
}2. Render tiles with MapLibre Native
Both SDKs expose tileURL(source, z, x, y) with the API key baked into the query string — works directly as a tile source.
Swift (UIKit/SwiftUI)
import MapBD
import MapLibre
let client = MapClient(apiKey: ProcessInfo.processInfo.environment["MAP_API_KEY"]!)
let tileTemplate = client.baseURL.appendingPathComponent("/v1/tiles/bangladesh/{z}/{x}/{y}").absoluteString
+ "?api_key=" + apiKey
let source = MLNRasterTileSource(identifier: "map-bd",
tileURLTemplates: [tileTemplate],
options: [.minimumZoomLevel: 0, .maximumZoomLevel: 14])
mapView.style?.addSource(source)Kotlin (Android)
import bd.map.MapClient
import org.maplibre.android.maps.MapView
import org.maplibre.android.style.sources.VectorSource
import org.maplibre.android.style.sources.TileSet
val client = MapClient(apiKey = BuildConfig.MAP_API_KEY)
val tileTemplate = "${client.baseURL}/v1/tiles/bangladesh/{z}/{x}/{y}?api_key=${BuildConfig.MAP_API_KEY}"
val source = VectorSource("map-bd", TileSet("2.0.0", tileTemplate).apply { minZoom = 0f; maxZoom = 14f })
style.addSource(source)3. Live driver positions (SSE)
The gateway streams every /v1/events/track ping to anyone subscribed at GET /v1/events/stream. Server-Sent Events — works over normal HTTPS, no special infra.
Swift — URLSession + AsyncSequence
func streamDriverPositions(apiKey: String, driverID: String? = nil) async throws {
var url = URL(string: "https://api.map.bd/v1/events/stream")!
if let d = driverID {
var c = URLComponents(url: url, resolvingAgainstBaseURL: false)!
c.queryItems = [URLQueryItem(name: "driver_id", value: d)]
url = c.url!
}
var req = URLRequest(url: url)
req.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
req.setValue("text/event-stream", forHTTPHeaderField: "Accept")
let (bytes, _) = try await URLSession.shared.bytes(for: req)
for try await line in bytes.lines {
if line.hasPrefix("data: "),
let data = line.dropFirst(6).data(using: .utf8),
let pos = try? JSONDecoder().decode(Position.self, from: data) {
await MainActor.run { moveMarker(pos) }
}
}
}
struct Position: Codable { let driver_id: String; let lat: Double; let lon: Double; let ts: Int64 }Kotlin — OkHttp + Flow
fun streamPositions(apiKey: String, driverId: String? = null): Flow<Position> = flow {
val url = if (driverId != null) "https://api.map.bd/v1/events/stream?driver_id=$driverId"
else "https://api.map.bd/v1/events/stream"
val req = Request.Builder().url(url).header("Authorization", "Bearer $apiKey").build()
val client = OkHttpClient.Builder().readTimeout(0, TimeUnit.MILLISECONDS).build()
client.newCall(req).execute().use { resp ->
val body = resp.body ?: return@flow
body.charStream().buffered().useLines { lines ->
for (line in lines) {
if (line.startsWith("data: ")) {
val pos = Json.decodeFromString<Position>(line.removePrefix("data: "))
emit(pos)
}
}
}
}
}.flowOn(Dispatchers.IO)
@Serializable data class Position(val driver_id: String, val lat: Double, val lon: Double, val ts: Long)4. Forwarding webhooks to FCM / APNS push
MAP webhooks land on your server. From there, you fan out to FCM (Android) or APNS (iOS). This pattern keeps push provider credentials on your infrastructure — neither MAP nor your mobile app sees them directly.
Node.js — verify + forward
import crypto from "node:crypto";
import express from "express";
import admin from "firebase-admin"; // for FCM
admin.initializeApp({ credential: admin.credential.applicationDefault() });
const app = express();
app.post("/webhooks/map", express.raw({ type: "application/json" }), async (req, res) => {
// 1. Verify signature
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.MAP_WEBHOOK_SECRET)
.update(req.body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(req.header("x-map-signature")),
Buffer.from(expected))) {
return res.status(401).end();
}
const ev = JSON.parse(req.body);
// 2. Look up the rider's device tokens by driver_id (your DB)
const tokens = await tokenStore.byDriver(ev.driver_id);
// 3. Send via FCM (Android) — same shape APNS via 'apn' header.
if (tokens.length > 0) {
await admin.messaging().sendEachForMulticast({
tokens,
notification: {
title: ev.event === "geofence.enter" ? "Your driver has arrived" : "Driver update",
body: ev.driver_id + " is now at the pickup point",
},
data: { event: ev.event, driver_id: ev.driver_id,
lat: String(ev.position.lat), lon: String(ev.position.lon) },
});
}
res.json({ ok: true });
});For APNS, swap firebase-admin for the node-apn package or call api.push.apple.com directly with HTTP/2 + JWT auth.
5. Where to go next
- Full SDK reference: Docs home · API ref
- Dispatch backend tutorial (Node): /docs/tutorial
- Live dispatch demo (browser): /dispatch · uses the same APIs
- Webhook delivery debugger: /dashboard/webhooks