Server API
Coming nextCreate links, send campaigns and pushes to single users, report events and delete users, from any language.
Flutter · Serverpod · MCP
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.
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.
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.
flutter pub add lotse
dart pub global activate skills
skills get --package lotse --all
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.
Add Lotse to this app with the lotse-setup skill. Then set up deep links with lotse-deep-links and push with lotse-push.
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.
claude mcp add --transport http --scope local lotse https://api.lotse.dev/mcp --header 'Authorization: Bearer ${LOTSE_MCP_TOKEN}'
The same steps without an agent, from sign-up to the first deep link on a device, on Android and iOS.
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.
The SDK is the package lotse on pub.dev.
flutter pub add lotse
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.
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 ?? '/'));
}
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.
<!-- 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>
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.
// 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 calls most apps need. The generated Serverpod client stays hidden behind them.
static Future<void> Lotse.init({required String publicKey, required String serverUrl})Future<LotseLink?> get deferredLinkFuture<LotseLink?> resolveLink(String slug)Future<void> track(String name, {Map<String, String>? props})Future<void> identify(String userId, {Map<String, String>? traits, String? userHash})Future<void> reset()LotseScreenObserver({String? Function(Route<dynamic> route)? screenName})Future<LotsePush?> get initialPushStream<LotsePush> get pushOpenedFuture<void> showFeedback(BuildContext context, {Map<String, String>? metadata, bool askForEmail = false, LotseFeedbackTexts? texts})Your server works with Lotse too: a REST API with server keys, and signed webhooks in the other direction.
Create links, send campaigns and pushes to single users, report events and delete users, from any language.
Feedback, survey answers, attributed installs, finished campaigns and more, posted to your backend and signed according to Standard Webhooks.
Lotse is not a thin layer over someone else’s backend. Every part of it uses a piece of Serverpod.
Projects, links, clicks, devices, events, campaigns and deliveries are .spy.yaml models. The schema moves with generated migrations.
Console and SDK talk to the server through the generated client. The SDK wraps it, so your app never sees a generated symbol.
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.
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.
A campaign becomes delivery rows. A worker claims them in batches, retries with backoff and marks rejected tokens as invalid.
The same Serverpod serves the API, the link domains and the Flutter web console, with Postgres underneath.
Every endpoint resolves the project from the key and filters every query by it. Integration tests with two projects guard endpoints and link router.
App Links fail on details. These are the ones that cost us time.
Free during the beta. Sign up, create a project, add the SDK.