# Installed app

Sellers can install the chat as an app at `https://app.mondochat.io/frame/?app=<appId>&mode=app` and get web push. The embed offers it to them. Buyers and guests stay in the embed.

## Before you start

- A role with `seller: true` in `roles`. Only a member of such a role gets the install offer.
- An https page on your site that signs the member in. It becomes `pwa.login_url`.
- A square PNG icon of at least 512 px, edge to edge, with the art inside the centre 80% circle because Android crops it to its shape.
- The app's server secret, to sign app links.

## Steps

1. Turn it on in the settings. `pwa.enabled` is off by default.

   ```json
   {
     "pwa": {
       "enabled": true,
       "name": "Example chat",
       "icon_url": "https://www.example.com/chat-icon.png",
       "login_url": "https://www.example.com/chat-app?lang={lang}"
     }
   }
   ```

   `name` is 1 to 45 characters and is the name under the icon. `enabled: true` without `name` and `icon_url`, or an `http://` `login_url`, answers 422 on save. `{lang}` in `login_url` becomes the language the app shows. `pwa.push_preview` makes notifications show the message preview instead of "New message". See the [settings reference](/reference/settings/).

2. Let sellers find it. With `pwa.enabled`, the embed offers a seller a dismissible Get the app card at the top of the inbox, and a Get the app row in settings that needs `workspace.inbound`. Tapping it opens the installed-app page in a new window, already signed in. Your page does nothing and receives no event.

3. Build the app link page at `pwa.login_url`. A signed-in app can lose its sign-in: it was unused for 30 days, the user logged out, or an iPhone copied no cookies. Its Sign in button opens `pwa.login_url` in the same window. Your page signs the member in if needed and answers 302 to a fresh app link. The app link is the client token with header `typ: app+jwt` and a required `jti`. `exp` is at most 600 seconds after `iat`. It goes in the fragment, never the query:

   ```js
   const iat = Math.floor(Date.now() / 1000)
   const claims = { iss: APP_ID, sub: user.id, name: user.name, role: user.role, jti: randomUUID(), iat, exp: iat + 300 }
   const body = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'app+jwt' }))}.${b64url(JSON.stringify(claims))}`
   const token = `${body}.${createHmac('sha256', SECRET).update(body).digest('base64url')}`
   res.redirect(302, `https://app.mondochat.io/frame/?app=${APP_ID}&mode=app#token=${token}`)
   ```

   `b64url`, `APP_ID` and `SECRET` are as in the Node server of the [quickstart](/get-started/quickstart/). The link works once. It signs the app in for 30 days from last use. The app removes the fragment from its address before any request. With no `login_url` set, a signed-out app asks the seller to open the chat on your site and tap Get the app again.

4. End access when a member loses it. Delete all of a user's devices:

   ```sh
   curl -X DELETE https://app.mondochat.io/v1/server/users/bob/devices \
     -H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID"
   ```

   The call answers 204, or 404 for an unknown user. See [Revoke devices](/api/operations/revoke-devices/). Each install is a device that signs in with an HttpOnly cookie. Bans, deleting the user and setting `pwa.enabled` to false also end access. A user can log out in the app's settings, which deletes that device.

5. Know what push sends. The worker sends one notification for the first message a seller has not seen in a conversation while she is away. mondochat holds one VAPID key per environment, so you configure nothing. The manifest is served for each app by [`GET /v1/apps/{app_id}/manifest.webmanifest`](/api/operations/manifest/).

## Check it works

- A seller sees the Get the app card in the embed. Tapping it opens the app in a new window, signed in.
- Opening your `login_url` while signed in to your site lands the app signed in.
- After step 4, the device no longer signs in and its sessions stop refreshing within 15 minutes.

## When it fails

| Symptom | Cause |
|---|---|
| The app shows that it is not available, with no retry | `pwa.enabled` is off. Device routes answer 403 `policy_denied` with reason `pwa`. Stored devices work again once it is turned on. |
| Device routes answer 503 | The push key on mondochat's side is missing or not valid. |
| The Sign in button loads a page that asks to open the chat on your site | `pwa.login_url` is not set. |
| An app link answers 401 | It was used before, has no `jti`, has another `typ`, belongs to another app, or is a guest token. |
| `POST /v1/session` refuses the app link | App links work only for the installed-app sign-in, and the installed-app sign-in refuses a plain client token. |
| 429 on opening the app from the chat | A user gets 5 app tickets per 10 minutes. |

## Limits

- A device lasts 30 days from last use, and a user has at most 10 devices: the least recently used is dropped.
- The one-time ticket the chat gives a seller lasts 120 seconds, and it is never given to a session that came from the app itself.
- An app link lives at most 600 seconds and works once.
- A seller who loses access stops refreshing within one session lifetime of 15 minutes.
- The installed-app routes under `/v1/device` are for the app itself and are not in the API reference. They accept no cross-origin calls.

## Related topics

- [Revoke devices](/api/operations/revoke-devices/)
- [Web app manifest](/api/operations/manifest/)
- [Settings reference](/reference/settings/)
- [Frame protocol](/reference/frame-protocol/)
- [Sessions and tokens](/guides/sessions-and-tokens/)
- [Users](/guides/users/)