# Bridge API

> The complete sollar.* namespace — identity, auth, rooms, messages, files, actions, storage, UI and permissions.

Source: https://miniapp.sollar.com/reference/bridge-api/

---

`sollar.*` is the only non-standard global a mini app gets. It is deliberately small.

## The rule that shapes this API

**Web first. The bridge covers only what the web cannot do.**

Camera, microphone, geolocation, file access, notifications, biometrics — these are standard Web
APIs that the engine already provides, with its own permission prompts. Sollar does not wrap them,
because a wrapper would be a worse version of something you already have.

```js
// Right — the platform's own API, with the engine's own prompt.
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
const position = await new Promise((ok, err) =>
  navigator.geolocation.getCurrentPosition(ok, err))
const cred = await navigator.credentials.get({ publicKey })   // WebAuthn
```

```js
// Wrong — these do not exist, and will not.
sollar.camera.take()
sollar.getLocation()
sollar.getDeviceContacts()
```

This is not minimalism for its own sake. Apple's Guideline 4.7.2 states that an app *"may not extend
or expose native platform APIs or technologies to the software without prior permission from
Apple."* Every native capability added to this bridge is a compliance question requiring Apple's
prior approval — a business-development timeline, not a sprint. `sollar.sendMessage()` is not
extending a platform API; `sollar.getDeviceContacts()` is.

### What you came looking for and will not find

If you arrive from WeChat, you will look for these. They are absent, on purpose:

| WeChat | Sollar | Why |
|---|---|---|
| `wx.requestPayment` | — | No in-app payment API. See [Store compliance](/compliance/) for how money works. |
| `<web-view>` | `sollar.ui.openExternal()` | The embedded-webview component is the single largest vulnerability vector in this class of platform. External content opens in the system browser, with a visible address bar and lock. |
| `wx.getLocation` | `navigator.geolocation` | The web API already has a permission prompt the engine controls. |
| `wx.chooseImage` | `<input type="file">` / `getUserMedia` | Same. |
| `wx.setStorage` | `localStorage`, IndexedDB | Partitioned per mini app by the engine's own origin isolation. |
| `wx.request` | `fetch` | Restricted to the manifest's network allowlist, enforced natively. |

## `sollar.app`

```ts
sollar.version: string                      // bridge version, e.g. "1.0.0"
sollar.supports(method: string): boolean    // feature detection
sollar.app.id: string
sollar.app.version: string
sollar.app.locale: string                   // BCP 47, the user's Sollar locale
sollar.app.theme: 'light' | 'dark'
sollar.app.onThemeChange(cb: (theme) => void): () => void
sollar.app.launchParams: Record<string, string>   // deep-link parameters
```

## `sollar.identity`

```ts
sollar.identity.get(): Promise<Identity>
```

```ts
interface Identity {
  app_user_id: string        // stable for (this user, this mini app). Not correlatable.
  display_name?: string      // requires identity.basic
  avatar_url?: string        // requires identity.basic
  email?: string             // requires identity.email
  tenant_id: string
  locale: string
}
```

> **DANGER**
**This is UI convenience, not authorisation.** Everything here reaches your JavaScript through a
process the user's device controls. Never authorise an action because `identity.get()` returned a
particular `app_user_id`. Authorise on your server, against the token, every time. This is the
most common breach in this class of platform, and it is entirely preventable.

`app_user_id` is derived as `HMAC(tenant_key, sollar_id ‖ mini_app_id)`. Two mini apps serving the
same human receive different, non-correlatable identifiers. Without that, a mini app store becomes a
tracking network by construction.

## `sollar.auth`

```ts
sollar.auth.getToken(): Promise<{ access_token: string, expires_in: number }>
sollar.auth.signIn(): Promise<void>          // external-oauth mode only
sollar.auth.signOut(): Promise<void>
sollar.auth.state(): Promise<'signed-in' | 'signed-out'>
```

Call `getToken()` **before each use**. Do not cache it, do not persist it, do not pass it anywhere
but your own backend. Tokens are short-lived and the host renews them; caching buys you nothing and
widens the window on revocation.

The mini app never sees a refresh token, never holds a `client_secret`, and never receives any
long-lived secret. See [Authentication](/guides/authentication/).

## `sollar.room`

```ts
sollar.room.current(): Promise<Room | null>   // requires room.context
```

```ts
interface Room {
  room_id: string
  name?: string
  is_encrypted: boolean
  member_count: number
}
```

`current()` returns `null` when the mini app was launched from the home grid rather than from inside
a room. Handle that — it is the common case, not the exception.

