> For the complete documentation index, see [llms.txt](https://basedapp.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://basedapp.gitbook.io/docs/integrations/mini-apps-platform/15-notifications.md).

# Notifications & Live Activities

Mini-apps don't get raw push tokens. The host mediates every notification surface so we control the threat model: rate limits, brand prefixing, icon enforcement, audit trails. This doc walks through the four notification surfaces in order of complexity.

## TL;DR

| Surface              | Trigger from             | Status                                                                                                                                    | Phase |
| -------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| Partner remote push  | partner backend          | shipped end-to-end                                                                                                                        | 1     |
| Local notifications  | mini-app JS bundle       | shipped end-to-end                                                                                                                        | 2     |
| iOS Live Activities  | partner backend + bundle | shipped end-to-end (Widget Extension target lives at `ios/MiniAppLiveActivity/`)                                                          | 3     |
| Android Live Updates | partner backend + bundle | shipped (foreground + FCM data-message background path; uses `flutter_local_notifications` progress fallback until Android 16 stabilises) | 3     |

## Phase 1 — Partner remote push (shipped)

**Built**: `~/based-one-mono/apps/trading-web/src/app/api/partners/notifications/push/route.ts`

The mini-app's JS bundle does NOT push directly. The partner's **backend** calls a single endpoint on `trading-web` with the same recipients-by-email-or-wallet model the host uses internally:

```
POST https://based.one/api/partners/notifications/push
Authorization: Bearer mp_<api-key>
Content-Type: application/json
Idempotency-Key: <client-uuid>      // optional, 24h dedupe

{
  "recipients": {
    "emails":  ["alice@example.com"],
    "wallets": ["0xabc..."]
  },
  "title":    "Deposit pending",
  "body":     "Confirmations: 1/6",
  "deepLink": "basedapp://miniapp/<id>/deposits/0xabc",
  "data":     { "txHash": "0x..." }
}

→ {
    "requestId": "...",
    "sent":       2,
    "failed":     0,
    "recipients": 2,
    "missing":    { "emails": [], "wallets": [] }
  }
```

### Security model

* **No raw tokens leave the host.** Partners specify recipients by identifiers they already own (email / wallet); the gateway resolves to internal user IDs and looks up the user's push tokens. A compromised partner can't broadcast to non-customers, exfiltrate tokens, or push past their tenant boundary.
* **Tenant-scoped.** `MiniAppPartner.tenantId` clamps which users can be addressed. A Based partner can't push to Hyena users.
* **Brand-prefixed title.** The partner sends `"Deposit pending"`, the user sees `"[Pull] Deposit pending"`. Configured per-row in `MiniAppPartner.displayName`, partners can't override.
* **Icon is host-controlled.** The push payload carries `data.miniAppId`; the mobile host uses that to fetch the icon from the manifest registry at render time. Partners can't spoof the iOS app icon or other partners' branding.
* **Rate-limited per partner.** Default 60/min, 10000/day; tunable per-row by an admin. Sliding window over `MiniAppPushLog`.
* **Idempotency-Key**ed. Same key from same partner within 24h replays the original response without re-sending.
* **Auditable.** Every send writes a `MiniAppPushLog` row capturing inputs, recipient count, sent/failed split.

### How partners get a key

`POST /api/admin/mini-app-partners` with `{miniAppId, displayName, rateLimitPerMinute?, rateLimitPerDay?}`. Returns the plaintext `mp_<32-hex>` exactly once. Operator hands it to the partner over a secure channel. `DELETE /api/admin/mini-app-partners/<id>` soft-revokes via `revokedAt`. Phase 5's signed-registry rollout will automate issuance.

### Mobile host responsibilities

When a push arrives with `data.channel === "mini-app"` and a `data.miniAppId`:

1. Look up the icon from the local registry (already in-app — `MiniAppRegistryEntry.iconUrl` / `.icon`).
2. Render the system notification using that icon (NOT the host's default app icon and NOT anything from `data.icon` — partners don't supply one).
3. On tap: route to `/mini-apps/launch?manifest=<registry's manifestUrl>` and pass `data.deepLink` through so the mini-app bundle navigates to the right screen on boot.

### What it does not do (yet)

* No silent / background pushes. Pushes always show a banner.
* No push-to-Live-Activity payload type. Phase 3.
* No emoji / rich media in `body`. Phase 2 partner-comms ask.

## Phase 2 — Local notifications from the bundle

**Shipped.** `based.notifications.show()` lives in the SDK; host implementation in `services/mini_apps/mini_app_notifications.dart`.

For events the mini-app generates synchronously (e.g., a long- running operation completes while the user is in another tab), add a JS-side bridge:

```ts
await based.notifications.show({
  title:    "Pull complete",
  body:     "You pulled a holographic Charizard",
  deepLink: "basedapp://miniapp/fun.pull.gacha/pulls/123",
});
```

New permission `notifications.local`. Host wires to `flutter_local_notifications`. Same brand-prefix rule as Phase 1 (host injects `[Pull]` server-side... wait, no, the host injects it from the mini-app's manifest `name`/`shortName`). Same icon rule — host uses manifest icon, no partner override.

Lifetime: shown immediately, not updateable, dismissed by user tap or system swipe.

## Phase 3 — iOS Live Activities

**Shipped.** Implementation:

* Dart: `services/mini_apps/mini_app_live_activity.dart`
* Swift WidgetExtension: `ios/MiniAppLiveActivity/`
* Gateway endpoints: `/api/partners/live-activities` (start / PATCH update / DELETE end). See [`17-partner-api.md`](/docs/integrations/mini-apps-platform/17-partner-api.md).
* APNs HTTP/2 push-to-LA dispatcher: `lib/live-activities.ts` on the gateway side.

Live Activities are iOS 16.1+ ActivityKit. They render on the lock screen / Dynamic Island, can update in-flight, and can be driven by APNs push so they tick even when the app is fully killed.

### Apple's hard constraint: layouts must compile in

ActivityAttributes + the SwiftUI views that render them are compiled into the host's Widget Extension at build time. You cannot construct a Live Activity layout from JS at runtime.

So we ship a **fixed catalogue of templates** in Swift, and mini-apps pick one. Initial set:

| Template    | Use case                                         | Fields                                                               |
| ----------- | ------------------------------------------------ | -------------------------------------------------------------------- |
| `progress`  | Deposits, mints, multi-step flows                | title, subtitle, stages\[], currentStage, progress (0–1), statusText |
| `status`    | Single-state pill (Pending → Confirmed → Failed) | title, subtitle, statusLabel, statusVariant, footnote                |
| `countdown` | Pack drops, auctions                             | title, subtitle, deadline (ISO), footnote                            |

The Bybit screenshots in the original ask map cleanly onto `progress`.

### Partner-driven via APNs (no token leak)

Same model as Phase 1 — the partner's backend tells `trading-web` to start/update/end the activity:

```
POST /api/partners/live-activities
{
  "recipient": { "email" | "wallet": ... },
  "template":  "progress",
  "attributes": {
    "title":    "Deposit confirmation",
    "subtitle": "0x001...a3c31"
  },
  "state": {
    "stages":       ["Confirming", "Processing", "Completed"],
    "currentStage": 0,
    "progress":     0.16,
    "statusText":   "Confirmations: 1/6"
  },
  "ttlSeconds": 1800
}
→ { activityId: "..." }

POST /api/partners/live-activities/<activityId>/update
{ "state": { "currentStage": 2, "progress": 1.0, "statusText": "Completed" }, "alert": { "title": "Deposit complete", "body": "499998.96 USDC" } }

DELETE /api/partners/live-activities/<activityId>
```

The endpoint resolves the user → user's APNs push-to-LA token (separate from regular APNs token) → fires Apple's Live Activity push. Tokens never leave the host.

### Brand and icon rules apply

Same as Phase 1: `displayName` is rendered on the activity card; icon comes from the manifest. Templates in Swift hard-code the positions so partners can't visually impersonate the host or other mini-apps.

### Rate / lifecycle

* Max 1 active Live Activity per (user, mini-app) pair. Re-starting replaces the existing one.
* Default 30-min TTL; extendable to 8h max.
* Partners may not start an activity for a user without an existing in-app session — prevents lock-screen spam from a partner the user hasn't engaged with.

### Effort estimate

3–4 weeks. Two-thirds is Swift / WidgetExtension work and the APNs push-to-LA plumbing; remaining is the gateway endpoint and permission gating.

## Phase 3 (parallel) — Android Live Updates

**Shipped.** Implementation:

* Dart: `services/mini_apps/mini_app_live_activity.dart` (Android branch uses `flutter_local_notifications` with progress as the foreground render).
* Background data-message handler: `services/notifications/notification_service.dart` picks up FCM messages tagged with `miniAppId` + `template` and re-renders the ongoing notification from a top-level entry- point function (compatible with the Flutter background isolate).
* Gateway dispatcher: `sendFcmDataMessage()` in `lib/live-activities.ts` uses the existing `firebase-admin` setup. Same partner-facing endpoints as iOS — the gateway forks on the registered token's platform field.

Android doesn't have a 1:1 ActivityKit equivalent. Two mechanisms:

1. **Notification.ProgressStyle** (Android 16+): the closest match to a Live Activity card — system-rendered progress UI on the lock screen, updateable via FCM. New API in 16, not yet stable across OEM lock screens.
2. **Foreground Service notifications**: works on every Android from 8+, but requires a running service in the host app. Not suitable for a mini-app whose runtime is paused while the user navigates away.

The pragmatic answer is **option 1 with graceful degradation**: on Android 16+ devices the activity renders as a true live card; older Androids fall back to a regular updateable notification.

The partner-facing API is identical to iOS (Phase 3) — partners target a template + state, the host picks the right rendering for the device. Templates are designed to render on both platforms.

### Effort estimate

2 weeks behind the iOS work, mostly because the templates need to render on both platforms (additional Compose layouts) and the gateway endpoint needs to fork on platform.

## Schema reference

Tables added in `~/based-one-mono/packages/database/prisma/schema.prisma`:

* `MiniAppPartner` — one row per partner. Holds `miniAppId`, `displayName`, `apiKeyHash`, `tenantId`, rate limits, `revokedAt`.
* `MiniAppPushLog` — one row per send. Holds resolved recipient count, prefixed title, body, deepLink, data, sent/failed counts, optional `idempotencyKey`. Partition on `createdAt` once volume warrants.

## Outstanding work

The above three phases are shipped end-to-end. A few items remain before we can declare the surface complete:

* **Send-to-self from JS.** Phase 2 covers local notifications from the bundle, which subsumes the original "send-to-self" ask — the mini-app can already trigger a banner for the current user without a partner backend roundtrip. Time-shifted notifications (alarm-style, "remind me in 10 minutes") still need a `based.notifications.schedule(..., when)` extension; not yet built.
* **Per-mini-app opt-out.** `Settings → Notifications → Mini-apps` surface listing every partner the user has received a push from with a per-row toggle. Not yet built; planned before Phase 1 scales past the initial whitelist.
* **Signed push payloads.** Today the mobile host trusts any push bearing the right APNs/FCM topic. A signed envelope (HMAC over the body, key shared between gateway and host build) would let the host reject pushes injected by a compromised APNs sender. Not yet built; tracked as a follow-up before opening the partner API to the public.
