Pushlanedocs
Documentation menu

Flutter Quickstart

Dart SDK with firebase_messaging — background handler before runApp.

#Prerequisites

  • Flutter SDK with Dart 3 or later
  • A Firebase project (firebase_core + firebase_messaging)
  • For iOS: a physical device running iOS 14+ and a paid Apple Developer account
  • Your Pushlane Tenant ID, Ingest URL, and write-key — copy them from Settings → Install SDK

#1. Add the Pushlane client (no pub.dev package)

Heads up
pushlane_flutter is not on pub.dev. Don't add pushlane_flutter: ^0.1.0 to pubspec.yaml. Use the zero-dependency drop-in below (recommended), or build the SDK from source in the Pushlane monorepo.
Note
No package to install — paste one file. Create lib/pushlane.dart and paste the client below. HTTP uses dart:io (no http package); push registration uses firebase_messaging, which your app already has for push. Your AI agent can scaffold this exact file via the Pushlane MCP (get_install_instructions).
yaml
# pubspec.yaml — the drop-in needs only Firebase (HTTP uses dart:io).
dependencies:
  firebase_core: ^3.0.0
  firebase_messaging: ^15.0.0

Paste this into lib/pushlane.dart:

dart
// Pushlane — zero-dependency drop-in client for Flutter. No pub.dev package to install.
// Paste this into lib/pushlane.dart. HTTP uses dart:io (no http package needed); push
// registration uses firebase_messaging (already present if your app does push).
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:firebase_messaging/firebase_messaging.dart';

class Pushlane {
  static String? _tenantId;
  static String? _key;
  static String _apiBase = 'https://loop-ingest.loop-push.workers.dev';
  static String _pushEnv = 'production';
  static String? _userId;

  static void configure({required String tenantId, required String publishableKey, String? apiBase, String pushEnvironment = 'production'}) {
    _tenantId = tenantId;
    _key = publishableKey;
    if (apiBase != null && apiBase.isNotEmpty) _apiBase = apiBase;
    _pushEnv = pushEnvironment;
  }

  static void identify(String userId) { _userId = userId; }
  static void reset() { _userId = null; }

  static String _uuid() {
    final r = Random();
    final b = List<int>.generate(16, (_) => r.nextInt(256));
    b[6] = (b[6] & 0x0f) | 0x40;
    b[8] = (b[8] & 0x3f) | 0x80;
    final h = b.map((x) => x.toRadixString(16).padLeft(2, '0')).join();
    return h.substring(0, 8) + '-' + h.substring(8, 12) + '-' + h.substring(12, 16) + '-' + h.substring(16, 20) + '-' + h.substring(20);
  }

  static String _hex(int n) {
    final r = Random();
    final sb = StringBuffer();
    for (var i = 0; i < n; i++) sb.write(r.nextInt(16).toRadixString(16));
    return sb.toString();
  }

  static Future<void> _post(String path, Map<String, dynamic> body, {Map<String, String>? extra}) async {
    if (_tenantId == null) return;
    var base = _apiBase;
    while (base.length > 1 && base.endsWith('/')) base = base.substring(0, base.length - 1);
    for (var attempt = 0; attempt <= 4; attempt++) {
      try {
        final client = HttpClient();
        final req = await client.postUrl(Uri.parse(base + path));
        req.headers.set('Content-Type', 'application/json');
        if (_key != null && _key!.isNotEmpty) req.headers.set('Authorization', 'Bearer ' + _key!);
        if (extra != null) extra.forEach((k, v) => req.headers.set(k, v));
        req.add(utf8.encode(jsonEncode(body)));
        final res = await req.close();
        client.close();
        if (res.statusCode >= 200 && res.statusCode < 300) return;
        if (res.statusCode < 500 && res.statusCode != 429) return;
      } catch (e) { /* retry */ }
      if (attempt < 4) {
        final ms = min(pow(2, attempt).toInt() * 500, 30000);
        await Future.delayed(Duration(milliseconds: ms));
      }
    }
  }

