Skip to content

Bridge API

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

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.

// 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
// 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

Section titled “What you came looking for and will not find”

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

WeChatSollarWhy
wx.requestPaymentNo in-app payment API. See Store 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.getLocationnavigator.geolocationThe web API already has a permission prompt the engine controls.
wx.chooseImage<input type="file"> / getUserMediaSame.
wx.setStoragelocalStorage, IndexedDBPartitioned per mini app by the engine’s own origin isolation.
wx.requestfetchRestricted to the manifest’s network allowlist, enforced natively.
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.get(): Promise<Identity>
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
}

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.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.

sollar.room.current(): Promise<Room | null> // requires room.context
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.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.

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.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
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 for the full call cycle and for how to write a description an agent uses correctly.

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.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.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.

Every rejection is a SollarError:

interface SollarError extends Error {
code: SollarErrorCode
message: string // human-readable, safe to log, never contains a token
retriable: boolean
}
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.

The canonical type definitions are published at sollar.d.ts. sollar create-mini-app installs them; otherwise:

Terminal window
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.