MAP
Mobile

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