Flutter · Serverpod · MCP

For developers

Let your coding agent build Lotse in with the skills that ship with the SDK, or do it by hand. One package, one init call, and a backend you can read: Lotse runs on Serverpod.

With your coding agent

Claude Code, Cursor or Codex can do the integration. The lotse package ships skills that teach your agent the SDK and, from lotse 0.2.0 on, the server API, and the MCP server lets it read your project afterwards.

  1. 1

    Add the package and its skills

    Run this in your app’s folder. The skills CLI copies the skills of the lotse package into your project, where your agent finds them. Run skills get again after every update of lotse.

    Terminal
    flutter pub add lotse
    dart pub global activate skills
    skills get --package lotse --all
    
  2. 2

    Ask your agent

    Create a project and an API key in the console first. The agent asks you for the public key and your handle and tells you what to enter in the console. It never needs a secret.

    Prompt
    Add Lotse to this app with the lotse-setup skill. Then set up deep links with lotse-deep-links and push with lotse-push.
    
  3. 3

    Connect the MCP server

    With a token from the project settings, your agent reads funnels, feedback and links, checks your setup and, if the token allows it, creates links and segments. This is the line for Claude Code. Cursor, VS Code and Codex are on the AI agents page.

    Terminal
    claude mcp add --transport http --scope local lotse https://api.lotse.dev/mcp --header 'Authorization: Bearer ${LOTSE_MCP_TOKEN}'
    

Quickstart by hand

The same steps without an agent, from sign-up to the first deep link on a device, on Android and iOS.

  1. 1

    Create a project

    Sign up in the console, create a project and choose its handle. The handle becomes your link domain, your-handle.lotse.dev. Add your Android app with its package name and the SHA-256 fingerprint of your signing key, and your iOS app with its bundle ID and Team ID. Then create an API key; its public key goes into the app.

  2. 2

    Add the package

    The SDK is the package lotse on pub.dev.

    Terminal
    flutter pub add lotse
    
  3. 3

    Initialise

    One call registers the device, reports the session and resolves a deferred link. It never throws into your app: failures are logged and the link is null.

    Dart
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Lotse.init(
        publicKey: 'lt_pk_…',
        serverUrl: 'https://api.lotse.dev/',
      );
    
      // Set on the first start after an install that came from a link.
      final link = await Lotse.instance.deferredLink;
      runApp(MyApp(initialLocation: link?.targetPath ?? '/'));
    }
    
  4. 4

    Let links open the app

    Declare your link domain in the Android manifest, and on iOS add the Associated Domains capability with applinks:your-handle.lotse.dev. Lotse serves assetlinks.json and apple-app-site-association for your project. Route /l/:slug in your router to Lotse.instance.resolveLink(slug) and go to its targetPath.

    XML
    <!-- android/app/src/main/AndroidManifest.xml, inside <activity> -->
    <intent-filter android:autoVerify="true">
      <action android:name="android.intent.action.VIEW"/>
      <category android:name="android.intent.category.DEFAULT"/>
      <category android:name="android.intent.category.BROWSABLE"/>
      <data android:scheme="https"
            android:host="your-handle.lotse.dev"
            android:pathPrefix="/l/"/>
    </intent-filter>
    
  5. 5

    Turn on push

    Add Firebase to your app as usual and upload the service account in the console. For iPhones, also upload an APNs key (.p8) for the iOS app; without one, pushes to iOS go through FCM. The SDK asks for permission, reports the token and tells you when a push was opened.

    Dart
    // A push tapped while the app was closed, and pushes tapped while it runs.
    final push = await Lotse.instance.initialPush;
    Lotse.instance.pushOpened.listen((push) => router.go(push.targetPath ?? '/'));
    

The SDK surface

The calls most apps need. The generated Serverpod client stays hidden behind them.

static Future<void> Lotse.init({required String publicKey, required String serverUrl})
Connects, registers the device, reports session_start. Calling it again does nothing.
Future<LotseLink?> get deferredLink
The link an install came from. Completes once, with null if there was none.
Future<LotseLink?> resolveLink(String slug)
Turns the slug of an opened link into its target route and campaign.
Future<void> track(String name, {Map<String, String>? props})
Writes an event with string properties to the stream.
Future<void> identify(String userId, {Map<String, String>? traits, String? userHash})
Ties the device to a signed-in user, with traits. userHash comes from your backend once the project verifies identities.
Future<void> reset()
For logout: the next events no longer belong to the user.
LotseScreenObserver({String? Function(Route<dynamic> route)? screenName})
Hand it to your router and every screen change becomes a screen_view event. screenName picks the name, the route name by default.
Future<LotsePush?> get initialPush
The push that started the app, if any.
Stream<LotsePush> get pushOpened
Pushes tapped while the app is running.
Future<void> showFeedback(BuildContext context, {Map<String, String>? metadata, bool askForEmail = false, LotseFeedbackTexts? texts})
Opens the feedback sheet: screenshot, marks, text and category. metadata is stored with the message, askForEmail adds an optional e-mail field.

From your backend

Your server works with Lotse too: a REST API with server keys, and signed webhooks in the other direction.

Server API

Coming next

Create links, send campaigns and pushes to single users, report events and delete users, from any language.

Webhooks

Coming next

Feedback, survey answers, attributed installs, finished campaigns and more, posted to your backend and signed according to Standard Webhooks.

What Serverpod does here

Lotse is not a thin layer over someone else’s backend. Every part of it uses a piece of Serverpod.

Models and migrations

Projects, links, clicks, devices, events, campaigns and deliveries are .spy.yaml models. The schema moves with generated migrations.

A generated, typed client

Console and SDK talk to the server through the generated client. The SDK wraps it, so your app never sees a generated symbol.

Three ways to authenticate

Console users sign in with the auth module. The SDK uses a public key that may write and read only itself. Backends use a server key with scopes, stored as a hash.

Web server routes

The link router /l/:slug and the .well-known files are Relic routes. They find the project by the Host header and never block the redirect for the click record.

Future calls

A campaign becomes delivery rows. A worker claims them in batches, retries with backoff and marks rejected tokens as invalid.

One server for everything

The same Serverpod serves the API, the link domains and the Flutter web console, with Postgres underneath.

Tenant isolation

Every endpoint resolves the project from the key and filters every query by it. Integration tests with two projects guard endpoints and link router.

Set your course.First project in ten minutes.

Free during the beta. Sign up, create a project, add the SDK.