  static void track(String name, [Map<String, dynamic> properties = const {}]) {
    if (_tenantId == null || _userId == null) return;
    final tp = '00-' + _hex(32) + '-' + _hex(16) + '-01';
    _post('/v1/events', {
      'eventId': _uuid(), 'tenantId': _tenantId, 'externalId': _userId, 'name': name, 'properties': properties,
      'occurredAt': DateTime.now().millisecondsSinceEpoch,
      'context': {'sdk': 'pushlane-dropin-flutter', 'sdkVersion': '1.0.0'}, 'traceparent': tp,
    }, extra: {'traceparent': tp});
  }

  static Future<void> registerForPush() async {
    if (_tenantId == null || _userId == null) return;
    await FirebaseMessaging.instance.requestPermission();
    final token = await FirebaseMessaging.instance.getToken(); // FCM
    if (token == null) return;
    await _post('/v1/register', {'tenantId': _tenantId, 'externalId': _userId, 'deviceToken': token, 'pushEnvironment': _pushEnv});
  }

  static void setMarketingConsent(bool optedIn) {
    if (_tenantId == null || _userId == null) return;
    _post('/v1/consent', {'externalId': _userId, 'category': 'marketing', 'action': optedIn ? 'opt_in' : 'opt_out'});
  }

  static void start() { track('app_open'); }
}

Android — Firebase setup: Add google-services.json to android/app/, then apply the plugin in your app-level and project-level Gradle:

groovy
// android/app/build.gradle — apply the Google Services plugin
apply plugin: 'com.google.gms.google-services'
groovy
// android/build.gradle — project-level
dependencies {
    classpath 'com.google.gms:google-services:4.4.0'
}

iOS — Firebase + APNs:

  1. Add your APNs key in the Firebase console under Project → Cloud Messaging → iOS.
  2. In Xcode, enable Push Notifications under Signing & Capabilities.

#2. Configure the client

Initialize Firebase, then configure Pushlane, identify the user, start, and register for push. Identify before any track call or registerForPush.

dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'pushlane.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  Pushlane.configure(
    tenantId: 'your-tenant-id',
    publishableKey: 'lpk_live_…',  // from Settings → Install SDK
  );

  Pushlane.identify('user@example.com');
  Pushlane.start();                  // emits app_open
  await Pushlane.registerForPush();  // requests permission, registers the FCM token

  runApp(const MyApp());
}
Heads up
Pushlane.track and Pushlane.registerForPush are no-ops until you have called Pushlane.configure and Pushlane.identify. On logout:
dart
// On logout: detach this device from the user.
Pushlane.reset();

#3. Track events

Property values can be String, int, double, bool, or List. The backend coerces against your event catalogue — never coerce client-side.

dart
// Values: String, int, double, bool, List.
Pushlane.track('plan_viewed', {'plan': 'pro', 'trial': true});
Pushlane.track('purchase_completed', {'amount': 9.99, 'currency': 'USD'});

// No-properties variant:
Pushlane.track('app_rated');
Note
The drop-in emits app_open only (when you call Pushlane.start()). Unlike the native SDK it does not auto-track opened, received, or session_started — instrument the events you care about explicitly with Pushlane.track. Rename any reserved name such as message_sent (see Events & catalogue).

Pushlane is opt-out by default — registration alone is sufficient for delivery. Call setMarketingConsent only when the user makes an explicit choice in your settings screen.

dart
// Pushlane is opt-out by default — call this only when the user makes an
// explicit choice in your settings screen.

// User turned the marketing toggle OFF:
Pushlane.setMarketingConsent(false);

// User turned it back ON:
Pushlane.setMarketingConsent(true);

#Push delivery — what works today

Note

Pushlane.track works over HTTP — your events and audiences flow into Pushlane immediately.

registerForPush registers your FCM token. Pushlane's sender delivers over APNs, so end-to-end push delivery is proven on iOS; Android FCM delivery is not yet end-to-end. Use the drop-in to get your events and audiences into Pushlane today.

#5. Verify the integration

Run your app on a physical device. In the Pushlane dashboard, open Settings → Install SDK and watch the live verification panel.

CheckWhat it means
SDK detectedAt least one event has reached the ingest worker
Events instrumentedDistinct event names from your app appear in the catalogue
Device connectedAn FCM token was registered

#Next step

To send real pushes to iOS devices you still need to upload your Apple push key (.p8) to Pushlane.

APNs setup (.p8) →