**Check `is_encrypted`.** It changes what your backend can do; see below.

## `sollar.message`

```ts
sollar.message.send(input: {
  room_id: string,
  body: string,
  card?: Card
}): Promise<{ event_id: string }>              // requires room.send
```

Sends **as the user**, from the user's client, and prompts for confirmation each time. A mini app
cannot post silently under someone's name.

> **CAUTION**
**Your backend cannot post into an end-to-end encrypted room.** It is not a cryptographic member of
the room and the server does not hold the keys. In the Enterprise and Sovereign tiers, encryption is
on by default, so this is the normal case, not an edge case.

The two paths that work:

1. `sollar.message.send()` — the message leaves from the user's own client, under their identity.
2. An AI agent that is a cross-signed member of the room posts under its own identity.

A mini app that needs to "notify the channel when the order is approved" has to be designed around
this from the start. Finding out during integration is expensive.

## `sollar.files`

```ts
sollar.files.pick(opts?: {
  accept?: string[],          // MIME types
  multiple?: boolean
}): Promise<SollarFile[]>                      // requires files.read

sollar.files.save(input: {
  name: string,
  blob: Blob
}): Promise<{ file_id: string }>               // requires files.write
```

This is Sollar storage — the files the user already has in Sollar. For files on the device, use
`<input type="file">` and the File System Access API, which are standard and need no bridge.

## `sollar.actions`

```ts
sollar.actions.handle(
  name: string,
  handler: (input: unknown, ctx: ActionContext) => Promise<unknown>
): void

sollar.actions.elicit(req: {
  prompt: string,
  schema: JSONSchema
}): Promise<unknown | null>       // null = the human refused or cancelled

sollar.actions.progress(message: string): void
```

```ts
interface ActionContext {
  invoked_by: 'agent' | 'user'
  agent_id?: string
  confirmed: boolean          // did a human approve this specific call?
}
```

Register a handler for every action declared in the manifest. An action declared without a handler
fails validation at build time.

See [Agent actions](/guides/agent-actions/) for the full call cycle and for how to write a
description an agent uses correctly.

## `sollar.ui`

```ts
sollar.ui.setTitle(title: string): void
sollar.ui.toast(input: { message: string, kind?: 'info' | 'success' | 'error' }): void
sollar.ui.confirm(input: { title: string, body: string }): Promise<boolean>
sollar.ui.openExternal(url: string): Promise<void>
sollar.ui.close(): void
sollar.ui.share(input: { title: string, url: string }): Promise<void>
```

`openExternal()` hands the URL to the system browser. It does not open an in-app browser, and there
is no configuration that makes it one — see the `<web-view>` row above.

## `sollar.storage`

```ts
sollar.storage.get(key: string): Promise<unknown>
sollar.storage.set(key: string, value: unknown): Promise<void>
sollar.storage.remove(key: string): Promise<void>
sollar.storage.clear(): Promise<void>
```

A small key-value store that survives reinstall for the same (user, mini app) and, unlike
`localStorage`, syncs across the user's devices. Quota is 256 KB. For anything larger, use IndexedDB
— which is local, partitioned to your synthetic origin, and not synced.

## `sollar.permissions`

```ts
sollar.permissions.query(scope: PermissionScope): Promise<'granted' | 'denied' | 'prompt'>
sollar.permissions.request(scope: PermissionScope): Promise<'granted' | 'denied'>
```

Only scopes declared in the manifest can be requested. Requesting an undeclared scope throws
`ERR_SCOPE_NOT_DECLARED` — it does not prompt the user.

Request at the moment of use, with the action visible on screen. A wall of prompts at launch is
both a worse experience and a review finding.

## Errors

Every rejection is a `SollarError`:

```ts
interface SollarError extends Error {
  code: SollarErrorCode
  message: string           // human-readable, safe to log, never contains a token
  retriable: boolean
}
```

```js
try {
  await sollar.message.send({ room_id, body })
} catch (e) {
  if (e.code === 'ERR_PERMISSION_DENIED') {
    // the user said no — degrade, do not re-prompt in a loop
  } else if (e.retriable) {
    // transient; back off and retry
  }
}
```

The full list is in [Errors](/reference/errors/).

## TypeScript

The canonical type definitions are published at
[`sollar.d.ts`](/schemas/sollar.d.ts). `sollar create-mini-app` installs them; otherwise:

```sh
curl -O https://miniapp.sollar.com/schemas/sollar.d.ts
```

The definition file is the contract. Where this page and `sollar.d.ts` disagree, the `.d.ts` wins.
