Skip to content

Permissions

Two rules define this system, and both are load-bearing:

  1. A permission must be declared in the signed manifest before it can be requested.
  2. A permission granted to the Sollar app does not flow to a mini app. Each mini app asks for its own. Apple’s Guideline 4.7.3 requires this, and Sollar enforces it on both platforms.
manifest.json
"permissions": [
{
"scope": "identity.basic",
"purpose": "Shows your name on the approval you are about to sign."
},
{
"scope": "camera",
"purpose": "Photographs a paper receipt so you do not have to type it in."
}
]

The user reads it in the prompt. A reviewer holds you to it. And it converts the permission grant from a technical checkbox into a contestable claim — “you asked for the camera to photograph receipts; this app has no receipt feature” is a rejection with a specific reason attached.

“For app functionality”
“Required”
“To improve your experience”
“Photographs a paper receipt so you do not have to type it in.”

Write what the user gets, not what your code needs.

Request at the moment of use, with the reason visible on screen.

async function scanReceipt() {
const state = await sollar.permissions.query('camera')
if (state === 'denied') {
showManualEntryForm() // degrade; do not re-prompt in a loop
return
}
if (state === 'prompt') {
const result = await sollar.permissions.request('camera')
if (result !== 'granted') { showManualEntryForm(); return }
}
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
// …
}

A wall of prompts at launch is both a worse experience and a review finding. The user has no context at launch and will decline; at the moment they tap “Scan receipt”, they will accept.

Requesting an undeclared scope throws ERR_SCOPE_NOT_DECLARED. It does not prompt — a scope missing from the manifest is a build mistake, and surfacing it as a runtime prompt would hide it.

For camera, microphone and geolocation, there are two gates, and both must pass:

  1. The Sollar gate — does the manifest declare the scope, and has the user granted it to this mini app?
  2. The engine gate — the WebView’s own prompt, which the operating system controls.
await sollar.permissions.request('camera') // gate 1
await navigator.mediaDevices.getUserMedia({ video: true }) // gate 2

This is not redundancy. The engine’s prompt is the one the platform guarantees, and Sollar cannot suppress it. Sollar’s gate is what stops a mini app reaching the engine prompt at all when the app never declared the capability.

Always in native code, against the signed manifest. Never in JavaScript.

mini app JS → [sollar.* shim] → native bridge → ┌────────────────────────┐
│ 1. does the manifest │
│ declare this scope? │
│ 2. has the user │
│ granted it? │
│ 3. does tenant policy │
│ allow it? │
│ 4. within quota? │
└───────────┬────────────┘
capability

The JavaScript shim is ergonomics. If the only thing between a call and a capability were a check in JavaScript, there would be no check at all — a mini app controls its own JS environment by definition.

This matters to you as a developer for one practical reason: you cannot patch around a denied permission. There is no client-side workaround, so build the degraded path.

Permissions are revocable at any time, by the user or by a tenant administrator.

try {
await sollar.files.pick({ accept: ['application/pdf'] })
} catch (e) {
if (e.code === 'ERR_PERMISSION_REVOKED') {
// granted earlier, gone now. Re-request at the point of use.
}
if (e.code === 'ERR_TENANT_POLICY') {
// the administrator disabled it organisation-wide.
// Re-requesting will not help. Say so, plainly.
}
}

ERR_PERMISSION_DENIED and ERR_TENANT_POLICY need different responses. The first may be worth asking about again later, in context. The second will never succeed, and prompting the user for it is asking them to override their employer, which they cannot do.

An administrator can disable scopes organisation-wide, independently of what any mini app declares or any user grants. A tenant that forbids camera forbids it for every mini app in the organisation.

There is no appeal path from inside a mini app. Detect it, explain it, and offer whatever still works.

ScopeGrantsEngine prompt too?
identity.basicDisplay name, avatarno
identity.emailEmail within the tenantno
room.contextWhich room the app was opened fromno
room.sendSend as the user, confirmed per messageno
files.readOpen a file the user picks from Sollar storageno
files.writeWrite into Sollar storage at the user’s directionno
notificationsPost a notification through the hostyes
cameraWeb getUserMedia videoyes
microphoneWeb getUserMedia audioyes
geolocationWeb Geolocationyes
clipboard.writeWrite to the clipboardno
contacts.directorySearch the tenant user directoryno

contacts.directory searches the organisation’s directory, not the device address book. There is no scope for the device address book, and there will not be one — that is a native platform API, and exposing it would fall under Guideline 4.7.2.

The permission code path is identical in development, trial and production. What differs between environments is data and endpoints, never the authorisation check.

This is deliberate. “Permission behaves differently across environments” is one of the six vulnerability categories documented across nine mini-program ecosystems in the research this platform is built against — a hole that exists in only one environment is a hole nobody finds until it is exploited. The conformance suite runs the same permission tests against all three.

For store submissions:

  1. Every declared scope has a plausible purpose for the app’s stated function.
  2. No scope is declared that the code never uses.
  3. Prompts appear at the point of use, not in a launch-time wall.
  4. Denial produces a working degraded path, not a dead end or a re-prompt loop.
  5. The privacy policy covers what the permissions actually collect.

Point 2 catches more submissions than any other. Declaring a scope “in case we need it later” reads as over-collection, and it is.