# Sollar Mini Apps > Documentation for building, signing, publishing and running mini apps inside the Sollar > superapp. A mini app is a signed web package that runs in a hardened WebView at its own > synthetic origin, with every capability declared in a manifest inside the signed package. > The same manifest that draws the UI is what an AI agent reads as a tool registry. ## Status This platform is SPECIFIED, NOT BUILT. These pages are a design contract, not an account of running software. Generate against the schemas — they are machine-checkable — and expect the surface to move before launch. ## Before you write code Install the CLI and read the actual version. Do not infer it. npm install -g @sollar/cli sollar --version sollar create-mini-app --template react Validate with the same checks the submission gate runs: sollar validate --strict sollar test --conformance ## Rules an agent gets wrong without being told 1. THE CLIENT IS NOT AN AUTHORITY. `sollar.identity.get()` returns data for drawing UI. Authorisation happens on your backend, against the token, every time. Authorising on a client-supplied `app_user_id` is the defining vulnerability of this platform class. 2. DEVICE CAPABILITIES ARE WEB APIs, NOT BRIDGE METHODS. Camera is `navigator.mediaDevices.getUserMedia`. Location is `navigator.geolocation`. Files are ``. `sollar.getLocation()`, `sollar.camera.take()` and `sollar.getDeviceContacts()` do not exist and will not. The bridge covers only Sollar's own domain: identity, rooms, messages, Sollar storage, agent actions. 3. THERE IS NO PAYMENT API. No `sollar.requestPayment()`, no WeChat Pay equivalent, on either platform. A mini app cannot take money in-app. 4. THE MANIFEST IS THE CEILING. A permission or a network host not declared in the signed `manifest.json` cannot be obtained at runtime. `ERR_HOST_NOT_ALLOWED` is not a bug; adding a host requires a new version. 5. A BACKEND CANNOT POST INTO AN END-TO-END ENCRYPTED ROOM. In the Enterprise and Sovereign tiers, encryption is on by default. Use `sollar.message.send()` from the user's own client, or an AI agent that is a cross-signed member of the room. 6. PERMISSION DECISIONS HAPPEN IN NATIVE CODE against the signed manifest. A check written in JavaScript is not a check — a mini app controls its own JS environment by definition. 7. OAUTH NEVER RUNS IN THE RUNTIME'S WEBVIEW. RFC 8252 §8.12 forbids embedded user-agents. `external-oauth` opens ASWebAuthenticationSession or Custom Tabs. There is no setting that changes this. 8. NO LONG-LIVED SECRET REACHES A MINI APP. No client_secret, no API key, no refresh token, no signing key. Call `sollar.auth.getToken()` before each use. ## Machine-readable contracts - https://miniapp.sollar.com/schemas/manifest.schema.json — JSON Schema 2020-12 for manifest.json - https://miniapp.sollar.com/schemas/sollar.d.ts — canonical bridge type definitions Where prose and these files disagree, these files are correct. ## Full text - https://miniapp.sollar.com/llms-full.txt — every page below, concatenated Any page is available as Markdown by appending `.md` to its path. ============================================================================== ============================================================================== # Sollar Mini Apps Source: https://miniapp.sollar.com/ ============================================================================== Draft · Pre-release **This documentation describes a platform that is being built.** Nothing here ships today. It is published early because the specification is meant to be read, argued with and corrected before the runtime is frozen — and because it is the interface a company's AI agent will read to produce a mini app. Where research did not settle a question, the page says so instead of guessing. ## What a Sollar Mini App is Sollar's core is deliberately narrow: messaging, calls, translation, groups, AI agents. Nothing else. There are no connectors, no MCP, no plugins, no skills in the core. Everything that turns Sollar into a superapp — a CRM, an ERP, an approvals queue, a mail client, a catalogue — arrives as a **mini app**: HTML, CSS and JavaScript in a signed package, verified and run in an isolated WebView, with access to a declared set of Sollar capabilities. Not a proprietary DSL. `fetch`, `getUserMedia`, IndexedDB, WebAuthn and Web Push all work. Chrome DevTools, Playwright and Vitest work. If you can build a web app, you can build a mini app. The `actions[]` you declare render as buttons for a human **and** as callable tools for a Sollar AI agent. One declaration, never two lists to keep in sync. A tenant-private mini app — your own ERP, for your own staff — is signed by your organisation and needs **no Sollar review**. Your release cycle is not our queue. Every page is served as raw Markdown at `.md`, plus `llms.txt` and `llms-full.txt`. The bridge contract is a TypeScript `.d.ts`; the manifest is JSON Schema. ## The shape of a mini app ```json title="manifest.json" { "manifest_version": 1, "name": "Acme ERP", "version": "2.4.1", "permissions": [ { "scope": "sollar.room.context", "purpose": "Attach the current conversation to a purchase order." } ], "network": { "connect": ["erp.acme.example"] }, "actions": [ { "name": "list_pending_approvals", "title": "List pending approvals", "description": "Returns purchase orders awaiting the current user's approval, most urgent first.", "annotations": { "read_only": true, "destructive": false, "idempotent": true, "open_world": false }, "confirm": "never" } ] } ``` ```js title="src/actions.js" sollar.actions.handle('list_pending_approvals', async ({ limit = 10 }) => { const { token } = await sollar.auth.getToken() const res = await fetch(`https://erp.acme.example/approvals?limit=${limit}`, { headers: { Authorization: `Bearer ${token}` }, }) return { approvals: await res.json() } }) ``` That is the whole idea. A human taps **List pending approvals**; an agent calls `list_pending_approvals`. Same code, same permission check, same audit trail. ## Where to go next | If you want to… | Read | |---|---| | Build something in ten minutes | [Get started](/start/) | | Understand what `sollar.*` can do | [Bridge API reference](/reference/bridge-api/) | | Wire a mini app to your own backend | [Authentication](/guides/authentication/) | | Make your app usable by AI agents | [Agent actions](/guides/agent-actions/) | | Know why the runtime is a WebView | [Runtime architecture](/platform/architecture/) | | Check what Apple and Google allow | [Store compliance](/compliance/) | | Point Claude Code at this site | [Build with AI](/ai/) | ============================================================================== # Get started Source: https://miniapp.sollar.com/start/ ============================================================================== You need Node.js 20 or newer and a Sollar account. You do **not** need an approved developer entity, an administrator, or permission from anyone. That comes later, and only if you publish publicly. ## 1. Install ```sh npm install -g @sollar/cli sollar login ``` `sollar login` opens your system browser for OIDC sign-in. The token lives in your OS keychain. Nothing is written to a config file — no key, no token, no secret, ever. ## 2. Create a test organisation ```sh sollar org create --name "My sandbox" ``` A **test organisation** is a throwaway tenant with synthetic users, synthetic conversations and synthetic files. Inside it: - permission and configuration changes take effect **immediately**, with no administrator review; - there is no path to production data, by credential or by network; - development-signed packages are accepted. You may hold **three** at a time. They are real infrastructure, so they are not free. ## 3. Scaffold ```sh sollar init my-mini-app # or: --template react | vue cd my-mini-app ``` You get: ``` my-mini-app/ ├── manifest.json the contract: identity, permissions, network, actions ├── sollar.config.json build config — commit this ├── .sollar/local.json your machine's preferences — gitignored ├── AGENTS.md instructions for coding agents ├── CLAUDE.md one line: @AGENTS.md └── src/ ├── main.ts ├── actions.ts your sollar.actions.handle() registrations └── pages/ ``` > **NOTE** `AGENTS.md` is the name Codex, Cursor, Jules, Factory and Aider look for. Claude Code reads `CLAUDE.md` and does **not** read `AGENTS.md`, so `CLAUDE.md` simply imports it with `@AGENTS.md`. One file is the source; the other is a pointer. Never duplicate the instructions by hand — hand-kept duplicates drift, and they drift silently. ## 4. Run ```sh sollar dev ``` This starts a local server and opens your mini app in a Sollar client in developer mode. You get hot reload, and you get the standard web toolchain: Chrome DevTools on Android, Safari Web Inspector on iOS. Sollar does not ship its own debugger, because it does not need to — the runtime *is* the web platform. Change a CSS file and it applies without reloading. Change `manifest.json` and the app reloads completely: permissions, routes and the network allowlist are re-validated by native code, and applying that hot would let the runtime and your source disagree about what is authorised. ## 5. Seed some data ```sh sollar seed --profile erp ``` Populates the test organisation with people, an org chart, conversations and files. Use it. The alternative — pointing at a real staging system "just to see it work" — is how test credentials end up in production code. ## 6. Check it before you ship ```sh sollar validate # manifest, CSP, network allowlist, SBOM sollar conformance # runs against both engines ``` `sollar conformance` matters more than it looks. iOS runs JavaScriptCore and Android runs V8 — the engines differ, permanently, because [Apple Guideline 2.5.6](/compliance/apple/) mandates WebKit. The conformance suite is how you find out before your users do. ## Next - [Your first mini app](/start/first-mini-app/) — a working approvals app, end to end - [Connect a backend](/start/connect-a-backend/) — authenticating against your own ERP - [Publish](/start/publish/) — tenant-private or public store ============================================================================== # Connect a backend Source: https://miniapp.sollar.com/start/connect-a-backend/ ============================================================================== There are two ways to authenticate, and choosing correctly matters more than anything else in this page. Pick based on **who owns the user's identity for your system.** | | `sollar-exchange` | `external-oauth` | |---|---|---| | Use when | Your backend can trust Sollar as the identity provider | Your system has its own IdP and will not delegate | | The user sees | Nothing — it is silent | A sign-in screen from your IdP, in the system browser | | You receive | A short-lived, audience-restricted token | A token from your own IdP | | Effort | One manifest field | An OAuth client, plus PKCE | Default to `sollar-exchange`. Reach for `external-oauth` only when your system genuinely owns the identity. ## Delegating identity to Sollar ```json title="manifest.json" "auth": { "type": "sollar-exchange", "audience": "acme-erp-api", "scopes": ["orders.read", "orders.approve"] } ``` ```ts const { token } = await sollar.auth.getToken() await fetch('https://erp.acme.example/approvals', { headers: { Authorization: `Bearer ${token}` }, }) ``` Behind that, the native Sollar client — which is the confidential OAuth client, not your mini app — performs an RFC 8693 token exchange against Keycloak: ```http POST /realms/{tenant}/protocol/openid-connect/token grant_type = urn:ietf:params:oauth:grant-type:token-exchange subject_token = audience = acme-erp-api scope = orders.read orders.approve ``` Your backend then validates the result the way it would validate any JWT: signature against the tenant's JWKS, `aud` equal to your audience, `exp` in the future, `iss` the expected realm. ```ts title="your backend" const claims = await verifyJwt(bearer, { jwks: `https://auth.sollar.com/realms/${tenantId}/protocol/openid-connect/certs`, audience: 'acme-erp-api', }) const userId = claims.sub // the app_user_id — stable for YOUR mini app only ``` **A token minted for your mini app is useless in another.** The audience restriction is what guarantees it: if your token leaks, the next mini app's backend rejects it on `aud`. ### Mapping Sollar users to your users `claims.sub` is the `app_user_id` — derived per (user, mini app), so two mini apps serving the same person receive different, non-correlatable identifiers. This is intentional: it is what stops a mini app store from becoming a tracking network. For your first sign-in you therefore need a linking step. Ask for `sollar.identity.basic`, use the email or employee ID your tenant has configured, and store the mapping on your side. Afterwards, `app_user_id` is your stable key. ## Signing in against your own IdP ```json title="manifest.json" "auth": { "type": "external-oauth", "issuer": "https://login.acme.example", "client_id": "acme-erp-miniapp", "scopes": ["openid", "profile", "orders"] } ``` ```ts await sollar.auth.signIn() // opens the system browser const { token } = await sollar.auth.getToken() ``` `signIn()` opens `ASWebAuthenticationSession` on iOS or Custom Tabs on Android. The user sees a real browser, with a real address bar and a real lock icon. The host exchanges the code with PKCE and stores the result in the OS keychain; your mini app never sees the refresh token. > **DANGER** RFC 8252 §8.12 forbids embedded user-agents in OAuth flows for native apps, and the reason is concrete: inside a WebView the user cannot verify the URL or the certificate, and the host application is technically capable of reading the typed password. A third-party mini app asking for corporate credentials inside a WebView controlled by another third party is precisely the attack the RFC describes. Sollar will not offer an API to do it, and no customer request will change that — including "to make it feel more integrated". ## Sending a request from your backend to Sollar When your backend calls Sollar — to post a card, say — sign the request: ``` base = "v0:" + timestamp + ":" + raw_body signature = "v0=" + hex(HMAC_SHA256(signing_secret, base)) X-Sollar-Signature: v0=… X-Sollar-Request-Timestamp: 1756704000 ``` Compare in constant time, and reject a timestamp outside a short window to block replay. ## Rules that do not bend **No long-lived secret reaches the mini app.** No `client_secret`, no API key, no refresh token, no signing key. If your design needs one in the browser, the design is wrong. WeChat documents the same rule about its own `session_key`, and for the same reason. **Client-side identity is a suggestion until your server validates it.** Always. **Call `getToken()` per use.** Do not cache it, do not persist it, do not pass it on. **Network hosts live in the manifest.** `erp.acme.example` must be in `network.connect` or the request is blocked by native code before it leaves the device — regardless of what your CSP says. ## When an agent calls on the user's behalf If a Sollar AI agent invokes one of your actions, the token carries **two** identities: the user it acts for, and the agent acting. RFC 8693 `actor_token` produces an `act` claim: ```json { "sub": "app_user_9f3c…", "act": { "sub": "agent:workforce/approvals-assistant" } } ``` Your backend can distinguish "Ana approved this" from "Ana's agent approved this" — which matters for audit, for spending limits, and for incident investigation. Read the `act` claim and record it. ============================================================================== # Your first mini app Source: https://miniapp.sollar.com/start/first-mini-app/ ============================================================================== We will build an approvals app. A human opens it from a conversation and sees what is waiting for them; an AI agent can ask the same question without opening anything. Both paths run the same code. ## The manifest Everything the runtime enforces is declared here. Nothing is discovered at runtime. ```json title="manifest.json" { "manifest_version": 1, "id": "8f3c2a1e9b7d4f60", "name": "Acme Approvals", "version": "1.0.0", "description": "Review and approve purchase orders from inside a conversation.", "icons": [ { "src": "assets/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "assets/icon-512.png", "sizes": "512x512", "type": "image/png" } ], "vendor": { "id": "acme-corp", "name": "Acme Corporation", "contact": "miniapps@acme.example" }, "distribution": { "channel": "tenant-private", "tenants": ["acme-corp"] }, "age_rating": "4+", "entry": "index.html", "routes": [ { "path": "/", "title": "Approvals" }, { "path": "/orders/:purchase_order_id", "title": "Purchase order" } ], "permissions": [ { "scope": "sollar.identity.basic", "purpose": "Show your name on approvals you submit." }, { "scope": "sollar.room.context", "purpose": "Link an approval to the conversation it came from." }, { "scope": "sollar.message.send", "optional": true, "purpose": "Post the approval result back into the conversation." } ], "network": { "connect": ["erp.acme.example"] }, "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://erp.acme.example; object-src 'none'; base-uri 'none'; frame-ancestors 'none'", "auth": { "type": "sollar-exchange", "audience": "acme-erp-api", "scopes": ["orders.read", "orders.approve"] }, "actions": [] } ``` Three things worth noticing: **Every permission carries a `purpose`.** It is shown to the user at consent time, and a reviewer can argue with it. A permission whose stated purpose does not match what the app does is a rejection reason, not a formality. **`network.connect` is a list of hosts.** It is enforced by native code in the request interceptor, not by the CSP alone — a mini app can rewrite its own CSP, but it cannot reach the interceptor. Adding a host means shipping a new signed version. That is deliberate. **`sollar.message.send` is `optional`.** The app must work without it. Ask for it when the user actually wants to post a result, not at install time. ## The page ```html title="index.html" Acme Approvals
Loading…
``` ```ts title="src/main.ts" async function boot() { const identity = await sollar.identity.get() const room = await sollar.room.getContext() sollar.ui.setTitle('Approvals') document.querySelector('#app')!.replaceChildren( await renderApprovals({ identity, room }) ) } sollar.app.onLifecycle((e) => { // The instance may have been destroyed and recreated on the same route. // Never assume continuity. if (e.type === 'resume' && (e.elapsed_ms ?? 0) > 60_000) boot() }) boot() ``` ## Fetching data ```ts title="src/api.ts" export async function listApprovals(limit = 10) { const { token } = await sollar.auth.getToken() const res = await fetch(`https://erp.acme.example/approvals?limit=${limit}`, { headers: { Authorization: `Bearer ${token}` }, }) if (!res.ok) throw new Error(`ERP returned ${res.status}`) return res.json() } ``` Call `getToken()` before each use rather than caching it. The token is short-lived and audience-restricted to `acme-erp-api`; the runtime handles renewal. > **DANGER** `sollar.identity.get()` returns client-side data. It is convenient for drawing a name in the corner. It is **not** proof of identity, and it must never be what your backend authorises against. Your ERP validates the bearer token — signature, `aud`, `exp`, `iss` — and derives the user from that. Trusting the client-supplied identity is the most common and most expensive mistake in this class of platform. Telegram publishes the same warning about `tgWebAppData`; the reason is the same. ## Adding an agent-callable action So far this is a web app in a WebView. The `actions[]` entry is what makes it something a Sollar agent can use. ```json title="manifest.json — actions" "actions": [ { "name": "list_pending_approvals", "title": "List pending approvals", "description": "Returns purchase orders awaiting the current user's approval, most urgent first. Only returns orders where the current user is a designated approver; it never returns another user's queue.", "route": "/", "input_schema": { "type": "object", "properties": { "limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10, "description": "Maximum number of purchase orders to return." } }, "additionalProperties": false }, "output_schema": { "type": "object", "properties": { "approvals": { "type": "array", "items": { "type": "object", "properties": { "purchase_order_id": { "type": "string" }, "vendor_name": { "type": "string" }, "amount_minor": { "type": "integer", "description": "Amount in the minor unit of currency_code, e.g. cents." }, "currency_code": { "type": "string", "description": "ISO 4217, e.g. USD." } }, "required": ["purchase_order_id", "vendor_name", "amount_minor", "currency_code"] } } }, "required": ["approvals"] }, "annotations": { "read_only": true, "destructive": false, "idempotent": true, "open_world": false }, "confirm": "never" } ] ``` ```ts title="src/actions.ts" sollar.actions.handle('list_pending_approvals', async ({ limit = 10 }) => { return { approvals: await listApprovals(limit) } }) ``` The `description` is doing real work here. It says what the action returns, in what order, and — critically — that it never returns another user's queue. An agent that does not know that will try. See [Agent actions](/guides/agent-actions/) for how to write these well. ## A destructive action ```json { "name": "approve_purchase_order", "title": "Approve purchase order", "description": "Approves one purchase order on behalf of the current user. This is final: an approved order is released to the vendor and cannot be revoked from within this mini app.", "input_schema": { "type": "object", "properties": { "purchase_order_id": { "type": "string", "description": "Identifier returned by list_pending_approvals." }, "note": { "type": "string", "maxLength": 500 } }, "required": ["purchase_order_id"], "additionalProperties": false }, "annotations": { "read_only": false, "destructive": true, "idempotent": true, "open_world": false }, "confirm": "always" } ``` ```ts sollar.actions.handle('approve_purchase_order', async (input) => { // The input already validated against input_schema. A schema guarantees SHAPE, never PERMISSION. // Authorise here — and again on your server. const result = await approve(input.purchase_order_id, input.note) const room = await sollar.room.getContext() if (room && await sollar.permissions.query('sollar.message.send') === 'granted') { await sollar.message.send({ room_id: room.room_id, card: { title: `Purchase order ${input.purchase_order_id} approved`, fields: [{ label: 'Vendor', value: result.vendor_name }], }, }) } return { status: 'approved', purchase_order_id: input.purchase_order_id } }) ``` With `confirm: "always"`, Sollar asks the human before this runs — showing the action title and the resolved arguments in plain language, never raw JSON. A human approving something they cannot read is not approval. > **CAUTION** `sollar.message.send()` sends **through the user's client**, with the user's identity. That is what makes it work in an end-to-end encrypted room. Your **backend cannot post into an E2EE room at all** — it is not a cryptographic member and the server holds no keys. Check `room.is_encrypted` and design around it from the start. Discovering this during integration is expensive. ## Ship it ```sh sollar validate sollar build sollar upload sollar submit # tenant-private: publishes. Store: enters review. ``` Next: [Connect a backend](/start/connect-a-backend/) or [Publish](/start/publish/). ============================================================================== # Publish Source: https://miniapp.sollar.com/start/publish/ ============================================================================== Sollar has **two distribution gates**, and the difference is not cosmetic. | | Tenant-private | Store | |---|---|---| | Who publishes | Your organisation, for its own staff | A developer, for anyone | | Sollar review | **None** | Required | | Verified entity | You are already a customer | Required | | Visible to | Your organisation only | The public catalogue | | Countersigned by | Your organisation's CA | The Sollar store key | If you are building your own ERP for your own employees, you want **tenant-private**. Your release cycle should not depend on a vendor's review queue, and it does not. A package cannot be both. Declaring both channels is a validation error, not a warning. ## Tenant-private ```json title="manifest.json" "distribution": { "channel": "tenant-private", "tenants": ["acme-corp"] } ``` ```sh sollar build sollar upload sollar submit --tenant acme-corp ``` There is no review. The package is countersigned by your organisation's CA, so there is still a verifiable chain — Sollar's client refuses a package without a countersignature from either the store or an organisation CA. What you skip is our queue, not the cryptography. Your tenant administrator controls which mini apps are available and which permissions are allowed organisation-wide. ## Store ```json title="manifest.json" "distribution": { "channel": "store" } ``` ```sh sollar submit ``` You will need a verified entity — an individual with verified identity for apps that touch no sensitive data, or a registered organisation for everything else. Sensitive categories (health, finance, education involving minors) require additional qualification documents. ## Version states Four states coexist: ``` development → trial → in review → published (your latest (named (ONE at (what users upload) testers) a time) run) ``` **Only one version can be in review at a time.** Re-submitting **overwrites** the one in review rather than queueing behind it. **Trial builds reach named testers only** — project members and testers you list explicitly. They must be members of your organisation. ## Rollout ```sh sollar release # everyone at once sollar release --phased # 1% → 2% → 5% → 10% → 20% → 50% → 100% ``` The phased curve is fixed at one step per day, and you can pause at any step. A fixed curve is better than a free percentage because it takes the decision away from the worst moment to make it — nobody chooses a good percentage at 3am with the error graph climbing. ## Rollback ```sh sollar rollback 2.4.0 ``` This republishes the code of `2.4.0` **as a new version number** — `2.4.2` — in about a minute, without review. It is not a true revert, and that is deliberate: monotonic versions are how clients know they need to update. Moving the number backwards breaks that guarantee and creates ambiguity about what is installed. Rollback skips review because the code was already reviewed. Publishing never-reviewed code this way is abuse, and the platform detects it by comparing hashes against previously approved versions. ## Mandatory updates Sollar can force an update independently of you. This is a security mechanism: a mini app with a known vulnerability cannot be left to its developer's release schedule. - **Recommended** — downloaded in the background, applied on next open. - **Mandatory** — the client refuses to open the old version. Reserved for security revocation. ## What review checks Beyond the obvious: 1. The manifest validates, and every permission's `purpose` is plausible for what the app does. 2. Every host in the network allowlist is justifiable. 3. The CSP meets the minimum — no remote script, no `unsafe-eval`. 4. An SBOM is present and no dependency carries a known high-severity CVE. 5. No remote script loading, no `eval`, no obfuscation that defeats review. 6. `actions[]` describe honestly what they do, and `destructive` is marked where it is destructive. 7. Report and block paths work; age rating is declared. Common rejection reasons, several inherited from what WeChat learned the hard way: induced sharing or following (requiring a share before the app becomes usable), false or fraudulent content, auto-playing media, cross-promotion or ranking of other mini apps — and, specific to Sollar, a misleading manifest, actions that lie about being destructive, and any attempt to collect user credentials inside the runtime WebView. ## Before you submit to the public store Read [Store compliance](/compliance/). Your mini app runs inside Sollar's iOS and Android apps, and under [Apple Guideline 4.7](/compliance/apple/) **Sollar is responsible for everything it hosts** — a mini app that violates a guideline puts the whole host app at risk, not just itself. The one that surprises people most: **there is no in-app payment API**, and the rules differ by platform. See [Monetisation](/compliance/#monetisation). ============================================================================== # Guides Source: https://miniapp.sollar.com/guides/ ============================================================================== Each page here answers one question, with working code. | Guide | Answers | |---|---| | [Authentication](/guides/authentication/) | How does the user sign in, and what does my backend verify? | | [Agent actions](/guides/agent-actions/) | How does a Sollar AI agent call my mini app? | | [Permissions](/guides/permissions/) | How do I ask for a capability, and what happens when I am refused? | | [Messaging and rooms](/guides/messaging-and-rooms/) | How do I read room context and post a message? | | [Offline and storage](/guides/offline-and-storage/) | Where do I put data, and what happens on a train? | | [Testing](/guides/testing/) | How do I test this with tools I already know? | New here? Start with [Get started](/start/) instead — it builds a working mini app end to end. Looking for the normative surface? That is [Reference](/reference/). ## Three things that catch everyone **The client is not an authority.** Anything the bridge hands your JavaScript is for drawing UI. Authorisation happens on your server, against a token, every time. **Your backend cannot post into an encrypted room.** In the Enterprise and Sovereign tiers, encryption is on by default. Decide how notifications work before you build them — [Messaging and rooms](/guides/messaging-and-rooms/) has the three patterns that do work. **The manifest is the ceiling.** A permission or a network host that is not declared cannot be obtained at runtime. There is no workaround, by design. ============================================================================== # Agent actions Source: https://miniapp.sollar.com/guides/agent-actions/ ============================================================================== Sollar's core is closed to connectors, MCP servers, plugins and skills. AI agents are nonetheless one of its five core pillars, and an agent that cannot act is a chatbot. The resolution: **a mini app is the only capability surface Sollar has**, and the `actions[]` array in its signed manifest is the tool registry. Installing a mini app simultaneously gives a human an app and gives an agent a tool. There are not two lists to keep in sync, and not two permission models to reconcile. ## What the agent sees For each installed and tenant-permitted mini app, the agent receives the declared actions in the format it already uses: ```json { "name": "acme_erp__approve_purchase_order", "description": "Approves one purchase order on behalf of the current user. This is final: an approved order is released to the vendor and cannot be revoked from within this mini app.", "input_schema": { "type": "object", "properties": { "purchase_order_id": { "type": "string" }, "note": { "type": "string", "maxLength": 500 } }, "required": ["purchase_order_id"] } } ``` The `__` prefix prevents collisions between apps that name actions the same. Nothing here is a novel protocol, deliberately. The shape mirrors Anthropic's `tool_use` / `input_schema` and OpenAI's function calling, so an agent does not have to learn a Sollar-specific convention to use a Sollar mini app. ## The call cycle ``` agent decides to call acme_erp__approve_purchase_order │ ▼ 1. runtime checks: installed? permitted in this tenant? does the action exist in the SIGNED manifest? does input match input_schema? │ ▼ 2. confirmation policy: confirm=always → ask the human BEFORE executing │ ▼ 3. RFC 8693 exchange with actor_token (the agent) + subject_token (the user) │ ▼ 4. runtime opens or resumes the mini app in the background, calls your handler │ ▼ 5. your handler runs, optionally emitting progress │ ▼ 6. result validated against output_schema, returned to the agent │ ▼ 7. audit record: who, on whose behalf, what, when, outcome ``` **Step 1 is what closes the tool-poisoning class of attack.** The action must exist in a signed, reviewed manifest — not in a server response. A compromised backend cannot invent a new tool or rewrite the description of an existing one. ## Why this is structurally safer than MCP MCP is the design reference and will not be adopted. The difference is worth stating plainly, because it is the technical justification for keeping the Sollar core closed: | | MCP | Sollar | |---|---|---| | Where the description comes from | A server response, at runtime | The manifest, in a signed package | | Can the server change it after trust is granted? | **Yes** — the rug pull | No. New version, new signature, new gate. | | Human confirmation | A SHOULD in a non-normative note | A required manifest field, enforced by the client | | Annotations | Hints (`ToolAnnotations`) | Hints — but inside a signed artifact | | Prior review | None | Store reviews; a tenant signs its own | Annotations remain declarations, not guarantees. Neither MCP nor Sollar can verify that an action marked `read_only` really is. What changes is that a false declaration in Sollar is recorded, attributable to a key, and cannot be swapped silently after a user has trusted it. ## Registering a handler ```js sollar.actions.handle('approve_purchase_order', async (input, ctx) => { const { access_token } = await sollar.auth.getToken() const res = await fetch( `https://erp.acme.example/api/orders/${input.purchase_order_id}/approve`, { method: 'POST', headers: { Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ note: input.note ?? null }), }) if (res.status === 409) { const { detail } = await res.json() throw new Error(detail) // a sentence, not a code — see below } if (!res.ok) throw new Error(`Approval failed: ${res.status}`) return await res.json() }) ``` ```ts interface ActionContext { invoked_by: 'agent' | 'user' agent_id?: string confirmed: boolean // did a human approve this specific call? } ``` > **DANGER** `ctx` is information, not authorisation. The real limit is enforced on your backend, from the `act` claim in the token — see [Authentication](/guides/authentication/). A handler that decides what an agent may do by reading `ctx.invoked_by` has placed the security boundary inside the WebView, where it does not exist. Every action declared in the manifest needs a handler. An action without one fails `sollar validate` at build time, not at 3 a.m. in production. ## Confirmation `confirm` in the manifest sets the **floor**. Tenant and user policy may tighten it; nothing can loosen it. | Value | Behaviour | |---|---| | `never` | Runs directly. Only acceptable with `read_only: true`. | | `if_destructive` | Resolves to `always` when `annotations.destructive` is true. | | `always` | Always asks a human first. | The prompt shows the action's `title`, the resolved arguments in readable language, and the mini app's name — never raw JSON. A human approving something they do not understand is not approval. For high-risk operations a tenant may require a second human approver. That is tenant policy, not mini app configuration, because authority limits belong to the organisation. ## Elicitation — when information is missing If the agent calls an action without enough information, your handler can ask the human directly: ```js sollar.actions.handle('approve_purchase_order', async (input) => { if (!input.note && await requiresNote(input.purchase_order_id)) { const note = await sollar.actions.elicit({ prompt: 'This order exceeds your standing limit. Add a justification note.', schema: { type: 'string', maxLength: 500 }, }) if (note === null) return { status: 'cancelled' } input.note = note } // … }) ``` Three outcomes, and they are different: **accept**, **decline** ("no, I will not justify it") and **cancel** ("I closed the screen"). An agent that treats declining and cancelling as the same thing makes the wrong decision on its next attempt. > **DANGER** **Elicitation never asks for a credential, a password, or sensitive personal data.** That is what the authentication flow is for, and it runs outside the WebView for exactly this reason. `sollar.actions.elicit()` throws `ERR_ELICITATION_UNAVAILABLE` when there is no interactive surface. Handle it — a background invocation is a normal case. ## Progress ```js sollar.actions.progress('Fetching the order…') sollar.actions.progress('Checking your approval limit…') ``` Surfaced in the agent's thinking view. Useful for anything over a second or two; noise below that. ## Writing a description an agent uses correctly The `description` is what decides whether the agent gets it right, and writing it is engineering, not documentation. **Make implicit context explicit.** > Returns only the current user's approval queue, never another user's. **Fully qualify parameter names.** `purchase_order_id`, not `id`. An agent holding six tools with a parameter called `id` will pass the wrong one. **State units and formats.** `amount_minor` in the currency's minor unit; `currency_code` in ISO 4217. "Amount" invites the agent to guess whether 1240 means €12.40 or €1,240. **Say what is irreversible.** This is what makes an agent ask for confirmation even when it could have skipped it. > This is final: an approved order is released to the vendor and cannot be revoked from within this > mini app. **Return errors that teach.** ```js // The agent will retry the identical call. throw new Error('ERR_CONFLICT') ``` ```js // The agent stops and can explain. throw new Error( 'Purchase order 4471 was already approved on 2026-08-30 by another approver. ' + 'No further approval is needed.') ``` **Prefer a few coarse actions to many fine ones.** One `list_pending_approvals` beats `get_approval_count` + `get_approval_ids` + `get_approval_detail`: fewer round trips, and fewer places for the agent to lose the thread mid-sequence. The manifest caps you at 64 actions, and you should be nowhere near it. ## Agents in encrypted rooms In the Enterprise and Sovereign tiers, messaging is end-to-end encrypted by default. An agent that needs to act on room content is admitted as a **verified, cross-signed device inside the room itself**. Its visibility is exactly that of an invited human member: honest and auditable, not confidentiality preserved during processing. The consequence for your mini app is direct: **your backend cannot post into an encrypted room.** Either the message goes through `sollar.message.send()` from the user's own client, or an agent that is a member of the room posts under its own identity. Design for this before you build the notification feature. ## Audit Every agent invocation writes an immutable record: mini app and version, action name, arguments (with schema-marked sensitive fields redacted), user identity, agent identity, whether a human confirmed and which one, the outcome, and a timestamp. Tenant administrators can query and export it. In tiers with E2EE, the record stays inside the customer's perimeter. ============================================================================== # Authentication Source: https://miniapp.sollar.com/guides/authentication/ ============================================================================== The case this platform exists to serve: an employee installs their company's ERP mini app and signs in with their corporate credentials. That looks simple and is not. It involves three identity authorities, a prohibition in RFC 8252, and a requirement that two mini apps must not be able to work out they are serving the same person. ## Choose a mode | Your situation | Mode | |---|---| | Your backend can trust Sollar as the identity provider | `sollar-exchange` | | Your system has its own identity provider and will not delegate | `external-oauth` | | No backend identity — a calculator, a reference tool | `none` | Declare it in the manifest. It cannot be changed at runtime. ## The host is the OAuth client The Sollar native client is the **confidential client**. Your mini app is not an OAuth client, holds no `client_secret`, and never sees a refresh token. This is the Backend-For-Frontend pattern that RFC 10017 (BCP 212, *OAuth 2.0 for Browser-Based Applications*) §6.1 recommends for browser applications — and a mini app is, technically, a browser application. ``` ┌──────────────────────────────────────────────────────┐ │ Sollar native client (confidential client) │ │ │ │ ┌──────────────┐ audience-scoped token │ │ │ your mini app│ ◀────────────────────────┐ │ │ │ (WebView) │ │ │ │ └──────────────┘ ┌──────┴──────┐ │ │ │ RFC 8693 │ │ │ │ exchange │ │ │ └──────┬──────┘ │ └────────────────────────────────────────────┼─────────┘ │ ┌──────▼──────┐ │ Keycloak │ └─────────────┘ ``` ## Mode 1 — `sollar-exchange` ```json title="manifest.json" "auth": { "mode": "sollar-exchange", "audience": "acme-erp-backend", "scopes": ["erp.read", "erp.approve"] } ``` In the mini app: ```js const { access_token } = await sollar.auth.getToken() const res = await fetch('https://erp.acme.example/api/approvals', { headers: { Authorization: `Bearer ${access_token}` } }) ``` That is the whole client side. There is no sign-in screen, because the user is already signed into Sollar. Behind it, the native runtime performs: ```http POST /realms/acme-corp/protocol/openid-connect/token grant_type=urn:ietf:params:oauth:grant-type:token-exchange subject_token= audience=acme-erp-backend scope=erp.read erp.approve ``` Keycloak returns a short-lived token whose `aud` is your backend and nothing else. If mini app A leaks its token, mini app B's backend rejects it on `aud`. > **NOTE** The exchange is keyed on `audience`, not on RFC 8707's `resource`. Keycloak's Standard Token Exchange V2 is GA and is the internal-to-internal path, but it does not yet support the `resource` parameter. Binding the design to `resource` today would not work; when Keycloak adds it, moving is a precision improvement rather than a rework. ### Verifying on your backend ```js const jwks = createRemoteJWKSet( new URL('https://auth.sollar.com/realms/acme-corp/protocol/openid-connect/certs')) export async function authenticate(req) { const token = req.headers.authorization?.replace(/^Bearer /, '') if (!token) throw new Unauthorized() const { payload } = await jwtVerify(token, jwks, { issuer: 'https://auth.sollar.com/realms/acme-corp', audience: 'acme-erp-backend', // yours, exactly }) return { appUserId: payload.sub, // the identity you may act on scopes: String(payload.scope ?? '').split(' '), actingAgent: payload.act?.sub ?? null, // set when an AI agent is acting } } ``` Check all four: signature, `iss`, `aud`, `exp`. Skipping `aud` is the mistake that makes token leakage between mini apps exploitable — it is the check that isolates you from every other mini app on the platform. ## Mode 2 — `external-oauth` For a system with its own identity authority. ```json title="manifest.json" "auth": { "mode": "external-oauth", "issuer": "https://login.acme.example", "client_id": "acme-erp-sollar", "scopes": ["openid", "erp.read"] } ``` ```js if (await sollar.auth.state() === 'signed-out') { await sollar.auth.signIn() // opens a system authentication sheet } const { access_token } = await sollar.auth.getToken() ``` What happens: the runtime opens **`ASWebAuthenticationSession`** on iOS or **Custom Tabs** on Android. The user authenticates against your identity provider, seeing a real address bar and a real lock icon. The redirect returns to the host through a registered scheme; the host exchanges the code with PKCE and stores the result in the keychain or keystore. Your mini app receives access tokens and never a refresh token. > **DANGER** **This flow never runs in the runtime's WebView, and there is no setting that changes it.** RFC 8252 §8.12 is explicit that embedded user-agents are forbidden in native-app OAuth flows. Inside a WebView the user cannot verify the URL or the certificate, and the hosting app is technically capable of reading the typed password. A third-party mini app asking for corporate credentials inside a WebView controlled by another third party is precisely the attack the RFC describes. Customers will occasionally ask for it, "so it feels more integrated". The answer is no. ## Mode 3 — `none` No identity, no token, no backend session. Declare it honestly; a mini app declaring `none` and then calling `getToken()` fails validation. ## Rules that do not bend **No long-lived secret reaches the runtime.** No `client_secret`, no API key, no refresh token, no signing key. WeChat documents the same rule about its own `session_key` — *"the developer server should not send the session key to the Mini Program"* — and the reasoning is identical. **Every client-side identity claim is a suggestion until your server validates it.** What `sollar.identity.get()` returns is for drawing the UI. Authorisation happens against the token, on your server, every time. Telegram publishes the same warning about `tgWebAppData`, and it is the most common failure in this entire class of platform. **Short tokens, host-managed renewal.** Call `getToken()` before each use. Do not cache, do not persist, do not forward. **Revocation reaches you.** An uninstall, an administrator removing the app, or an employee leaving revokes the exchange immediately. Because tokens are short, the window is small; because the exchange runs through Keycloak, the cut is central. Handle `user.deprovisioned` — see [Server API](/reference/api/server/). ## Agents acting for a user When a Sollar AI agent invokes one of your actions, the chain records **two** identities: on whose behalf, and who is acting. ```http grant_type=urn:ietf:params:oauth:grant-type:token-exchange subject_token= ← on whose behalf actor_token= ← who is acting actor_token_type=urn:ietf:params:oauth:token-type:jwt audience=acme-erp-backend ``` The resulting token carries an `act` claim: ```json { "sub": "u_9f3a…", "act": { "sub": "agent_admin_assistant" }, "aud": "acme-erp-backend" } ``` ```js if (auth.actingAgent && amountMinor > tenantPolicy.agentLimitMinor) { throw new Forbidden( `An agent may approve up to €${tenantPolicy.agentLimitMinor / 100}. ` + `This order is €${amountMinor / 100} and needs ${user.name} to approve it directly.`) } ``` Your backend can distinguish "Ana approved this" from "Ana's agent approved this" — which matters for audit, for spending limits, and for incident investigation. A tenant may permit an agent to read but not to approve; the `act` claim is what makes that enforceable. For delegation across a trust boundary — a Sollar agent acting against the tenant's own identity provider — the `draft-ietf-oauth-identity-chaining` work is the direction of travel. It is a draft: treat it as direction, not as a dependency. ## Testing Your test organisation issues real tokens against a test realm. Permission and configuration changes take effect immediately, without administrator approval, so the loop is short. Never point a test organisation at production data. The limit is three test organisations per developer. ============================================================================== # Messaging and rooms Source: https://miniapp.sollar.com/guides/messaging-and-rooms/ ============================================================================== Sollar is a messenger first. A mini app can be launched from inside a room, can know which room, and can send a message on the user's behalf. What it cannot do is post from a server into an encrypted room — and that constraint should shape your design from the first sketch. ## Room context ```js const room = await sollar.room.current() // requires room.context if (room === null) { // Launched from the home grid, not from a room. This is the common case. renderStandalone() } else { renderForRoom(room) } ``` ```ts interface Room { room_id: string name?: string is_encrypted: boolean member_count: number } ``` `null` is normal, not an error. Most launches come from the home grid. **Check `is_encrypted` early** — it decides what your backend can do, and the answer should shape the UI, not surface as a failure later. ## Sending as the user ```js await sollar.message.send({ room_id: room.room_id, body: 'Purchase order 4471 approved — €12,400 to Nordwerk GmbH.', }) // requires room.send ``` The message leaves from **the user's own client**, under **the user's identity**, and the user confirms each send. A mini app cannot post silently under someone's name, and there is no scope that enables it. Rate limit: 10 per minute, each confirmed. ### Cards ```js await sollar.message.send({ room_id: room.room_id, body: 'Purchase order 4471 approved', // plain-text fallback card: { title: 'Purchase order 4471 approved', body: '€12,400 · Vendor: Nordwerk GmbH', actions: [{ label: 'Open', deep_link: '/orders/4471' }], }, }) ``` `body` is required even when you send a card. It is what appears in notifications, in search, in accessibility tooling, and in any client that cannot render your card. A card with a `body` of "Update" is a card nobody can find again. `deep_link` resolves against the routes in your manifest's `pages`. A link to an undeclared route does not open. ## The encryption constraint > **CAUTION** **Your backend cannot post into an end-to-end encrypted room.** Sollar's servers do not hold the room keys, and your backend is not a cryptographic member of the room. That is what makes the encryption real, so there is no server-side path around it — not a scope, not an enterprise tier, not a support ticket. In the Enterprise and Sovereign tiers, encryption is **on by default**. Assume encrypted. Two paths that work: **1. Send from the user's client.** ```js // Runs in the mini app, in the user's session, with the user's keys. await sollar.message.send({ room_id, body }) ``` Works in any room the user is in, encrypted or not. Requires the mini app to be open, and requires the user to confirm. **2. An agent that is a member of the room.** An AI agent admitted to a room as a verified, cross-signed device can post under its own identity. It sees the room's content exactly as an invited human member would — visible and auditable, not confidential during processing. Your mini app's `actions[]` are what the agent calls; the agent posts the result. ### Designing around it The pattern that does **not** work: ``` order approved on your server → your server posts to the room → ✗ encrypted room ``` The patterns that do: ``` order approved → push notification to the user → user opens the mini app → mini app posts with sollar.message.send() ``` ``` order approved → your server tells the agent → the agent, a room member, posts ``` ``` order approved → your server posts to a NON-encrypted room → works ``` The third is real: not every room is encrypted, and a tenant may designate an unencrypted channel for system notifications precisely so that integrations can reach it. That is a tenant decision, not one your mini app can make. **Decide this before you build.** Discovering during integration that the notification feature is impossible as designed is the expensive way to learn it. ## Notifications instead When you cannot post to the room, notify the user directly: ```http POST /v1/users/{app_user_id}/notifications ``` Requires the `notifications` scope from that user. It reaches the person rather than the channel — which is often what was actually wanted, and is what makes the encrypted-room limit less painful than it first sounds. See [Server API](/reference/api/server/). ## Deep links Every route in `pages` is deep-linkable: ``` https://sollar.com/app/com.acme.erp/orders/4471 ``` Opens Sollar, launches the mini app, and routes to `/orders/4471`. Parameters arrive at `sollar.app.launchParams`. Public universal links for store-channel mini apps also satisfy Apple's Guideline 4.7.4, which requires an index of the software available in the app, with universal links to all of it. That index is [the public catalogue](/apps/). ## What a mini app cannot do | | Why | |---|---| | Read a room's message history | No scope grants it. A mini app is not a client. | | Post without the user confirming | Structural: sends are per-message confirmed. | | Post as another user or as the system | The message carries the user's identity. | | Create, join or leave a room | Room membership is the messenger's job. | | Message a user who has not installed the app | No installation, no channel. | | Navigate to another mini app | Cross-promotion, ranking and inter-app navigation are prohibited. | The last row is a store policy borrowed verbatim from WeChat's rejection criteria, and the reason is sound: a store where mini apps promote each other becomes an attention marketplace rather than a tool directory. ============================================================================== # Offline and storage Source: https://miniapp.sollar.com/guides/offline-and-storage/ ============================================================================== A mini app runs at its own synthetic origin — `https://.miniapp.localhost/` on Android, served through a custom scheme handler on iOS. Every web storage mechanism is therefore partitioned to that origin **by the browser engine**, not by a convention the runtime remembers to apply. That distinction is the point. Cache reuse between mini apps is the first of six vulnerability categories documented across nine mini-program ecosystems, and it exists in those platforms because they store everything in a shared directory and rely on the runtime to keep the paths apart. Origin isolation is a rule the engine cannot violate. ## Choosing | Need | Use | Quota | Syncs across devices | |---|---|---|---| | Small preference, should follow the user | `sollar.storage` | 256 KB | **yes** | | Structured local data, offline records | IndexedDB | ~50 MB | no | | Trivial local flag | `localStorage` | ~5 MB | no | | Static assets for offline launch | Cache API + service worker | shared with IndexedDB | no | | Anything secret | **nowhere on the client** | — | — | > **DANGER** No token, no key, no credential, no long-lived secret goes into any of these. All of them are readable by the page, and the page runs on a device you do not control. Call `sollar.auth.getToken()` before each use and let the host manage renewal. ## `sollar.storage` ```js await sollar.storage.set('preferred_view', 'compact') const view = await sollar.storage.get('preferred_view') // undefined if unset await sollar.storage.remove('preferred_view') ``` Keyed to (user, mini app). Survives reinstall and follows the user to a new device — which `localStorage` does not. Use it for the handful of preferences that should follow a person, not for data. Exceeding 256 KB throws `ERR_STORAGE_FULL`. It is a preference store; if you are near the limit you want IndexedDB. ## IndexedDB Local, per-origin, and the right place for an offline record set. ```js const db = await new Promise((ok, err) => { const req = indexedDB.open('acme-erp', 1) req.onupgradeneeded = () => { req.result.createObjectStore('orders', { keyPath: 'purchase_order_id' }) } req.onsuccess = () => ok(req.result) req.onerror = () => err(req.error) }) ``` It is not synced and not backed up. Treat it as a cache that can vanish: a user clearing storage, an OS reclaiming space under pressure, or a reinstall all take it. Anything that must survive belongs on your server. ## Offline ```json title="manifest.json" "capabilities": { "offline": true } ``` Declaring `offline` lets the mini app launch with no network. The package is already local — it was installed, not fetched — so the shell always loads. What you have to decide is what happens when your API is unreachable. ### A service worker for the shell ```js title="sw.js" const SHELL = 'shell-v3' self.addEventListener('install', (e) => { e.waitUntil(caches.open(SHELL).then((c) => c.addAll(['/', '/index.html', '/app.css', '/app.js']))) }) self.addEventListener('fetch', (e) => { const url = new URL(e.request.url) // Same-origin: cache first. The package is local; it cannot be stale. if (url.origin === location.origin) { e.respondWith(caches.match(e.request).then((r) => r ?? fetch(e.request))) return } // Your API: network first, fall back to the last good response. e.respondWith( fetch(e.request) .then((res) => { const copy = res.clone() caches.open('api-v1').then((c) => c.put(e.request, copy)) return res }) .catch(() => caches.match(e.request))) }) ``` > **CAUTION** Service workers require a secure context. Mini apps are served over `https` at a synthetic origin, so they qualify — but on iOS this depends on a custom scheme served through `WKURLSchemeHandler` being treated as a secure context, which is verified per release and tracked as an open risk. Feature-detect rather than assume: ```js if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') } ``` ### A write queue The harder half of offline is not reading; it is what happens to a write made on a train. ```js async function queueApproval(orderId, note) { const tx = (await openDb()).transaction('outbox', 'readwrite') await tx.objectStore('outbox').add({ id: crypto.randomUUID(), // the idempotency key kind: 'approve', order_id: orderId, note, queued_at: new Date().toISOString(), }) } async function flushOutbox() { if (!navigator.onLine) return const { access_token } = await sollar.auth.getToken() for (const item of await readOutbox()) { const res = await fetch(`https://erp.acme.example/api/orders/${item.order_id}/approve`, { method: 'POST', headers: { Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json', 'Idempotency-Key': item.id, // the same key on every retry }, body: JSON.stringify({ note: item.note }), }) if (res.ok || res.status === 409) await removeFromOutbox(item.id) else break // stop; try the whole queue again later } } window.addEventListener('online', flushOutbox) ``` Three things that are not optional: **The idempotency key is generated when the item is queued, not when it is sent.** A retry after a timeout on a request the server *did* process is the normal case, and only a stable key makes the second attempt harmless. **`409` counts as done.** "Already approved" means the queue succeeded on an earlier attempt whose response was lost. **Stop at the first hard failure.** Draining the rest of a queue against a server that is refusing requests turns one problem into many. ### Be honest in the UI An action that is queued is not an action that happened. Show it as pending. A mini app that renders an approval as complete when it is sitting in an outbox has told the user something untrue, and they will act on it. ## What you never cache - Tokens. Call `getToken()` each time. - Anything you would not want read off a stolen, unlocked device. - Personal data beyond what the current screen needs. When a user is deprovisioned, your backend receives `user.deprovisioned` and must delete their data — but you cannot reach the copy on their device. Cache the minimum, and clear it on sign-out: ```js await sollar.storage.clear() for (const key of await caches.keys()) await caches.delete(key) indexedDB.deleteDatabase('acme-erp') ``` ## Quotas and eviction Storage is subject to eviction under pressure, and mini apps are evicted before the host. Do not build anything whose correctness depends on the cache still being there — including a "we already synced this" flag whose loss would silently skip a sync. ## Uninstall Uninstalling removes every trace on the device: package, origin storage, caches, and the `sollar.storage` entries for that user. Nothing survives except what your backend holds — which is why `user.deprovisioned` matters, and why the deletion obligation is yours. ============================================================================== # Permissions Source: https://miniapp.sollar.com/guides/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. ## Declaring ```json title="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 purpose string is not decoration 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. ## Requesting Request at the moment of use, with the reason visible on screen. ```js 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. ## Two prompts for one capability 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. ```js 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. ## Where the decision is made **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. ## Losing a permission Permissions are revocable at any time, by the user or by a tenant administrator. ```js 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. ## Tenant policy 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. ## Scopes | Scope | Grants | Engine prompt too? | |---|---|---| | `identity.basic` | Display name, avatar | no | | `identity.email` | Email within the tenant | no | | `room.context` | Which room the app was opened from | no | | `room.send` | Send as the user, confirmed per message | no | | `files.read` | Open a file the user picks from Sollar storage | no | | `files.write` | Write into Sollar storage at the user's direction | no | | `notifications` | Post a notification through the host | yes | | `camera` | Web `getUserMedia` video | **yes** | | `microphone` | Web `getUserMedia` audio | **yes** | | `geolocation` | Web Geolocation | **yes** | | `clipboard.write` | Write to the clipboard | no | | `contacts.directory` | Search the tenant user directory | no | `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. ## Consistency across environments 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. ## What reviewers check 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. ============================================================================== # Testing Source: https://miniapp.sollar.com/guides/testing/ ============================================================================== Because the runtime **is** the web platform, the testing tools you already use work unchanged. There is no Sollar equivalent of `miniprogram-simulate` or `miniprogram-automator`, and there will not be — Vitest, jsdom and Playwright already do the job better than a platform-specific reimplementation would. ## Test organisations A test organisation is a real Sollar tenant with real tokens, isolated from anything that matters. ```sh sollar org create acme-erp-test sollar dev --tenant acme-erp-test ``` | | | |---|---| | Permission and configuration changes | Take effect **immediately** — no administrator approval | | Limit | 3 per developer | | Production data | **Never.** Synthetic data only. | The immediate-effect rule is what makes the loop short: changing a scope during development should not require a person in another department. ## Three environments | | Data | Endpoints | Permission checks | Who reaches it | |---|---|---|---|---| | **Development** | Synthetic | Your dev backend | Identical | You | | **Trial** | Synthetic or staging | Staging | Identical | Named testers | | **Production** | Real | Production | Identical | Users | **The permission code path is identical in all three.** What changes is data and endpoints, never the authorisation check. Environment-divergent permission behaviour is one of the six documented vulnerability categories in this platform class, and it is designed out rather than tested for. ### Trial versions ```sh sollar upload --trial sollar tester add ana@acme.example ``` Only project members and explicitly named testers can reach a trial version. Testers must be members of the organisation — the tenant is the natural boundary, so there is no separate invitation system to administer. Each developer holds one development version at a time; uploading replaces it. That is a working state, not a history. ## Unit tests ```sh npm install -D vitest jsdom ``` Mock the bridge. It is a plain object, so this is not elaborate: ```js title="test/setup.js" globalThis.sollar = { version: '1.0.0', supports: () => true, app: { id: 'com.acme.erp', locale: 'en', theme: 'light' }, identity: { get: vi.fn().mockResolvedValue({ app_user_id: 'u_test', tenant_id: 'acme-corp-test' }) }, auth: { getToken: vi.fn().mockResolvedValue({ access_token: 'test-token', expires_in: 300 }) }, room: { current: vi.fn().mockResolvedValue(null) }, message: { send: vi.fn().mockResolvedValue({ event_id: '$evt' }) }, permissions: { query: vi.fn().mockResolvedValue('granted'), request: vi.fn().mockResolvedValue('granted') }, actions: { handle: vi.fn(), elicit: vi.fn(), progress: vi.fn() }, ui: { toast: vi.fn(), confirm: vi.fn().mockResolvedValue(true) }, storage: { get: vi.fn(), set: vi.fn(), remove: vi.fn(), clear: vi.fn() }, } ``` ### Test the paths you hope never happen ```js it('offers manual entry when the camera is refused', async () => { sollar.permissions.request.mockResolvedValue('denied') render() await user.click(screen.getByRole('button', { name: /scan/i })) expect(screen.getByLabelText(/enter amount/i)).toBeVisible() }) it('does not offer channel posting in an encrypted room', async () => { sollar.room.current.mockResolvedValue({ room_id: '!r:sollar.com', is_encrypted: true, member_count: 8, }) render() expect(screen.queryByRole('button', { name: /notify channel/i })).toBeNull() }) it('keeps one idempotency key across retries', async () => { await queueApproval('4471', 'urgent') fetchMock.mockRejectOnce(new Error('offline')) await flushOutbox() await flushOutbox() const [first, second] = fetchMock.mock.calls expect(first[1].headers['Idempotency-Key']) .toBe(second[1].headers['Idempotency-Key']) }) ``` Denied permissions, encrypted rooms and retried writes are where mini apps actually break. The happy path tends to work. ## End-to-end tests Playwright drives `sollar dev` directly — it is a web page at a real origin. ```js title="e2e/approvals.spec.js" test('approves an order', async ({ page }) => { await page.goto('https://com.acme.erp.miniapp.localhost/') await page.getByRole('button', { name: 'Approve 4471' }).click() await page.getByRole('button', { name: 'Confirm' }).click() await expect(page.getByText('Approved')).toBeVisible() }) ``` ```js title="playwright.config.js" export default { webServer: { command: 'sollar dev --tenant acme-erp-test', port: 4321 }, use: { ignoreHTTPSErrors: true }, // the synthetic origin's local certificate } ``` ## Testing actions An action handler is a function. Call it. ```js it('surfaces a conflict as a sentence the agent can act on', async () => { fetchMock.mockResponseOnce( JSON.stringify({ detail: 'Purchase order 4471 was already approved on 2026-08-30 by another approver.' }), { status: 409 }) await expect(handlers.approve_purchase_order({ purchase_order_id: '4471' })) .rejects.toThrow(/already approved on 2026-08-30/) }) ``` Assert on the *content* of the error, not just that it threw. An agent reads that sentence and decides whether to retry; "it threw" does not tell you whether it will. Verify your schemas match your handlers: ```js it('every declared action has a handler', () => { for (const action of manifest.actions) { expect(handlers[action.name], `no handler for ${action.name}`).toBeTypeOf('function') } }) ``` `sollar validate` checks this too, but a test failure names the action. ## On device ```sh sollar build && sollar preview ``` Prints a QR code. Scanning it in Sollar installs the build on your phone. This is where you find what the emulator hides: how it feels on a slow network, whether the touch targets work, whether the keyboard covers the field you need. ```sh sollar logs --tenant acme-erp-test ``` Streams `console` output and bridge traffic from trial installs. Chrome DevTools attaches to Android over USB; Safari Web Inspector attaches to iOS. Both work normally, because there is nothing unusual to attach to. ## Conformance ```sh sollar test --conformance ``` Runs Sollar's suite against the attached runtime: the ES2022 floor, the CSS baseline, every `sollar.*` method, and every permission path. Run it against the iOS simulator and an Android emulator before submitting — JavaScriptCore and V8 differ, and this is what surfaces the difference before a user does. ## Continuous integration ```yaml title=".github/workflows/ci.yml" - run: npm ci - run: npm run test:unit - run: npx playwright install --with-deps chromium - run: npm run test:e2e - run: npx sollar validate --strict - run: npx sollar build --sbom ``` `sollar validate --strict` applies the store review checks. Running it in CI means a review failure is a red build, not a rejected submission three days later. ============================================================================== # Reference Source: https://miniapp.sollar.com/reference/ ============================================================================== Everything in this section is **normative**. If the documentation and the validator disagree, the validator is right and this page is a bug — report it. | Page | What it fixes | |---|---| | [Manifest](/reference/manifest/) | Every field of `manifest.json`, and what the validator rejects | | [Bridge API](/reference/bridge-api/) | The complete `sollar.*` namespace | | [Package format](/reference/package-format/) | `.sapp` layout, signing, updates | | [CLI](/reference/cli/) | `sollar` commands and flags | | [Errors](/reference/errors/) | Every error code the bridge can throw | | [Compatibility baseline](/reference/baseline/) | The JavaScript and CSS floor you can rely on | | [Mini app ↔ superapp API](/reference/api/) | The contract between your code and Sollar | ## Two things that are true everywhere **The client is not an authority.** Anything `sollar.*` hands your JavaScript — an identity, a room, a permission state — is a hint for drawing UI. Authorisation happens on your server, against a token, every time. This is the single most common way platforms in this category get breached, and it is the reason the warning is repeated on nearly every page here rather than stated once. **The signed manifest is the contract.** Permissions, network destinations and agent actions are declared in a file inside a signed package. Nothing your server sends at runtime can widen them. A compromised backend cannot grant itself a permission, reach a new host, or invent an agent tool. ## Versioning The bridge is versioned independently of the Sollar client. `sollar.version` returns the bridge version at runtime; `sollar.supports('')` answers whether a method exists before you call it. ```js if (sollar.supports('actions.elicit')) { // use it } ``` Feature detection over version comparison. Version numbers tell you what shipped; `supports()` tells you what this device actually has, which is the question you are really asking. ## Stability | Marker | Meaning | |---|---| | *(unmarked)* | Stable. Breaking changes require a major bridge version and a deprecation window. | | `Preview` | Shipping, shape may still change. Safe to try, not safe to depend on. | | `Planned` | Specified here, not implemented. Calling it throws `ERR_NOT_IMPLEMENTED`. | This documentation set is itself pre-release: the platform described here is specified but not yet built. Treat every page as a design contract, not as an account of running software. ============================================================================== # Compatibility baseline Source: https://miniapp.sollar.com/reference/baseline/ ============================================================================== A mini app runs in **JavaScriptCore on iOS** and **V8 on Android**. That is not a design choice Sollar made; it is a legal one it inherited. Apple's Guideline 2.5.6 requires that apps browsing the web *"use the appropriate WebKit framework and WebKit JavaScript."* The Embedded Browser Engine Entitlement that would allow otherwise exists only in the European Union, and qualifying for it requires 90% pass rates on the Web Platform Tests, 80% on Test262, a memory-safe implementation language and a 30-day CVE remediation commitment. So the engines differ, and they will keep differing. What Sollar can do is name a floor and test against it. ## The floor | | Baseline | |---|---| | JavaScript | ECMAScript 2022 | | CSS | Everything in Baseline "Widely available" as of 2024 | | iOS | WKWebView on iOS 16.4+ | | Android | Android System WebView 114+ | Anything at or below this floor works on every device Sollar supports. Anything above needs feature detection. ## What ES2022 gives you Class fields and private methods, `at()`, `Object.hasOwn()`, top-level `await`, `Error.cause`, `Array.prototype.findLast()`, RegExp match indices. Modules, `async`/`await`, optional chaining and nullish coalescing are all far below the floor. ## Above the floor — check first | Feature | Note | |---|---| | `Array.prototype.group` / `groupBy` | Shipped at different times on the two engines. Detect. | | `Temporal` | Not on the floor. Use a library. | | Decorators | Compile them; do not ship them raw. | | `structuredClone` | Available, but with divergent handling of some types. | | Web Locks | Not on iOS. | | `showOpenFilePicker` | Chromium only. Fall back to ``. | | `container-queries` | Above the floor on the oldest supported iOS. Detect. | | `:has()` | Same. | ```js if ('group' in Array.prototype) { /* … */ } else { /* … */ } if (CSS.supports('selector(:has(a))')) { /* … */ } ``` Feature detection, never user-agent sniffing. The user agent inside a mini app is deliberately uninformative, and treating it as a capability signal will produce wrong answers on both platforms. ## Known divergences | | JavaScriptCore (iOS) | V8 (Android) | |---|---|---| | Stack trace format | `Error.stack` differs in shape | | | `toLocaleString` output | ICU version differs; the *string* differs | | | Regex Unicode property escapes | Both support them; edge cases differ | | | Timer clamping in background | More aggressive on iOS | | | Number formatting near precision limits | Differs in the last digit | | The practical rule: **never parse a formatted string you produced with `toLocaleString`.** Format for display, compute on the underlying value. This is the single most common source of a bug that appears on one platform only. ## Conformance suite ```sh sollar test --conformance ``` Runs Sollar's own suite against whichever runtime is attached — dev server, iOS simulator, Android emulator, or a physical device. It covers the ES2022 floor, the CSS baseline, every `sollar.*` method, and the permission paths. **The permission tests run identically in all three environments.** Development, trial and production share one authorisation code path; what changes between environments is data and endpoints, never the permission check. Environment-divergent permission behaviour is one of the six vulnerability categories this platform is designed against — see [Security model](/platform/security-model/). ## Version policy - The floor rises **at most once a year**, announced at least two release cycles ahead. - A raised floor never breaks an installed mini app; it changes what new submissions may assume. - Removing anything from the bridge requires a major version and a deprecation window. ## Secure context Mini apps run at a synthetic origin over `https`, so they are secure contexts and `crypto.subtle`, service workers and the rest are available. > **CAUTION** On iOS this depends on a custom scheme served through `WKURLSchemeHandler` being treated as a secure context. This is verified per release and is tracked as an open risk in the internal specification — if it fails on a future iOS version, `crypto.subtle` becomes unavailable inside mini apps and the encrypted-storage design changes. Do not build a design whose only failure mode is this assumption holding. ============================================================================== # Bridge API 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. | | `` | `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` | `` / `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 // deep-link parameters ``` ## `sollar.identity` ```ts sollar.identity.get(): Promise ``` ```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 // external-oauth mode only sollar.auth.signOut(): Promise 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 // 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 // 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 `` 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 ): void sollar.actions.elicit(req: { prompt: string, schema: JSONSchema }): Promise // 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 sollar.ui.openExternal(url: string): Promise sollar.ui.close(): void sollar.ui.share(input: { title: string, url: string }): Promise ``` `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 `` row above. ## `sollar.storage` ```ts sollar.storage.get(key: string): Promise sollar.storage.set(key: string, value: unknown): Promise sollar.storage.remove(key: string): Promise sollar.storage.clear(): Promise ``` 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. ============================================================================== # CLI Source: https://miniapp.sollar.com/reference/cli/ ============================================================================== ```sh npm install -g @sollar/cli sollar --version ``` The CLI is **headless by design**. Every command that a release depends on runs without a GUI, so continuous integration never needs a desktop IDE installed. This is a deliberate correction of the most common complaint about WeChat's `miniprogram-ci`, where the compiler lived inside the IDE and CI had to work around it. ## Commands | Command | Does | |---|---| | `sollar init` | Adds Sollar scaffolding to an existing project | | `sollar create-mini-app ` | New project from a template | | `sollar dev` | Local dev server with hot reload | | `sollar validate` | Validates the manifest and the project against the submission gate | | `sollar build` | Produces the `.sapp` package | | `sollar sign` | Adds the signature block | | `sollar preview` | Opens the built package on your device via QR code | | `sollar upload` | Uploads a development or trial version | | `sollar submit` | Submits for review (store) or publishes (tenant-private) | | `sollar release` | Publishes an approved version | | `sollar rollback` | Republishes a previous build as a new version | | `sollar logs` | Streams runtime logs from trial installs | | `sollar doctor` | Diagnoses the environment | ### `sollar create-mini-app` ```sh sollar create-mini-app acme-erp --template react ``` Templates: `vanilla` (default), `react`, `vue`, `svelte`. All are plain Vite projects — the runtime *is* the web platform, so there is no proprietary dialect to compile into and no framework you are obliged to use. The scaffold includes `sollar.d.ts`, a validated `manifest.json`, an `AGENTS.md`, and a `CLAUDE.md` that imports it. See [Build with AI](/ai/). ### `sollar dev` ```sh sollar dev --port 4321 --tenant acme-corp-test ``` Serves the mini app at its synthetic origin — the same `https://.miniapp.localhost/` the device uses — so origin-dependent behaviour matches production from the first run. Reloads the page on source changes; a manifest change restarts the runtime, because permissions and the network allowlist are read at launch. Chrome DevTools and Safari Web Inspector attach normally. Sollar does not ship a debugger, because the platform is the web and you already have two good ones. ### `sollar validate` ```sh sollar validate # manifest + project sollar validate --strict # also applies store-review checks ``` Checks the manifest against the published schema, verifies every declared action has a handler, verifies every requested scope is declared, checks the CSP floor, and rejects bare IP addresses in the network allowlist. `--strict` adds the review checks: purpose strings that plausibly match the declared function, justifiable network hosts, an SBOM with no high-severity findings, no remote scripts, no `eval`. Run it in CI. A validation failure at submission has already cost you a round trip. ### `sollar build` ```sh sollar build --sbom --mode production ``` | Flag | Effect | |---|---| | `--sbom` | Emits `sbom.cdx.json` (CycloneDX 1.7). Required for the store channel. | | `--mode` | `development` \| `production` | | `--out` | Output directory (default `dist/`) | | `--delta-from ` | Also produces a patch against that version | ### `sollar sign` ```sh sollar sign --key op://Personal/sollar-signing/private-key sollar sign --keyless ``` > **DANGER** `--key` takes a **reference**, never a key. The CLI resolves it at the moment of signing and never writes key material to disk. A literal path to a private key file is refused. `--keyless` uses Sigstore: an ephemeral key bound to an OIDC identity, with a transparency-log record. Nothing to store, and therefore nothing to leak. Recommended for CI. ### `sollar submit` ```sh sollar submit --tenant acme-corp # tenant-private: publishes, no review sollar submit # store: enters the review queue sollar submit --notes "Fixes the expense export." ``` For the store channel, only one version can be in review at a time. Submitting again **replaces** the queued version rather than queueing behind it. ### `sollar release` ```sh sollar release --version 2.4.1 sollar release --version 2.4.1 --phased ``` Approval and publication are separate steps: review approves, you decide when it goes live. An approval that is never released expires after 30 days. `--phased` follows a fixed curve — **1%, 2%, 5%, 10%, 20%, 50%, 100%**, one step per day, pausable at any point. The curve is fixed rather than freely chosen on purpose: nobody picks a good percentage at three in the morning with the error graph climbing. ### `sollar rollback` ```sh sollar rollback --to 2.4.0 ``` Republishes the 2.4.0 build as **2.4.2**. It is not a reversal — monotonic versions are what let a client know it needs to update, and lowering a version number destroys that guarantee. No review, because the code was already reviewed. Publishing never-reviewed code through this path is abuse, and the platform detects it by comparing hashes against approved versions. ### `sollar doctor` ```sh sollar doctor ``` Checks the Node version, CLI version, bridge type definitions, manifest validity, signing-key reachability, and connectivity to the test organisation. Run it first when something is wrong. ## Configuration Split in two, and this split matters: ```json title="sollar.config.json — committed" { "manifest": "./manifest.json", "root": "./src", "out": "./dist", "build": { "target": "es2022", "sbom": true } } ``` ```json title=".sollar/local.json — gitignored" { "tenant": "acme-corp-test", "device": "iphone-15-pro", "signing_key_ref": "op://Personal/sollar-signing/private-key" } ``` Shared build configuration is versioned; per-machine preference is not. WeChat's `project.config.json` mixes the two, and the result is a file that produces a diff every time a different developer opens the project. `.sollar/local.json` holds only *references* to secrets, never secrets. ## Continuous integration ```yaml title=".github/workflows/release.yml" - run: npm ci - run: npx sollar validate --strict - run: npx sollar build --sbom - run: npx sollar sign --keyless - run: npx sollar submit --tenant acme-corp ``` No GUI, no desktop IDE, no key in a repository variable. ============================================================================== # Errors Source: https://miniapp.sollar.com/reference/errors/ ============================================================================== Every rejected bridge call throws a `SollarError`. ```ts interface SollarError extends Error { code: SollarErrorCode message: string // human-readable, safe to log, never contains a token retriable: boolean } ``` Branch on `code`. Never parse `message` — it is written for humans and it is translated. ```js try { await sollar.files.pick({ accept: ['application/pdf'] }) } catch (e) { switch (e.code) { case 'ERR_PERMISSION_DENIED': showManualUploadInstead() break case 'ERR_USER_CANCELLED': break // not an error; the user changed their mind default: if (e.retriable) scheduleRetry() else report(e) } } ``` ## Permission and scope | Code | Retriable | Cause | |---|---|---| | `ERR_PERMISSION_DENIED` | no | The user declined, or the tenant policy forbids it. Degrade; do not loop. | | `ERR_SCOPE_NOT_DECLARED` | no | The scope is not in the manifest. A build-time mistake, not a runtime condition. | | `ERR_TENANT_POLICY` | no | The administrator has disabled this capability organisation-wide. | | `ERR_PERMISSION_REVOKED` | no | Granted earlier, revoked since. Re-request at the point of use. | ## Authentication | Code | Retriable | Cause | |---|---|---| | `ERR_NOT_AUTHENTICATED` | no | No session. Call `sollar.auth.signIn()`. | | `ERR_TOKEN_EXCHANGE_FAILED` | yes | The identity provider refused or was unreachable. | | `ERR_AUDIENCE_MISMATCH` | no | `manifest.auth.audience` does not match a registered backend. | | `ERR_SIGNIN_CANCELLED` | no | The user closed the authentication sheet. | ## Network | Code | Retriable | Cause | |---|---|---| | `ERR_HOST_NOT_ALLOWED` | no | The host is not in `network.connect`. Requires a new version. | | `ERR_OFFLINE` | yes | No connectivity. | | `ERR_TIMEOUT` | yes | | | `ERR_QUOTA_EXCEEDED` | yes | A rate limit on this bridge method. Back off. | `ERR_HOST_NOT_ALLOWED` is the one developers hit most in their first week. It is not a bug: the allowlist is enforced natively so that a compromised page cannot reach a new destination. Add the host to the manifest and rebuild. ## Rooms and messages | Code | Retriable | Cause | |---|---|---| | `ERR_NO_ROOM_CONTEXT` | no | Launched from the home grid, not from a room. `sollar.room.current()` returns `null` rather than throwing. | | `ERR_ROOM_ENCRYPTED` | no | A server-side path attempted to post into an E2EE room. Structural — see [Bridge API](/reference/bridge-api/). | | `ERR_NOT_A_MEMBER` | no | The user is not in that room. | | `ERR_MESSAGE_REJECTED` | no | Content policy, or the user declined the send confirmation. | ## Files and storage | Code | Retriable | Cause | |---|---|---| | `ERR_USER_CANCELLED` | no | The picker was dismissed. Expected, not exceptional. | | `ERR_FILE_TOO_LARGE` | no | | | `ERR_UNSUPPORTED_TYPE` | no | | | `ERR_STORAGE_FULL` | no | `sollar.storage` quota is 256 KB. Use IndexedDB for more. | ## Actions | Code | Retriable | Cause | |---|---|---| | `ERR_ACTION_NOT_FOUND` | no | Not declared in the manifest, or no handler registered. | | `ERR_INPUT_INVALID` | no | Input failed `input_schema`. The message names the failing path. | | `ERR_OUTPUT_INVALID` | no | Your handler returned something `output_schema` rejects. | | `ERR_CONFIRMATION_REQUIRED` | no | `confirm` policy demanded a human and none was available. | | `ERR_CONFIRMATION_DENIED` | no | The human declined. | | `ERR_ELICITATION_UNAVAILABLE` | no | No interactive surface — a background or scheduled invocation. | ## Platform | Code | Retriable | Cause | |---|---|---| | `ERR_NOT_IMPLEMENTED` | no | A `Planned` method on this client version. Guard with `sollar.supports()`. | | `ERR_VERSION_UNSUPPORTED` | no | The mini app requires a newer bridge than this client has. | | `ERR_INTERNAL` | yes | A host-side fault. Report it with the message. | ## Writing errors your users can act on The same rule applies to the errors *you* return from action handlers, and it matters more there, because an AI agent reads them and decides what to do next. ```js // Teaches nothing. The agent will retry the same call. throw new Error('ERR_CONFLICT') ``` ```js // Actionable. The agent stops, and can explain what happened. throw new Error( 'Purchase order 4471 was already approved on 2026-08-30 by another approver. ' + 'No further approval is needed.' ) ``` An opaque code makes an agent guess. A sentence makes it stop. ============================================================================== # Manifest Source: https://miniapp.sollar.com/reference/manifest/ ============================================================================== `manifest.json` sits at the root of the package and is covered by the signature. It is the only place a mini app can declare what it is allowed to do — there is no runtime path that widens it. The manifest is a **dual artifact**. The same file that tells the runtime which permissions to prompt for is what an AI agent reads as a tool registry. One declaration, two readers. ## A complete example ```json title="manifest.json" { "manifest_version": 1, "id": "com.acme.erp", "name": "Acme ERP", "short_name": "Acme", "version": "2.4.1", "description": "Purchase orders, approvals and expense reports for Acme staff.", "icons": [ { "src": "assets/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "assets/icon-512.png", "sizes": "512x512", "type": "image/png" } ], "vendor": { "id": "acme-corp", "name": "Acme Corporation", "contact": "miniapps@acme.example", "privacy_policy": "https://acme.example/privacy" }, "distribution": { "channel": "tenant-private", "tenants": ["acme-corp"] }, "entry": "index.html", "pages": [ { "path": "/", "title": "Approvals" }, { "path": "/orders/:order_id", "title": "Purchase order" }, { "path": "/expenses", "title": "Expenses" } ], "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." } ], "network": { "connect": ["erp.acme.example", "*.cdn.acme.example"] }, "auth": { "mode": "sollar-exchange", "audience": "acme-erp-backend", "scopes": ["erp.read", "erp.approve"] }, "actions": [ { "name": "list_pending_approvals", "title": "List pending approvals", "description": "Returns the purchase orders awaiting approval by the current user. Never returns another user's queue.", "input_schema": { "type": "object", "properties": { "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } } }, "output_schema": { "type": "object", "properties": { "orders": { "type": "array", "items": { "$ref": "#/$defs/order" } } } }, "annotations": { "read_only": true, "destructive": false, "idempotent": true, "open_world": false }, "confirm": "never" }, { "name": "approve_purchase_order", "title": "Approve a purchase order", "description": "Approves one purchase order on behalf of the current user. This is final: an approved order is released to the vendor and cannot be revoked from within this mini app.", "input_schema": { "type": "object", "properties": { "purchase_order_id": { "type": "string" }, "note": { "type": "string", "maxLength": 500 } }, "required": ["purchase_order_id"] }, "annotations": { "read_only": false, "destructive": true, "idempotent": true, "open_world": false }, "confirm": "always" } ], "capabilities": { "offline": true, "background_refresh": false }, "content_rating": { "system": "sollar", "value": "general" }, "locales": ["en", "pt-BR", "zh-Hans"] } ``` ## Identity | Field | Type | Required | Notes | |---|---|---|---| | `manifest_version` | integer | yes | `1`. A future version bumps this; the runtime refuses unknown values. | | `id` | string | yes | Reverse-DNS, immutable for the life of the app. Also derived from the signing key — see [Package format](/reference/package-format/). | | `name` | string | yes | 1–48 characters. | | `short_name` | string | no | ≤ 12 characters, for the home grid. Falls back to `name`. | | `version` | string | yes | Semantic version. Must increase monotonically; the store rejects a re-used or lowered version. | | `description` | string | yes | 1–300 characters. Shown in the store and in the install prompt. | | `icons` | array | yes | At least `192x192` and `512x512` PNG, packaged, not remote. | | `locales` | array | no | BCP 47 tags. First entry is the default. | ## `vendor` | Field | Required | Notes | |---|---|---| | `id` | yes | Stable vendor identifier. Two mini apps sharing a `vendor.id` may share a `vendor_id` identity — with explicit user consent only. | | `name` | yes | Legal or trading name, shown on the install prompt. | | `contact` | yes | A monitored address. Guideline 4.7.1 requires timely response to reports; this is where they go. | | `privacy_policy` | conditional | Required if any permission is declared. | ## `distribution` Exactly one channel. Declaring both is a validation error, not a warning — channels with different review rules cannot share an artifact. ```json "distribution": { "channel": "tenant-private", "tenants": ["acme-corp", "acme-eu"] } ``` ```json "distribution": { "channel": "store" } ``` | `channel` | Requires | Sollar review | |---|---|---| | `tenant-private` | `tenants` — a non-empty list of tenant slugs | None | | `store` | Verified entity on the developer account | Required | ## `pages` Declares the routes the mini app answers. Used to render deep links, to populate the store listing, and to let the host restore a user to where they were. ```json { "path": "/orders/:order_id", "title": "Purchase order" } ``` Path parameters use `:name`. A route not declared here still renders — routing is yours — but it cannot be deep-linked to and does not appear in the index required by Apple's Guideline 4.7.4. ## `permissions` Every entry needs a `purpose` string. This is not decoration: it is what the user reads in the prompt, and it is what a reviewer holds you to. ```json { "scope": "camera", "purpose": "Photographs a paper receipt so you do not have to type it in." } ``` A `purpose` that does not plausibly match the app's declared function is a rejection reason. "For app functionality" is not a purpose. | Scope | Grants | |---|---| | `identity.basic` | Display name and avatar of the current user | | `identity.email` | The user's email address within the tenant | | `room.context` | Which room the mini app was opened from | | `room.send` | Sending a message as the user, after per-message confirmation | | `files.read` | Opening a file the user picks from Sollar storage | | `files.write` | Writing a file into Sollar storage, at the user's direction | | `notifications` | Posting a notification through the host | | `camera` | Web `getUserMedia` video — the engine still prompts | | `microphone` | Web `getUserMedia` audio — the engine still prompts | | `geolocation` | Web Geolocation — the engine still prompts | | `clipboard.write` | Writing to the clipboard | | `contacts.directory` | Searching the tenant's user directory (not the device address book) | > **NOTE** Permission granted to the Sollar app does **not** flow to a mini app. Apple's Guideline 4.7.3 requires this, and Sollar enforces it whether or not the app is on iOS. Each mini app asks for its own, and the user can revoke it at any time. ## `network` An allowlist of hosts the mini app may reach. Enforced natively, in the request interceptor — not by Content Security Policy, which a page can rewrite at runtime. ```json "network": { "connect": ["erp.acme.example", "*.cdn.acme.example"] } ``` - Hostnames only. Scheme is always `https`. **Bare IP addresses are rejected.** - One leading wildcard label is allowed (`*.cdn.acme.example`); `*` alone is not. - Changing this list requires a new version and a new gate. That is the point. ## `auth` One of three modes. ```json "auth": { "mode": "sollar-exchange", "audience": "acme-erp-backend", "scopes": ["erp.read"] } ``` | `mode` | For | Additional fields | |---|---|---| | `sollar-exchange` | Backends that trust Sollar as the identity provider | `audience` (required), `scopes` | | `external-oauth` | Systems with their own IdP | `issuer`, `client_id`, `scopes` | | `none` | Mini apps with no backend identity | — | > **DANGER** `external-oauth` opens `ASWebAuthenticationSession` on iOS or Custom Tabs on Android — never the runtime's WebView. RFC 8252 §8.12 forbids embedded user-agents in native-app OAuth flows, and the reason is direct: inside a WebView the user cannot verify the URL or the certificate, and the host is technically able to read the password. There is no configuration that changes this. ## `actions` The agent tool registry. Each entry is simultaneously a tool an AI agent may call and an affordance the host can surface to a human. | Field | Required | Notes | |---|---|---| | `name` | yes | `snake_case`. Exposed to agents as `__`. | | `title` | yes | Human-readable, shown in the confirmation prompt. | | `description` | yes | Written for the agent. See [Agent actions](/guides/agent-actions/) for what makes one work. | | `input_schema` | yes | JSON Schema. Validated before your handler runs. | | `output_schema` | no | JSON Schema. Validated before the result reaches the agent. | | `annotations` | yes | All four booleans, explicitly. | | `confirm` | yes | `never`, `if_destructive` or `always`. | ### `annotations` All four are required. Omitting one is a validation error — the point is a deliberate answer, not a default. | Annotation | Question it answers | |---|---| | `read_only` | Does this change any state? | | `destructive` | Is the change hard or impossible to undo? | | `idempotent` | Does calling it twice with the same input differ from calling it once? | | `open_world` | Does it reach systems outside this mini app's control? | These are declarations, not guarantees. Sollar cannot verify that an action marked `read_only` really is. What it can do — and MCP cannot — is make the declaration part of a signed, reviewed artifact that is attributable to a key and cannot be silently changed after a user has trusted it. ### `confirm` | Value | Behaviour | |---|---| | `never` | Runs without asking. Only acceptable with `read_only: true`. | | `if_destructive` | Resolves to `always` when `annotations.destructive` is true. | | `always` | Always asks a human first. | The manifest sets the **floor**. A tenant policy or a user preference can tighten it; nothing can loosen it. ## `capabilities` | Field | Default | Notes | |---|---|---| | `offline` | `false` | The app declares it functions without network. Enables offline launch. | | `background_refresh` | `false` | `Planned`. Reserved; currently rejected if `true`. | ## Validation ```sh sollar validate ``` Runs the same schema the submission gate runs. The canonical schema is published at [`manifest.schema.json`](/schemas/manifest.schema.json) (JSON Schema 2020-12) — point your editor at it and get completion and inline errors while you type. ============================================================================== # Package format Source: https://miniapp.sollar.com/reference/package-format/ ============================================================================== A mini app ships as a single `.sapp` file. It is a ZIP archive with a signature block inserted before the central directory, and it borrows its structure from formats that have already survived adversarial contact: Android's APK Signature Scheme, Chrome's CRX3, and fs-verity. ## Layout ``` ┌─────────────────────────────────────────────┐ │ ZIP entries │ │ manifest.json ← covered by sig │ │ index.html │ │ assets/… │ │ sbom.cdx.json │ ├─────────────────────────────────────────────┤ │ Sollar signature block │ │ • signer chain (lineage) │ │ • Ed25519 signature │ │ • ECDSA P-256 signature │ │ • Merkle root over content │ │ • countersignature (store or org CA) │ ├─────────────────────────────────────────────┤ │ ZIP central directory │ │ End of central directory │ └─────────────────────────────────────────────┘ ``` Placing the signature block *before* the central directory is the APK v2 arrangement. It means the signature covers the archive's bytes rather than its parsed entries, which closes the class of attack where a verifier and an extractor disagree about what the archive contains. ## What each borrowed idea buys | Problem | Solution | Precedent | |---|---|---| | Verifier and extractor parse differently | Sign the byte range, not the entry list | APK Signature Scheme v2 | | Signing key must be replaced without breaking installs | Signer lineage: the old key signs the new key's authority | APK Signature Scheme v3 | | Whole-file hash means reading the whole file before first paint | Merkle tree — verify each block as it is read | APK v4 / fs-verity | | One algorithm breaks and everything installed is unverifiable | Two independent signatures, both required | CRX3 | | An attacker publishes under someone else's identifier | The app `id` is derived from the public key | CRX3 | | A dependency turns out to be vulnerable and nobody knows who shipped it | CycloneDX SBOM inside the package | — | ## Signing Two signatures, both required, over the same content: - **Ed25519** — fast, small, no parameter choices to get wrong. - **ECDSA P-256** — broad hardware-backed keystore support on both platforms. A package that verifies under only one is refused. This is CRX3's reasoning: the cost of carrying a second algorithm is a few hundred bytes, and the benefit is that a break in either curve does not strand every installed app. ```sh sollar build # produces dist/com.acme.erp-2.4.1.sapp sollar sign --key # adds the signature block ``` > **DANGER** The signing key never goes in a file, a repository, a CI variable, or an environment file. Pass a reference to a secret manager or a hardware token; `sollar sign` resolves it at the moment of use and never writes it to disk. ### Keyless signing `sollar sign --keyless` uses Sigstore: an ephemeral key, an OIDC identity, and a transparency-log entry. Nothing to store, nothing to leak, and a public record of who signed what. Available for both channels; recommended for CI. ### Countersignature The developer signature proves authorship. It is not enough to install. | Channel | Countersigned by | |---|---| | `store` | The Sollar store key, after review | | `tenant-private` | The organisation's CA | The client refuses a package with no countersignature from one of the two. Tenant-private apps skip Sollar's *review*; they do not skip the *cryptography*. ## Key rotation The signer lineage is a chain: each new key is authorised by a signature from the previous one. A client that has installed version 2.4.1 signed by key A will accept 2.5.0 signed by key B, because B's authority is proved by A within the package. Losing a signing key with no lineage entry means the app cannot be updated by anyone, including you. Rotate deliberately, and keep the lineage. ## Updates Full and delta, both supported. - **Delta** — a bsdiff patch against a specific prior version. The client falls back to a full download when it holds a version with no patch path. - **Merkle verification** — the client verifies blocks as it reads them, so a mini app starts before the whole package is hashed. - **Monotonic versions** — the client accepts only a higher version than the one installed. This is what makes rollback work as a *new version*, not a reversal; see [Distribution](/platform/distribution/). ## Limits | | Limit | Note | |---|---|---| | Package size | 8 MB | The main package; enforced at submission | | Total with subpackages | 24 MB | Lazily loaded parts | | Single file | 4 MB | | | Manifest | 256 KB | | | `actions[]` | 64 per app | An agent given hundreds of tools chooses badly | | `network.connect` | 32 hosts | A list longer than this is not an allowlist | Size limits exist because a mini app that takes as long as an app-store download has lost the only advantage it had. ## SBOM `sbom.cdx.json`, CycloneDX 1.7, generated at build: ```sh sollar build --sbom ``` Required for the store channel, recommended for tenant-private. The submission gate scans it against known-vulnerability data, and a high-severity match in a shipped dependency blocks the release. The SBOM is what makes the question "which mini apps ship the vulnerable version of this library" answerable in minutes rather than by asking every developer. ## What is *not* in the package **No remote code.** The Content Security Policy floor is `script-src 'self'` with no remote host and no `'unsafe-eval'` — every executable byte arrives inside the verified package. This is a security property and a legal one. Google Play's Device and Network Abuse policy exempts from its downloaded-code prohibition anything running *"in a virtual machine or an interpreter where either provides indirect access to Android APIs (such as JavaScript in a webview or browser)"*. A mini app pulling a script from a CDN is downloading executable code from outside the verified package, which is exactly what the exemption does not cover. See [Google Play](/compliance/google-play/). A third-party resource that genuinely must be remote requires Subresource Integrity with a pinned hash. ============================================================================== # Mini app ↔ superapp API Source: https://miniapp.sollar.com/reference/api/ ============================================================================== This section defines the **integration API between mini apps and the Sollar superapp**. It is the contract: what a mini app may ask Sollar for, what Sollar may ask a mini app to do, and what crosses the boundary in each direction. > **NOTE** This is the API *of the mini app environment*. Whether Sollar's servers will offer a general integration API outside that environment is not decided, and nothing here should be read as committing to one. If you are looking for "the Sollar API" in the sense of a public server-to-server platform API, it does not exist yet. ## Three planes The contract has three surfaces, and they have different trust properties. Confusing them is the root of most integration mistakes. ``` ┌──────────────────────────────────────────┐ │ SOLLAR CLIENT │ │ ┌────────────────────────────────────┐ │ ① bridge ◀──────┼──│ mini app (WebView, own origin) │ │ in-process │ └───────────────┬────────────────────┘ │ │ │ │ │ ┌───────────────▼────────────────────┐ │ │ │ native runtime — permission gate, │ │ │ │ network gate, token exchange │ │ │ └───────────────┬────────────────────┘ │ └──────────────────┼───────────────────────┘ │ ② HTTPS, short-lived │ audience-scoped token ┌────────▼─────────┐ │ YOUR BACKEND │ └────────┬─────────┘ │ ③ signed server calls ┌────────▼─────────┐ │ SOLLAR SERVERS │ └──────────────────┘ ``` | Plane | Direction | Transport | Authenticated by | Trust | |---|---|---|---|---| | ① **Bridge** | mini app → runtime | In-process message channel | The signed manifest | The runtime trusts the manifest, never the page | | ② **Backend** | mini app → your server | HTTPS | RFC 8693 exchanged token | Your server trusts the token, never the client | | ③ **Server** | your server ↔ Sollar | HTTPS | HMAC-SHA256 request signature | Mutual, over a shared signing secret | **Plane ① is not a security boundary you control.** The user's device runs the WebView. Anything your JavaScript receives, a determined user can also produce. The boundary that matters is ②, and it is enforced by your server checking a token. ## Plane ① — the bridge Documented in full at [Bridge API](/reference/bridge-api/). Two properties define it: **Web first.** Camera, microphone, geolocation, files, notifications and biometrics are standard Web APIs, not bridge methods. The bridge covers only the Sollar domain — identity, rooms, messages, Sollar storage, agent actions. **The manifest is the ceiling.** Every permission, every reachable host and every agent action is declared in a signed file. There is no runtime negotiation that widens any of them. ## Plane ② — mini app to your backend Your mini app calls your own API over `fetch`, restricted to the hosts in `network.connect`, with a token obtained from the bridge. ```js const { access_token } = await sollar.auth.getToken() const res = await fetch('https://erp.acme.example/api/approvals', { headers: { Authorization: `Bearer ${access_token}` } }) ``` The token is issued by Keycloak through an RFC 8693 token exchange performed by the **native runtime**, not by the page. It is short-lived and its `aud` is your backend and nothing else — if it leaks, another mini app's backend refuses it. Your backend verifies signature, `aud`, `iss` and `exp` before doing anything: ```js const jwks = createRemoteJWKSet( new URL('https://auth.sollar.com/realms/acme-corp/protocol/openid-connect/certs')) const { payload } = await jwtVerify(token, jwks, { issuer: 'https://auth.sollar.com/realms/acme-corp', audience: 'acme-erp-backend', }) const appUserId = payload.sub // the identity you may act on const actor = payload.act?.sub // present when an AI agent is acting ``` > **DANGER** `payload.sub` is the only user identity you may authorise against. The `app_user_id` your JavaScript received from `sollar.identity.get()` is a display convenience, and treating it as an authorisation input is the defining vulnerability of this platform category. Full detail in [Authentication](/guides/authentication/). ## Plane ③ — your backend to Sollar When your backend calls Sollar — to place a card in a room, to update a badge — the request is signed. The recipe is Slack's, which is the best-documented of its kind: ``` base = "v0:" + timestamp + ":" + raw_body signature = "v0=" + hex(HMAC_SHA256(signing_secret, base)) headers X-Sollar-Signature, X-Sollar-Request-Timestamp ``` Verify with a constant-time comparison, and reject a timestamp outside a short window to block replay. HMAC-SHA256, not SHA-1 — the scheme works with SHA-1, but it is not a defensible choice for a new design. The same signature protects webhooks Sollar sends to you. Verify them the same way, before parsing the body. > **CAUTION** **Plane ③ cannot post into an end-to-end encrypted room.** Your server is not a cryptographic member and Sollar's servers do not hold the keys. In the Enterprise and Sovereign tiers, encryption is on by default, so this is the normal case. Use `sollar.message.send()` from the user's client, or an AI agent that is a cross-signed member of the room. Design for this before you build the notification feature, not after. ## Identity across the planes One human, four identifiers, deliberately: | Identifier | Scope | Who sees it | |---|---|---| | `sollar_id` | Global, immutable | **Nobody.** Never leaves Sollar infrastructure. | | `user_id` | Unique within a tenant | Tenant administrators, administrative APIs | | `app_user_id` | Unique per (user, mini app) | Your mini app and your backend | | `vendor_id` | Unique per (user, developer) | Optional, with explicit consent | `app_user_id = HMAC(tenant_key, sollar_id ‖ mini_app_id)`. Two mini apps serving the same person get different identifiers that cannot be correlated. A breach of your database does not expose the underlying Sollar identity, and the tenant can rotate its key to break every correlation at once. `vendor_id` exists for the legitimate case where one company runs several mini apps and wants to recognise the same user across them. It requires explicit consent and is not the default. ## Rate limits | Plane | Limit | |---|---| | Bridge — read methods | 120/minute per mini app instance | | Bridge — `message.send` | 10/minute, each with a confirmation | | Bridge — `actions.elicit` | 5 per action invocation | | Server → Sollar | 600/minute per vendor, burst 60 | Exceeding a bridge limit throws `ERR_QUOTA_EXCEEDED` with `retriable: true`. Back off; do not spin. ## Versioning and compatibility - The bridge carries its own version. Read it with `sollar.version`; test for a method with `sollar.supports()`. - Server APIs are versioned in the path: `/v1/…`. - Removing anything requires a major version and a published deprecation window. - A raised [compatibility baseline](/reference/baseline/) never breaks an installed mini app. ## Error model The bridge throws `SollarError` with a stable `code` and a `retriable` flag — see [Errors](/reference/errors/). Server planes use RFC 9457 problem details: ```json { "type": "https://miniapp.sollar.com/errors/room-encrypted", "title": "Room is end-to-end encrypted", "status": 409, "detail": "Server-side posting is not possible in an encrypted room. Send from the user's client with sollar.message.send(), or use an agent that is a member of the room.", "instance": "/v1/rooms/!abc:sollar.com/messages" } ``` `detail` is written to be actionable — for a human reading a log, and for an AI agent deciding whether to try something else. A bare error code teaches neither. ============================================================================== # Server API Source: https://miniapp.sollar.com/reference/api/server/ ============================================================================== Plane ③ of the [integration contract](/reference/api/): your backend talking to Sollar's servers. Base URL: `https://api.sollar.com/v1` Everything here is `Planned` — specified, not yet running. Build against it; expect the shapes to hold and the host to be confirmed at launch. ## Authentication Every request is signed with HMAC-SHA256 over the raw body. ``` base = "v0:" + timestamp + ":" + raw_body signature = "v0=" + hex(HMAC_SHA256(signing_secret, base)) ``` ```http POST /v1/rooms/!abc:sollar.com/cards HTTP/1.1 Host: api.sollar.com Content-Type: application/json X-Sollar-Request-Timestamp: 1788278400 X-Sollar-Signature: v0=8f2c... X-Sollar-Mini-App: com.acme.erp ``` ```js function sign(secret, timestamp, rawBody) { const base = `v0:${timestamp}:${rawBody}` return 'v0=' + createHmac('sha256', secret).update(base).digest('hex') } function verify(secret, timestamp, rawBody, received) { if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false // replay window const expected = Buffer.from(sign(secret, timestamp, rawBody)) const actual = Buffer.from(received) return expected.length === actual.length && timingSafeEqual(expected, actual) } ``` Three details that are not optional: 1. **Sign the raw body**, before any JSON parsing. A re-serialised body produces a different signature and you will chase it for a day. 2. **Constant-time comparison.** `===` on a signature leaks its bytes through timing. 3. **A short timestamp window** — five minutes. Without it a captured request replays forever. > **DANGER** The signing secret lives in a secret manager, never in a file, a repository, an environment file, or a CI variable. Rotate it from the developer console; both the old and new secret verify during a one-hour overlap so rotation needs no downtime. ## Endpoints ### Post a card into a room ```http POST /v1/rooms/{room_id}/cards ``` ```json { "mini_app_id": "com.acme.erp", "app_user_id": "u_9f3a…", "card": { "title": "Purchase order 4471 approved", "body": "€12,400 · Vendor: Nordwerk GmbH", "actions": [ { "label": "Open", "deep_link": "/orders/4471" } ] } } ``` `201` with `{ "event_id": "…" }`. `409` with `type: room-encrypted` when the room is end-to-end encrypted — see below. ### Send a notification ```http POST /v1/users/{app_user_id}/notifications ``` ```json { "mini_app_id": "com.acme.erp", "title": "Approval needed", "body": "Purchase order 4472 is waiting for you.", "deep_link": "/orders/4472" } ``` Requires the `notifications` scope, granted by that user. A notification to a user who has not granted it returns `403`, not silence. ### Read the mini app's own installation state ```http GET /v1/apps/{mini_app_id}/installations/{app_user_id} ``` ```json { "installed": true, "version": "2.4.1", "granted_scopes": ["identity.basic", "camera"], "tenant_id": "acme-corp" } ``` Useful for deciding whether a notification will land before sending it. ### Resolve an agent delegation ```http POST /v1/agents/verify ``` Given a token carrying an `act` claim, returns the agent's identity and the tenant policy that applies to it — so your backend can enforce a lower limit for agent-initiated actions than for human ones. ```json { "subject": { "app_user_id": "u_9f3a…" }, "actor": { "agent_id": "agent_admin_assistant", "kind": "sollar_agent" }, "tenant_policy": { "max_approval_minor": 500000, "currency_code": "EUR" } } ``` ## Webhooks Sollar calls your backend for events you subscribe to in the developer console. The signature scheme is identical, and the same three rules apply. | Event | Sent when | |---|---| | `installation.created` | A user installs the mini app | | `installation.deleted` | Uninstalled, or removed by an administrator | | `scope.granted` / `scope.revoked` | A permission changes | | `user.deprovisioned` | The user leaves the tenant — **delete their data** | | `app.revoked` | Sollar has revoked a version. Stop serving it. | ```json { "event": "user.deprovisioned", "occurred_at": "2026-09-01T09:14:22Z", "mini_app_id": "com.acme.erp", "app_user_id": "u_9f3a…", "tenant_id": "acme-corp" } ``` Respond `2xx` within 5 seconds. Sollar retries with exponential backoff for 24 hours, then stops and raises an alert in your developer console. Make handlers idempotent — a retry after a timeout on a request you *did* process is normal, not exceptional. `user.deprovisioned` is the one with a legal deadline attached. Treat it as a deletion instruction, not a notification. ## Encrypted rooms > **CAUTION** **Your backend cannot post into an end-to-end encrypted room, and no endpoint here changes that.** Sollar's servers do not hold the room keys. Your backend is not a cryptographic member of the room. This is the property that makes the encryption meaningful, so there is no server-side path around it — not a special scope, not an enterprise tier, not a support request. In the Enterprise and Sovereign tiers, encryption is on by default. Assume the room is encrypted. Two paths that work: 1. **`sollar.message.send()`** — the message leaves from the user's own client, under their identity. The user confirms it. 2. **An AI agent that is a cross-signed member of the room** posts under its own identity. It sees the room's content exactly as an invited human member would — visible and auditable, not confidential during processing. Design the notification path before building the feature. ## Errors RFC 9457 problem details: ```json { "type": "https://miniapp.sollar.com/errors/scope-not-granted", "title": "Scope not granted", "status": 403, "detail": "The user has not granted 'notifications' to com.acme.erp. Request it in the mini app at the point of use; it cannot be granted from the server.", "instance": "/v1/users/u_9f3a…/notifications" } ``` | Status | Meaning | |---|---| | `400` | Malformed request; `detail` names the field | | `401` | Signature invalid, missing, or outside the timestamp window | | `403` | Scope not granted, or tenant policy forbids it | | `404` | Unknown room, user or mini app | | `409` | State conflict — encrypted room, version revoked | | `429` | Rate limited; honour `Retry-After` | | `503` | Retriable | `detail` is always a sentence that says what to do. That is a deliberate contract: your logs are read by humans under time pressure and increasingly by agents deciding what to try next, and neither learns anything from a bare code. ## Rate limits 600 requests/minute per vendor, burst 60. `429` carries `Retry-After` in seconds. Sustained overrun contacts you before it throttles you. ============================================================================== # Platform Source: https://miniapp.sollar.com/platform/ ============================================================================== This section explains **why** the platform is shaped the way it is. It is background, not instructions; nothing here is needed to ship a mini app, and all of it is needed to argue with the design. | Page | | |---|---| | [Architecture](/platform/architecture/) | The runtime, and why it is a hardened WebView | | [Security model](/platform/security-model/) | The threat model, and the six failures it is built against | | [Identity](/platform/identity/) | Four layers of identifier, and why correlation is prevented | | [Review policy](/platform/review-policy/) | What Sollar checks, and what it refuses | | [Distribution](/platform/distribution/) | Two gates, version states, rollout and revocation | ## The shape of the decision Sollar's core is deliberately narrow: messaging, calls, translation, groups, AI agents. Everything that involves communication is in; everything else is out. There are no connectors, no MCP servers, no plugins, and no skills in the core. That is not a limitation to be worked around later. It is the premise. A messenger that accepts arbitrary third-party extensions into its core has, in practice, accepted arbitrary third-party code into the process that holds every conversation its users have. **Mini apps are what make Sollar a superapp** — and they are the *only* extension surface. A mini app is a signed, reviewed, sandboxed web package whose capabilities are enumerated in a manifest that a human approved and a key attested. That is a very different security proposition from a plugin. It also resolves what would otherwise be a contradiction. AI agents are a core pillar, and an agent that cannot act is a chatbot. So agents need capabilities — but opening the core to give them capabilities would undo the premise. The resolution is that the mini app's `actions[]` **is** the agent tool registry. One installation, one permission model, one review, two consumers. ## What this costs Being honest about it, since the rest of this section argues for the design: - **No payment API.** A mini app cannot take money in-app. See [Store compliance](/compliance/). - **No embedded browser component.** External content opens in the system browser. - **No native API surface beyond Sollar's own domain.** Camera, location and files are Web APIs, and what they cannot do, a mini app cannot do. - **Every new native capability is an Apple approval, not a sprint.** Guideline 4.7.2 makes exposing a platform API to third-party software a matter requiring Apple's prior permission. - **Nothing here has been through an independent security audit.** The platform is specified, not built. Treat the claims in this section as a design intent with evidence, not as a result. ============================================================================== # Architecture Source: https://miniapp.sollar.com/platform/architecture/ ============================================================================== A mini app runs in a **hardened WebView, one per app, at its own synthetic origin**. That sentence contains the two decisions that shape everything else. ## Why not a two-thread runtime WeChat's runtime splits a mini program into a render layer in a WebView and a logic layer in a separate JavaScript engine with no DOM, relayed by the native client. It is a well-understood design and Sollar does not use it, on three grounds. ### 1. Apple requires WebKit, worldwide Guideline 2.5.6: apps that browse the web *"must use the appropriate WebKit framework and WebKit JavaScript."* The Embedded Browser Engine Entitlement that would permit an alternative exists **only in the European Union**, and qualifying requires 90% on the Web Platform Tests, 80% on Test262, a memory-safe implementation language, and a 30-day CVE remediation commitment. That is out of reach, and it would not cover the rest of the world in any case. ### 2. Performance argues the same way, which is counter-intuitive WKWebView's WebContent process **has JIT**. A JavaScript engine embedded in the app's own process **does not** — Apple does not grant the entitlement for writable-executable memory to ordinary apps. So a custom two-thread runtime with its own JavaScriptCore instance would be *slower* on iOS than the WebView it was meant to improve on. This is the same ceiling WeChat's own logic layer runs into, and it is not a small effect. ### 3. Lark already tried it and reversed Lark's two-thread "gadget" runtime is deprecated in favour of Web Apps over WebView. When the company with the closest requirements and full control of both clients abandons the approach, that is evidence rather than opinion. ### What about Android? Android reaches the same architecture by a different and thinner legal path. Google Play's *Device and Network Abuse* policy exempts from its downloaded-code prohibition anything that runs *"in a virtual machine or an interpreter where either provides indirect access to Android APIs (such as JavaScript in a webview or browser)."* Note the asymmetry, because it is the reverse of what most people assume: **Apple has a dedicated policy that names mini apps and permits them** (Guideline 4.7). **Google does not.** Android depends on a generic interpreter exemption. That constrains the design — every execution path on Android must end in interpreted JavaScript with indirect, mediated access through the Sollar bridge, never a native code loader. ## Isolation comes from the origin Each mini app is served from a synthetic origin of its own: | Platform | Mechanism | Origin | |---|---|---| | Android | `WebViewAssetLoader` + `shouldInterceptRequest` | `https://.miniapp.localhost/` | | iOS | `WKURLSchemeHandler` | A per-app scheme, treated as a distinct origin | From that one decision, the browser engine gives: - **Storage partitioning** — `localStorage`, IndexedDB, the Cache API and cookies are separated by origin. Not by a directory convention the runtime has to remember; by the same-origin policy. - **CSP scope** — a policy applies to one mini app and cannot leak to another. - **A bridge boundary** — `allowedOriginRules` on Android binds the message channel to exactly one origin. This is the difference between a rule the runtime remembers to enforce and a rule it cannot violate. Cache reuse between mini apps is the first of the six vulnerability categories in [the security model](/platform/security-model/), and it exists in other platforms precisely because they chose shared directories with runtime-enforced paths. > **CAUTION** On iOS, whether a custom scheme served through `WKURLSchemeHandler` counts as a **secure context** is verified per release and tracked as an open risk. If it fails, `crypto.subtle` and service workers become unavailable inside mini apps and the encrypted-storage design changes. This is stated here rather than assumed away. `WKWebsiteDataStore(forIdentifier:)` gives an additional per-mini-app store on iOS, and `nonPersistent()` gives an ephemeral mode. On Android, `setDataDirectorySuffix` looks like the equivalent but is not usable for this: it is process-wide and settable once, so it cannot separate apps within a running client. Origin separation solves what it cannot. ## The bridge | Platform | Mechanism | Never | |---|---|---| | Android | `WebViewCompat.addWebMessageListener` with `allowedOriginRules` | `addJavascriptInterface` | | iOS | `WKScriptMessageHandlerWithReply` in a dedicated `WKContentWorld` | — | `addJavascriptInterface` has no origin control and is the vector of CVE-2012-6636, where attacker-controlled JavaScript reached arbitrary Java methods by reflection. The usual mitigation — "only affects `targetSdk` ≤ 16" — is incomplete, and AOSP's own documentation says so: apps targeting later versions remain vulnerable when running on Android before 4.2. Sollar's `minSdk` makes that historical, but the operational lesson stands: one API has origin control and the other does not. `allowedOriginRules` matches scheme, host and port. **The path is ignored.** A design that assumes the bridge can be restricted to a subdirectory is a design built on a misreading. > **NOTE** **`WKContentWorld` is not a security boundary.** It isolates JavaScript variables; it does **not** isolate the DOM, and DOM mutations are visible across every world. Apple documents it as a solution to script and namespace conflicts, not as a capability restriction. It is the right tool for stopping a mini app from overwriting the bridge function. It is the wrong tool for hiding data from one. ## Where permission decisions happen **In native code, against the signed manifest. Never in JavaScript.** ``` mini app JS → [sollar.* shim] → native bridge → ┌────────────────────────┐ │ 1. manifest declares │ │ this scope? │ │ 2. user granted? │ │ 3. tenant allows? │ │ 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 — a mini app controls its own JS environment by definition. ## Lifecycle ``` install ──▶ launch ──▶ foreground ⇄ background ──▶ suspended ──▶ destroyed │ │ └──── agent invocation (headless) ─────────┘ ``` - **Launch** — package verified against its Merkle root, manifest parsed, origin established, bridge installed, entry page loaded. - **Background** — timers throttled, network paused. The instance is retained for a short window and then suspended. - **Suspended** — the renderer may be discarded to reclaim memory; `setRendererPriorityPolicy` on Android makes the mini app's renderer sacrificable before the host process. Returning re-launches and restores route state. - **Headless** — an agent invocation can start or resume an instance without showing it. `ctx` tells your handler which case it is in. Nothing survives destruction except origin storage and `sollar.storage`. Treat in-memory state as disposable. ## Network Every request passes a **native interceptor** that checks the host against `manifest.network.connect` before it leaves the device. CSP is a second layer, not the primary one — a page can rewrite its own CSP at runtime, but it cannot reach the native interceptor. This ordering is what stops the third vulnerability category, silent exfiltration of sensitive data to an undeclared host. Changing the destination list requires a new version, a new signature, and a new gate. That is the point of it. ## Quotas A mini app cannot degrade Sollar. Per instance: a ceiling on concurrent connections, on storage, and on memory — with the instance destroyed rather than the host running out — plus a rate limit per bridge method. ## What was considered and rejected | Option | Why not | |---|---| | A two-thread runtime with an embedded JS engine | 2.5.6, plus the JIT asymmetry above | | FinClip and similar container SDKs | A third-party runtime inside the client's trust boundary; opaque to review | | Quick App | Vendor alliance runtime, Android-only, no iOS story | | kbone / Taro compile-to-mini-program | Solves cross-compiling to WXML, a problem Sollar does not have | | React Native or Flutter per mini app | Native code from third parties, downloaded — the exact thing both stores prohibit | Taro deserves a note because Sollar's own earlier stack study chose it. It compiles React or Vue into WeChat's WXML/WXSS dialect. If the runtime is the web platform, there is no dialect and no problem to solve. The Chinese cross-compile ecosystem is also contracting: Remax archived 2024-03-07, WePY 2026-03-18, Chameleon 2026-04-15. Taro returns to the table only if Sollar ever wants to *import* WeChat mini programs, which is a different goal and should be decided as one. ============================================================================== # Distribution Source: https://miniapp.sollar.com/platform/distribution/ ============================================================================== ## Two gates | | **Tenant-private** | **Store** | |---|---|---| | Who publishes | The company, or its AI tooling | A third-party developer | | Sollar review | **None** | Required | | Verified entity | Already a contracted customer | Required | | Visibility | The organisation only | Public catalogue | | Countersigned by | The organisation's CA | The Sollar store key | | Installation | Administrator provisions, or staff install from the internal catalogue | User installs from the public catalogue | | Price | Outside the app | Outside the app — see [Store compliance](/compliance/) | **A package cannot be both.** `distribution.channel` is exclusive, and declaring both is a validation error. Managed Google Play enforces the same exclusivity for the same reason: channels with different rules cannot share an artifact. Precedents: Lark separates self-built from store apps exactly this way. DingTalk does the same for enterprise-internal applications. And Firefox demonstrated that "unlisted but still platform-signed" is a workable model — you lose curation, never the signature chain. ## Four version states Coexisting, following WeChat's model, which is the best-resolved of the ones surveyed: ``` development ──▶ trial ──▶ in review ──▶ published (latest per (named (ONE at (what users developer) testers) a time) are running) ``` **One review slot.** Only one version can be in review at a time; resubmitting **replaces** rather than queues. This avoids a backlog of superseded versions nobody wants any more. **One development version per developer.** Each team member has their own, and a new upload replaces it. It is a working state, not a history. **Named testers.** Only project members and explicitly listed testers reach a trial version, and testers must be members of the organisation — the tenant is the natural boundary. ## Rollout Full, or phased. Phased follows Apple's fixed curve, the best-documented starting point available: **1%, 2%, 5%, 10%, 20%, 50%, 100%**, one step per day, pausable at any time. A fixed curve beats a freely chosen percentage because it removes the decision from the moment of stress. Nobody picks a good number at three in the morning with the error graph climbing. WeChat also offers phased publication but does not document the mechanics publicly — the steps, rollback limits, or emergency path. ## Rollback Fast, and as a **new version**, not a true reversal. This is the Chrome Web Store's model: about a minute, no review, republishing an earlier build under a higher version number. ```sh sollar rollback --to 2.4.0 # publishes 2.4.2 carrying 2.4.0's content ``` Why not a real reversal: the monotonic version is what tells a client it needs to update. Lowering the number breaks that guarantee and creates ambiguity about what is installed. **Rollback skips review** because the code was already reviewed. Publishing never-reviewed code through this path is abuse, and the platform detects it by comparing hashes against approved versions. ## Emergency revocation Distinct from rollback, and covered in [Review policy](/platform/review-policy/): a signed revocation list consulted at launch with a short TTL; the client refuses the revoked version and updates to a safe one if there is one; a fully revoked app leaves the catalogue and stops opening. ## Updates - **Delta updates** — a bsdiff patch against a specific prior version, with a full-download fallback when no patch path exists. - **Merkle verification** — blocks verified as they are read, so a mini app starts before the whole package is hashed. - **Monotonic versions** — the client accepts only a higher version than the one installed. ## The public index Apple's Guideline 4.7.4 requires: > *You must provide an index of software and metadata available in your app. It must include > universal links that lead to all of the software offered in your app.* [The catalogue at `/apps/`](/apps/) is that artifact, with a universal link per mini app (`https://sollar.com/app/`). **This means the public catalogue cannot be closed or invitation-only** if Sollar wants App Store approval. That is a constraint on the product, not a documentation detail. > **CAUTION** **Tenant-private mini apps are the point of tension.** The defensible reading is that a tenant-private app is not "software offered in your app" in the sense of the guideline — it is configuration provisioned by a customer's administrator, analogous to MDM. That reading is defensible, **not confirmed**. And alongside it sits a question our research could not close from primary sources: whether the App Review Guidelines apply equally to Custom Apps distributed through Apple Business Manager. That is precisely the question that decides the tenant-private route on iOS, and it is open. Two enterprise dead ends are closed with evidence, at least: the **Apple Developer Enterprise Program requires 100+ employees and is internal-use only**, so it cannot serve a vendor distributing to many customers; and **Managed Google Play private apps cannot charge anything** and cannot coexist with a public listing. ## Third-party service providers WeChat operates a service-provider platform that authorises an external company to administer many merchants' mini programs, through its own handshake (`component_verify_ticket` → `pre_auth_code` → authorisation), where the effective scope is the **intersection** of what the merchant granted and what the provider holds. It is relevant here because a consultancy building mini apps for several tenants is an obvious case. It is not in v1, but the permission model is designed knowing it is coming — `vendor_id` and `actor_token` delegation already have the right shape. ## The China perimeter Since September 2023, the MIIT's ICP filing (备案) applies to mobile apps and mini programs as well as websites, and Tencent enforces it inside its own developer portal. Reported service levels: platform review 1–2 days, SMS verification 24 hours, and the provincial telecommunications bureau 1–20 days. If Sollar operates a mini app store in mainland China, each mini app needs its own filing, and the provincial bureau's timeline sits on the developer's critical path. See [China](/compliance/china/). ============================================================================== # Identity Source: https://miniapp.sollar.com/platform/identity/ ============================================================================== An identity system for a mini app store has to answer a question that a single application never faces: **can two mini apps work out that they are serving the same person?** If the answer is yes, the store is a tracking network. Two apps compare identifiers and reconstruct a person's behaviour across unrelated services, and no privacy policy fixes that, because it is a property of the architecture. ## Four layers The model follows Lark's, which handles this most carefully. | Layer | Scope | Exposed to | |---|---|---| | `sollar_id` | Global, immutable, unique in Sollar | **Nobody.** It never leaves Sollar infrastructure. | | `user_id` | Unique within a tenant | Tenant administrators, administrative APIs | | `app_user_id` | Unique per (user, mini app) | The mini app and its backend | | `vendor_id` | Unique per (user, developer) | Optional, with explicit consent | ### `app_user_id` is the load-bearing one ``` app_user_id = HMAC(tenant_key, sollar_id ‖ mini_app_id) ``` Two mini apps serving the same human receive different, derived, non-correlatable identifiers. The tenant key lives in an HSM or KMS. Three consequences worth stating: - A breach of one mini app's database does not expose the underlying Sollar identity. - Two mini apps cannot join their data on a shared key, even if both want to. - A tenant can **rotate its key** and break every correlation at once — a real incident-response capability, not a theoretical one. ### `vendor_id` is the deliberate exception A company running several mini apps has a legitimate reason to recognise the same user across them — the case Lark and DingTalk serve with `unionid`. Sollar supports it, with two conditions: the mini apps must share a `vendor.id`, and the user must consent explicitly. It is not the default, and it is not silent. ## The host is the OAuth client The Sollar native client is the **confidential client**. A mini app is never an OAuth client, never holds a `client_secret`, and never sees a refresh token. This is the Backend-For-Frontend pattern that RFC 10017 (BCP 212, *OAuth 2.0 for Browser-Based Applications*) §6.1 recommends for browser applications — and a mini app is, technically, a browser application. ``` ┌───────────────────────────────────────────────────────┐ │ Sollar native client (confidential client) │ │ │ │ ┌──────────────┐ audience-scoped token │ │ │ mini app │ ◀─────────────────────────┐ │ │ │ (WebView) │ │ │ │ └──────────────┘ ┌──────┴───────┐ │ │ │ RFC 8693 │ │ │ │ exchange │ │ │ └──────┬───────┘ │ └─────────────────────────────────────────────┼─────────┘ │ ┌──────▼──────┐ │ Keycloak │ └─────────────┘ ``` The exchange is keyed on `audience`, not on RFC 8707's `resource`: Keycloak's Standard Token Exchange V2 is GA and is the internal-to-internal path, but does not yet support `resource`. When it does, migrating improves precision; it does not block anything today. **A token for one mini app is useless to another.** The restricted `aud` guarantees it: if mini app A leaks its token, mini app B's backend rejects it. ## Three paths | Path | For | Mechanism | |---|---|---| | `sollar-exchange` | A backend that trusts Sollar as IdP | RFC 8693 exchange in the native runtime | | `external-oauth` | A system with its own IdP | `ASWebAuthenticationSession` / Custom Tabs, PKCE | | `none` | No backend identity | — | > **DANGER** `external-oauth` never runs in the runtime's WebView. RFC 8252 §8.12 forbids embedded user-agents in native-app OAuth flows: the user cannot verify the URL or certificate, and the host is technically able to read the typed password. A third-party mini app requesting corporate credentials inside a WebView controlled by another third party is exactly the attack the RFC describes. There is no configuration that changes this, and customers do ask. ## Agent delegation When a Sollar agent acts, the chain records **two** identities — on whose behalf, and who is acting. RFC 8693 with `actor_token` produces a token carrying an `act` claim: ```json { "sub": "u_9f3a…", "act": { "sub": "agent_admin_assistant" }, "aud": "acme-erp-backend" } ``` A mini app's backend can therefore distinguish "Ana approved this" from "Ana's agent approved this". That matters for audit, for authority limits — a tenant may let an agent read but not approve — and for incident investigation. For delegation across a trust boundary, `draft-ietf-oauth-identity-chaining` (v17) is the standards-track direction. It is a draft: a direction, not a dependency. ## Rules **No long-lived secret reaches the runtime.** No `client_secret`, no API key, no refresh token, no signing key. WeChat documents the equivalent about its own `session_key` — *"the developer server should not send the session key to the Mini Program"* — for the same reason. **Client-side identity is a suggestion until the server validates it.** What `sollar.identity.get()` returns is UI convenience. Authorisation happens against the token, on the backend, every time. Telegram publishes the same warning about `tgWebAppData`, and it remains the most common failure in this platform class. **Short tokens, host-managed renewal.** The mini app calls `getToken()` before each use; it does not cache, persist or forward. **Revocation is central.** An uninstall, an administrator removing the app, or an employee leaving revokes the exchange immediately. Short tokens keep the window small; the exchange running through Keycloak makes the cut central. ## Server callback authentication When a mini app's backend calls Sollar, the request is signed — Slack's recipe, the best-documented of its kind: ``` base = "v0:" + timestamp + ":" + raw_body signature = "v0=" + hex(HMAC_SHA256(signing_secret, base)) headers X-Sollar-Signature, X-Sollar-Request-Timestamp ``` Constant-time comparison, and a short timestamp window to block replay. HMAC-SHA256, not SHA-1 — Lark's `jsapi_ticket` scheme uses SHA-1 and works, but SHA-1 is not a defensible choice for a design started in 2026. ## Open questions - **RFC 8707 `resource` in Keycloak.** When it lands, migrating from `audience` improves precision. - **Keycloak Organizations** is GA since 26.0.0 and is the intended B2B multi-tenancy base, but has not been validated under load on Sollar's topology. - **Cross-domain identity chaining** depends on an unfinished IETF draft. - **Keycloak inside mainland China** under PIPL and the national firewall remains unconfirmed — an inherited infrastructure risk that applies here unchanged. ============================================================================== # Review policy Source: https://miniapp.sollar.com/platform/review-policy/ ============================================================================== ## Who gets reviewed **Store apps do. Tenant-private apps do not.** That is a structural decision, not a courtesy. The central case this platform exists for is a company building a mini app of its own ERP for its own employees. If every change to that ERP joined a Sollar review queue, the platform would be unusable in a corporate setting — an internal release cycle cannot depend on a vendor's service level. Lark separates them the same way: self-built apps confined to one tenant with no Lark review, versus store apps with official review. DingTalk does the same for enterprise-internal applications, where an administrator creates the app under the `corpid` and grants its scopes without a platform gate. Skipping review is not skipping cryptography. A tenant-private package is countersigned by the organisation's CA, and Sollar's client refuses any package lacking a countersignature from either the store or an organisation CA. ## Entity verification To publish in the public store, a developer must be a verified entity. | Type | May publish | Requires | |---|---|---| | Individual | Apps touching no sensitive data, no external auth | Verified identity | | Organisation | Anything, subject to category | Business registration + a responsible contact | | Tenant (private) | For itself only | Already a contracted customer | Sensitive categories — health, finance, education involving minors — require additional qualification. This is not decorative bureaucracy: it is what makes it possible to refuse a "payroll advance loan" mini app from an entity with no licence to offer one. The model follows WeChat's, which recognises six subject types, each enabling different service categories and each requiring its own documents. ## The checklist Every store submission is checked against all of this. Tenant-private packages are validated mechanically against the same schema, without the human review. ### Manifest and permissions 1. The manifest validates against the published schema. 2. Every declared permission has a `purpose` that plausibly matches the app's stated function. 3. No permission is declared that the code never uses. **This catches more submissions than anything else** — a scope declared "in case we need it later" reads as over-collection, because it is. 4. Prompts appear at the point of use, not in a wall at launch. 5. Refusal produces a working degraded path, not a dead end or a re-prompt loop. ### Network and code 6. The network allowlist is justifiable. An unexplained host is a question to the developer, not an automatic rejection. 7. The CSP meets the floor: `script-src 'self'`, no `'unsafe-eval'`, no remote script host. 8. No `eval`, no `Function()`, no dynamically constructed code. 9. An SBOM is present and contains no high-severity known vulnerability. 10. No obfuscation that prevents review from reading what the code does. ### Actions 11. Every `actions[]` entry describes honestly what it does. 12. `destructive` is set where the effect is destructive. An action that releases an order to a vendor and is marked `read_only` is a rejection, and a serious one. 13. `confirm` is appropriate to the annotations. `never` with `read_only: false` is refused. 14. Descriptions do not attempt to steer an agent toward actions the user did not ask for. ### Content and conduct 15. Reporting and blocking are reachable from inside the mini app (Guideline 4.7.1). 16. A content rating is declared (4.7.5). 17. A privacy policy exists and covers what the permissions actually collect. 18. A monitored `vendor.contact`. ## Reasons for rejection WeChat publishes a specific list, and several entries map directly onto abuse any mini app store has to police. Quoted, because the wording is precise: - *"诱导分享、诱导添加、诱导关注公众号、诱导下载等"* — induced sharing, adding, following or downloading; requiring a user to share before they can use the app. - *"小程序的页面内容中不能存在虚假、欺诈类内容"* — false or fraudulent content, including fake prizes and campaigns. - *"禁止视频、音乐、语音等多媒体的自动播放"* — automatic playback of video, music or audio is prohibited. - *"不能做小程序导航,不能做小程序链接互推,小程序排行榜等"* — no navigation between mini apps, no cross-promotion, no ranking lists. The last one deserves emphasis. A store where mini apps promote each other stops being a tool directory and becomes an attention marketplace, and the tools lose. Sollar adds three of its own: - **A misleading manifest** — a permission whose `purpose` does not match the function. - **Actions that lie about destructiveness.** - **Any attempt to collect a user credential inside the runtime's WebView.** This one is not a warning and not a request for changes; it is a rejection and a look at everything else the developer has published. ## Approved is not published Review approves; the developer decides when to publish. This is the Chrome Web Store's separation and it is worth copying — it lets a release coordinate with a product launch, a campaign, or a maintenance window. With an expiry: **an unpublished approval lapses after 30 days.** Without that, old approvals accumulate whose security context has since changed. ## Reporting and moderation Guideline 4.7.1 requires that offered software *"include a method for filtering objectionable material, a mechanism to report content and timely responses to concerns, and the ability to block abusive users."* Concretely: - **Report** is reachable from inside any mini app, in the **host's chrome** — it does not depend on the mini app implementing it, because a malicious mini app would not. - A user can block a mini app; an administrator can block one for the whole organisation. - A published response time for reports. - `vendor.contact` is mandatory in the manifest. ## Emergency revocation Distinct from rollback. When a published version is dangerous: 1. A signed revocation list, consulted at launch, with a short TTL. 2. The client refuses to open the revoked version; if a safe version exists, it updates first. 3. If the whole app is revoked, it leaves the catalogue and installed copies stop opening. The client has to work offline, so there is a window. A mini app with sensitive permissions can be required to verify online at launch — declared in the manifest. ## What review cannot do Stated plainly: - **Review cannot verify an annotation.** Nothing proves an action marked `read_only` really is. What review adds is that the claim is attributable to a key and cannot be changed silently afterwards. - **Review reads a snapshot.** A mini app's backend can change its behaviour the day after approval. The manifest cannot — which is why capabilities live in the manifest and not in a server response. - **Review is not an audit.** An approved mini app has passed a checklist, not a penetration test. Nothing in this section has itself been through an independent audit; the platform is specified, not built. ============================================================================== # Security model Source: https://miniapp.sollar.com/platform/security-model/ ============================================================================== ## Start from what has already gone wrong The reference study is *A Small Leak Will Sink Many Ships: Vulnerabilities in Mini-Programs* (arXiv 2205.15202). It tested **more than 2,580 APIs across 9 mini-program ecosystems**, and its result is the honest starting point for anything new in this category: > **Every one of the nine ecosystems examined had at least one vulnerability.** There is no secure implementation here to copy. There is a catalogue of how everyone got it wrong. > **NOTE** A correction for anyone citing the paper: the seven million figure is the size of the ecosystem ("as of June 2021, the number of mini programs in the whole network exceeded 7 million"), not the sample. The authors examined 9 host apps. "Studied seven million mini programs" is a misreading that circulates. Section 3.2 of the paper names six categories. They are Sollar's review checklist and the agenda for its internal penetration testing. ## 1. Cache file reuse One mini app reads a file cached by another, because the host stores everything in a common directory. **Sollar:** a synthetic origin per mini app. Cache, `localStorage`, IndexedDB and cookies are partitioned **by the browser engine**, not by a directory convention. It is the difference between a rule the runtime remembers to apply and one it cannot violate. ## 2. PEL-API — permission encapsulated in a leaked API A high-level API internally invokes a privileged capability and, once exposed, hands the privilege over for free. The paper's example: obtaining the latitude and longitude of a map's centre "without the user's authorisation", through a map context function. **Sollar:** the bridge is minimal by construction, and every method is audited against one question — *what capability does this grant indirectly?* A method that accepts a broad configuration object is suspect by default, because that is where the extra capability hides. Where a web equivalent exists it is used instead: `navigator.geolocation` already carries the engine's own prompt and has no side path through another API. ## 3. Silent transmission of sensitive data Data leaves for a remote server without the user noticing. The paper cites the clipboard: *"the copied text could be sent to a remote server without user's awareness."* **Sollar:** a network allowlist in the signed manifest, enforced in the **native** request interceptor. Changing a destination requires a new version and a new gate. CSP is the second layer — a mini app can rewrite its own CSP at runtime, but it cannot reach the native interceptor. ## 4. Permission management failure A permission granted once lasts forever, leaks between contexts, or cannot be revoked. **Sollar:** permissions are keyed on (user, mini app, scope) and revocable at any time. Host app permissions do **not** flow to a mini app — required by Apple's Guideline 4.7.3 and enforced on both platforms. And the mandatory `purpose` string turns the grant into a contestable claim at review rather than a technical checkbox. ## 5. WebView bypassing permission control An embedded webview component loads external content that escapes the runtime's permission model. **Sollar: there is no `` component.** External content opens in the system browser via `sollar.ui.openExternal()`, with a visible address bar and lock icon. This is a deliberate amputation. WeChat's `web-view` requires a business-domain allowlist covering nested iframes and *still* is the vector for this entire category. The feature is not worth its attack surface. ## 6. Permission divergence across environments The permission model behaves differently in development, trial and production — and the hole exists in only one of them. **Sollar: the same permission-checking code in all three environments.** What changes between environments is data and endpoints, never the authorisation path. The [conformance suite](/reference/baseline/) runs the same permission tests against all three. ## Platform controls ### Android | Control | Rule | |---|---| | Bridge | `WebViewCompat.addWebMessageListener` with `allowedOriginRules`. **Never `addJavascriptInterface`.** | | Serving the package | `WebViewAssetLoader` + `shouldInterceptRequest` → `https://.miniapp.localhost/` | | File access | `setAllowFileAccessFromFileURLs(false)`, `setAllowUniversalAccessFromFileURLs(false)`, `setAllowFileAccess(false)` | | Safe Browsing | Enabled | | Renderer | Separate process, with `WebViewRenderProcessClient` to detect a hang | | Priority | `setRendererPriorityPolicy` — the mini app's renderer is sacrificed before the host | `allowedOriginRules` matches scheme, host and port; **the path is ignored**. ### iOS | Control | Rule | |---|---| | Bridge | `WKScriptMessageHandlerWithReply` in a dedicated `WKContentWorld` | | Serving the package | `WKURLSchemeHandler` — see the secure-context risk in [Architecture](/platform/architecture/) | | Storage | `WKWebsiteDataStore(forIdentifier:)` per mini app; `nonPersistent()` for ephemeral mode | | Domains | `WKAppBoundDomains` where applicable — **capped at 10 domains**, which makes it unusable as a per-mini-app control | | JIT | Present in the WebContent process — an advantage, not a risk | > **CAUTION** **`WKContentWorld` is not a security boundary.** It isolates JavaScript variables; it does not isolate the DOM, and DOM mutation is visible across all worlds. Apple documents it as a solution to script and namespace conflicts. It is right for preventing a mini app from overwriting the bridge function, and wrong for hiding data from one. ## The CSP floor The submission validator refuses a package that does not meet: ``` default-src 'self'; script-src 'self'; ← no 'unsafe-eval', no remote host object-src 'none'; base-uri 'none'; frame-ancestors 'none'; connect-src 'self' ``` `script-src 'self'` with no remote host is also what keeps Sollar inside Google Play's interpreter exemption: every executable byte arrives in the verified package. A mini app pulling a script from a CDN is, technically, downloading executable code from outside it. A third-party resource that genuinely must be remote requires Subresource Integrity with a pinned hash. ## Denial of service Per instance: a ceiling on concurrent connections, on storage, and on memory — with the mini app destroyed rather than the host running out of memory — plus a rate limit per bridge method. A mini app cannot degrade Sollar for the user. ## What review checks Beyond the six categories: 1. The manifest validates, and every permission has a `purpose` plausible for the declared function. 2. The network allowlist is justifiable — an unexplained host is a question to the developer. 3. The CSP meets the floor. 4. An SBOM is present, with no high-severity CVE in a shipped dependency. 5. No remote scripts, no `eval`, no obfuscation that defeats review. 6. `actions[]` describe honestly what they do; `destructive` is set where it is destructive. 7. Reporting and blocking are reachable (Guideline 4.7.1); a content rating is declared (4.7.5). ## Automated checks The submission pipeline: validate the manifest → scan the SBOM against vulnerability data → check the CSP → static analysis for prohibited patterns (`eval`, `Function()`, remote scripts) → **dynamic tests for all six categories**, with a deliberately malicious mini app kept as a permanent test case. **TaintMini** (ICSE 2023), a static taint-analysis tool for mini programs, is a candidate for the pipeline. ## What remains open Stated plainly, because a security page that claims completeness is not a security page. 1. **Secure context on iOS.** If a custom scheme is not a secure context, `crypto.subtle` and service workers are unavailable inside mini apps and the storage design changes. Verified per release. 2. **Obfuscation versus review.** WeChat ships code obfuscation as a first-party plugin with a reversible sourcemap. Obfuscation that review cannot undo is incompatible with real review; the policy must be settled before the store opens. 3. **No independent audit yet.** Nothing described here has been tested by an outside team. Before opening the store to unvetted third parties, penetration testing with a written scope, an isolated environment and synthetic data is a precondition, not a nice-to-have. ============================================================================== # Store compliance Source: https://miniapp.sollar.com/compliance/ ============================================================================== Sollar ships through the App Store and Google Play. Everything a mini app can do is bounded by what those two stores permit a host app to offer, and the two are **not symmetric** in the way most people expect. | Page | | |---|---| | [Apple App Store](/compliance/apple/) | Guideline 4.7 clause by clause, plus 2.5.6, 3.1.1 and 3.1.3(c) | | [Google Play](/compliance/google-play/) | The interpreter exemption, and the missing B2B carve-out | | [China](/compliance/china/) | ICP filing, and why it sits on your critical path | ## The asymmetry **Apple has a policy that names mini apps and permits them.** Guideline 4.7, "Mini apps, mini games, streaming games, chatbots, plug-ins, and game emulators", is an explicit authorisation with explicit conditions. **Google does not.** There is no Play policy for superapps or mini apps. Android's legality runs through a generic exemption in the *Device and Network Abuse* policy, which exempts from the downloaded-code prohibition anything running *"in a virtual machine or an interpreter where either provides indirect access to Android APIs (such as JavaScript in a webview or browser)."* This is the reverse of the usual intuition — iOS is the side with explicit permission, Android the side depending on a generic reading. It constrains the architecture: every execution path on Android must terminate in interpreted JavaScript with indirect, mediated access, never a native code loader. ## What it means for you ### Everything runs in a WebView, and that is not negotiable Guideline 2.5.6 requires WebKit, worldwide. The entitlement that would allow otherwise is EU-only and out of reach. So a mini app is a web application — see [Architecture](/platform/architecture/). ### The bridge stays small Guideline 4.7.2: *"Your app may not extend or expose native platform APIs or technologies to the software without prior permission from Apple."* Every native capability added to the Sollar bridge requires Apple's prior approval. That is a business-development timeline, not a sprint. It is why camera, geolocation and files are standard Web APIs here and not bridge methods, and why `sollar.getDeviceContacts()` does not exist. ### There is no payment API **A mini app cannot take money inside Sollar.** No `sollar.requestPayment()`, no equivalent of WeChat Pay or `my.tradePay`. If you arrive from WeChat you will look for it, so it is stated here rather than left as silence. Money works through a contract and an invoice, outside the app — and the rules for that differ by platform: | | iOS | Android | |---|---|---| | Baseline | 3.1.1 requires In-App Purchase; licence keys, QR codes and crypto are prohibited | Play Billing required | | B2B exemption | **Exists** — 3.1.3(c) *Enterprise Services*, for sales direct to organisations for their employees | **Does not exist** — the policy explicitly covers *"business productivity software"* and *"cloud software and services"* | The exemption that supports the business model on iOS **has no Android equivalent**. On Android, the sale must be entirely outside the app — a contract, an invoice — and never an unlock inside a mini app. This is counter-intuitive and it is load-bearing, so it appears on every page where it matters. > **CAUTION** Anti-steering is under active litigation in the United States (*Epic v. Apple* and its aftermath). The economics of monetisation in that market may change before Sollar launches. Nothing on these pages should be read as a settled position on US anti-steering. ### Permissions do not inherit Guideline 4.7.3: permission granted to the host does not flow to a mini app. Each asks for its own. Sollar enforces this on both platforms — see [Permissions](/guides/permissions/). ### Reporting, blocking and age ratings Guideline 4.7.1 requires content filtering, a reporting mechanism, timely response, and the ability to block abusive users. 4.7.5 requires an age rating per mini app. Sollar provides reporting and blocking in the **host's chrome**, so they do not depend on a mini app implementing them. You provide the content rating. ### The public index Guideline 4.7.4 requires an index of the software available in the app, with universal links to all of it. That is [the public catalogue](/apps/), and it means the catalogue cannot be closed or invitation-only. ## What this section is not It is a working interpretation by the Sollar team of published store guidelines, based on primary sources quoted where they are quoted. It is **not legal advice**, and several questions here remain open — most importantly whether the App Review Guidelines apply equally to Custom Apps distributed through Apple Business Manager, which our research could not close from a primary source. Open questions are marked as open on the pages where they arise. ============================================================================== # Apple App Store Source: https://miniapp.sollar.com/compliance/apple/ ============================================================================== Apple is the platform with an **explicit** mini app policy. Guideline 4.7 names the category and authorises it under conditions. Those conditions shape the whole platform. Quotations are from the App Review Guidelines. Read them at [developer.apple.com/app-store/review/guidelines](https://developer.apple.com/app-store/review/guidelines/) — the guidelines change, and the version in front of a reviewer is the one that counts. ## 4.7 — the authorisation > *Apps may offer certain software that is not embedded in the binary, specifically HTML5 mini apps, > mini games, streaming games, chatbots, plug-ins, and game emulators.* HTML5 mini apps are named. That is the permission Sollar operates under, and it is why the runtime is a WebView rather than anything more ambitious. ## 4.7.1 — moderation > *…include a method for filtering objectionable material, a mechanism to report content and timely > responses to concerns, and the ability to block abusive users.* **Sollar's implementation:** report and block live in the **host's chrome**, reachable from inside any mini app. They do not depend on the mini app implementing them — a malicious mini app would not. A user can block a mini app; an administrator can block one organisation-wide. Response times are published. **Yours:** a monitored `vendor.contact` in the manifest. It is where reports about your app go. ## 4.7.2 — the constraint that shapes the bridge > *Your app may not extend or expose native platform APIs or technologies to the software without > prior permission from Apple.* This single sentence is why the Sollar bridge is small. Every native capability exposed to mini apps is a compliance question needing Apple's **prior permission** — a business-development gate with a lead time, not an engineering decision with a sprint. So the rule is **web first**: - Camera, microphone, geolocation, files, notifications, biometrics → standard Web APIs the engine already provides, with the engine's own prompts. These are capabilities of the web platform, not extensions of a native API. - `sollar.*` covers **only Sollar's own domain** — identity, room context, sending a message, agent actions, Sollar file storage. `sollar.sendMessage()` is not extending a platform API. `sollar.getDeviceContacts()` would be, and that is why it does not exist. ## 4.7.3 — permissions do not inherit > *…may not extend the permissions granted to your app to the software.* Permission the user granted to Sollar does not flow to a mini app. Each mini app asks for its own, declares a `purpose`, and can be revoked independently. Sollar enforces this on Android too. It is a sound rule regardless of who requires it. ## 4.7.4 — the public index > *You must provide an index of software and metadata available in your app. It must include > universal links that lead to all of the software offered in your app.* [`miniapp.sollar.com/apps/`](/apps/) is that index, with a universal link per mini app (`https://sollar.com/app/`). Consequence: **the public catalogue cannot be closed or invitation-only.** > **CAUTION** **Tenant-private mini apps are the unresolved part.** The defensible reading is that a tenant-private app is not "software offered in your app" — it is configuration provisioned by a customer's administrator, analogous to MDM, and never offered to the public. That is a defensible reading, **not a confirmed one**. Alongside it sits a question our research could not close from a primary source: whether the App Review Guidelines, 4.7 included, apply equally to Custom Apps distributed through Apple Business Manager. That question decides the tenant-private route on iOS and it is open. ## 4.7.5 — age ratings > *Software offered in your app must be rated with the appropriate age rating.* Declared per mini app in the manifest. Sollar surfaces it in the catalogue and in the install prompt. ## 2.5.6 — WebKit, worldwide > *Apps that browse the web must use the appropriate WebKit framework and WebKit JavaScript.* The Embedded Browser Engine Entitlement that would permit an alternative exists **only in the European Union** — not Japan, not the United States — and qualifying requires 90% on the Web Platform Tests, 80% on Test262, a memory-safe implementation language and a 30-day CVE remediation commitment. Out of reach, and geographically useless even if it were not. This is the first of three reasons the runtime is a WebView. The other two are in [Architecture](/platform/architecture/), and one of them is that WKWebView's WebContent process has JIT while an in-process engine does not — so the alternative would be *slower*, not just illegal. ## 2.5.2 — no downloaded executable code > *Apps should be self-contained… and may not read or write data outside the designated container > area, nor may they download, install, or execute code which introduces or changes features or > functionality of the app.* A mini app is interpreted JavaScript running in the WebView, inside the container, mediated by the bridge. 4.7 is the specific authorisation that makes this consistent with 2.5.2; the CSP floor of `script-src 'self'` is what keeps a mini app from reaching outside its verified package for code. ## 3.1.1 — in-app purchase > *If you want to unlock features or functionality within your app… you must use in-app purchase.* The guideline also prohibits unlocking through licence keys, QR codes and cryptocurrencies. **Therefore: Sollar has no payment API.** No `sollar.requestPayment()`, no equivalent of WeChat Pay. A mini app cannot unlock features for money inside Sollar. ## 3.1.3(c) — the enterprise exemption > *Enterprise Services: Apps that are sold directly by you to your organization or to other > organizations for use by their employees…* This is what supports the business model on iOS: Sollar sells to a company; the company's employees use it. Money moves through a contract, outside the app. > **DANGER** **This exemption has no Google Play equivalent.** Play Billing's policy explicitly covers *"business productivity software"* and *"cloud software and services"*. The carve-out that works on iOS does not work on Android, and assuming symmetry here is the mistake this page exists to prevent. See [Google Play](/compliance/google-play/). ## 5.3 — real money gaming, contests, lotteries Applies per mini app. Anything in this territory needs its own licensing and geographic restriction, declared at submission. Sollar's review refuses it without documentation. ## Dead ends, closed with evidence | Route | Why it does not work | |---|---| | Apple Developer Enterprise Program | Requires 100+ employees and is **internal use only**. A vendor cannot distribute to many customers through it. | | Ad-hoc distribution | 100 devices, one year. Not a distribution channel. | | TestFlight as production | Builds expire after 90 days. Not a distribution channel. | ## Submission checklist Before Sollar itself submits a client build: 1. The public index at `/apps/` is live, with a universal link per store mini app (4.7.4). 2. Report and block are reachable from inside any mini app, in the host's chrome (4.7.1). 3. Every mini app has a content rating (4.7.5). 4. No mini app permission inherits from the host (4.7.3). 5. The bridge exposes no native platform API beyond what Apple has approved (4.7.2). 6. Every runtime is WKWebView; no alternative engine anywhere in the binary (2.5.6). 7. No mini app can unlock paid functionality in-app (3.1.1), and the enterprise route is documented for review (3.1.3(c)). 8. Review notes explain the mini app model, name the guideline, and provide a demo account with at least one installed mini app. Point 8 is not bureaucracy. A reviewer encountering a superapp without an explanation reaches for 2.5.2 and rejects it. Naming 4.7 in the review notes is what makes it a short conversation. ============================================================================== # China Source: https://miniapp.sollar.com/compliance/china/ ============================================================================== If Sollar operates a mini app store in mainland China, a second and entirely separate regulatory perimeter applies. This page describes what is known and marks what is not. ## ICP filing covers mini programs Since **September 2023**, the MIIT's ICP filing requirement (备案) applies to mobile applications and mini programs, not only to websites. Tencent enforces it inside its own developer portal for WeChat mini programs, and any comparable platform operating in the mainland faces the same requirement. **Each mini app needs its own filing.** It is not covered by the host's. ### Reported timeline | Stage | Reported duration | |---|---| | Platform review | 1–2 days | | SMS verification | 24 hours | | Provincial telecommunications bureau | **1–20 days** | The provincial bureau's step is the one that matters for planning. It sits on the developer's critical path, it is outside anyone's control, and its range is wide enough that a release date cannot be committed until it clears. Plan filing as a precondition of the project, not a step before launch. ## What a filing requires A registered mainland business entity, a responsible person with a mainland identity document, a domain filed to that entity, and hosting inside the mainland. A foreign company without a mainland entity cannot file, which makes the entity question a prerequisite rather than a detail. ## Content and data obligations Beyond filing, mainland operation brings obligations that are not documentation footnotes: - **PIPL** — the Personal Information Protection Law governs collection, consent, and cross-border transfer of personal information. Moving personal data out of the mainland has its own approval regime. - **Data localisation** for several categories. - **Real-name verification** requirements for certain service categories. - **Content review** obligations that fall on the platform operator, meaning Sollar, for everything its mini apps publish. Sensitive categories — health, finance, education, news, mapping — each carry their own licensing. ## Open, and material > **CAUTION** **Keycloak inside mainland China is unconfirmed.** Sollar's identity layer depends on Keycloak, and whether it can operate compliantly inside the mainland, under PIPL and behind the national firewall, has not been established. This is an inherited infrastructure risk, and it is not small: if it does not hold, the identity architecture in the mainland is a different design, not a configuration change. Also open: - Whether Sollar would operate the mainland store itself or through a licensed local partner. The answer changes who holds the filing, who carries the content-review obligation, and who is liable. - How the mainland catalogue relates to the international one. Two catalogues, two review queues and two sets of accepted categories is the likely answer, and it doubles the operational surface. - Whether cross-border identity — an employee of a multinational using the same Sollar account in Shanghai and in Frankfurt — is achievable under PIPL's transfer rules. ## What this means today Nothing in Sollar's mini app specification assumes mainland operation, and nothing in it prevents mainland operation. The package format, the manifest, the bridge and the review model are jurisdiction-neutral. What is **not** jurisdiction-neutral is the identity layer, the hosting, and the store operation. Those are open questions with commercial and legal answers, not technical ones, and they are recorded here as open rather than resolved by assumption. ## If you are building for a mainland tenant Practical advice, given the above: 1. **Start the filing before you start the code.** The provincial step will not compress. 2. **Assume data localisation.** Design your backend so the mainland deployment is a separate instance with its own data, not a region flag on a global one. 3. **Do not design around cross-border identity** until the transfer question is answered. 4. **Expect a separate review queue** with different accepted categories. ============================================================================== # Google Play Source: https://miniapp.sollar.com/compliance/google-play/ ============================================================================== Google Play has **no policy for superapps or mini apps**. There is no Android equivalent of Apple's Guideline 4.7 — no clause that names the category and authorises it. That is the reverse of what most people assume, and it changes the architecture. ## The interpreter exemption Android's legality runs through the *Device and Network Abuse* policy, which prohibits apps from downloading executable code but exempts: > *…code that runs in a virtual machine or an interpreter where either provides indirect access to > Android APIs (such as JavaScript in a webview or browser).* Every word there is load-bearing: | Phrase | Consequence for Sollar | |---|---| | *in a virtual machine or an interpreter* | Mini apps are interpreted JavaScript. Never a native code loader, never a dynamically loaded `.so` or `.dex`. | | *indirect access to Android APIs* | Every capability reaches Android through the Sollar bridge, mediated and checked. Never a direct binding. | | *such as JavaScript in a webview* | The named example is exactly the architecture. That is not a coincidence; it is why the architecture is this. | **All executable code arrives inside the verified package.** The CSP floor — `script-src 'self'`, no remote host, no `'unsafe-eval'` — is what makes that true. A mini app pulling a script from a CDN is downloading executable code from outside the verified package, which is precisely what the exemption does not cover. That is why the CSP floor is enforced by the submission validator rather than recommended in a style guide. It is a legal boundary wearing a security control's clothes. ## What has no Android equivalent > **DANGER** **Apple's 3.1.3(c) enterprise exemption does not exist on Google Play.** Apple exempts apps *"sold directly by you to your organization or to other organizations for use by their employees"* from the In-App Purchase requirement. Google's Payments policy has no such carve-out — and it explicitly names the categories a B2B vendor would hope to fall under: *"business productivity software"* and *"cloud software and services"* are covered by Play Billing, not exempted from it. **Consequence:** on Android the sale must be **entirely outside the app** — a contract, an invoice, a purchase order between two companies. Never an unlock inside a mini app, never a purchase flow inside Sollar, never a link that steers a user from inside the app to a payment page. Assuming that what works on iOS works on Android is the single most expensive mistake available on this page. What remains available on Android, and is what Sollar uses: the customer signs a contract with Sollar, is invoiced, and their employees use the product. No transaction happens in the app or is initiated from it. ## Distribution routes | Route | Verdict | |---|---| | Play Store, public listing | The route Sollar uses | | Managed Google Play — private apps | **Cannot charge anything**, and cannot coexist with a public listing. Closed. | | Sideloading / APK download | Not a product distribution channel | | Alternative stores | Reach without the guarantees, and no help with the billing question | Managed Google Play private apps deserve their line because they look like the answer to tenant-private distribution and are not: a private app cannot charge, and the exclusivity with a public listing means Sollar would have to choose one. Tenant-private mini apps are therefore provisioned **inside** the single publicly listed Sollar app, by tenant administrators — which is the design in [Distribution](/platform/distribution/) anyway. ## Data safety Google Play requires a Data safety declaration covering what the app collects and shares — and it covers what mini apps collect through the host. This is why the manifest's `purpose` strings and the permission model matter beyond user experience: Sollar's Data safety form is assembled from what mini apps are permitted to do, so an over-declared scope in your manifest becomes an over-declaration on a form Google reads. A permission declared and never used is therefore not a harmless leftover. Remove it. ## Target API level Google Play requires a recent `targetSdkVersion` and raises it annually. This is Sollar's obligation, not yours, but it has one consequence you will feel: Sollar's `minSdk` moves over time, and the [compatibility baseline](/reference/baseline/) moves with it — at most once a year, announced at least two release cycles ahead. ## WebView versions Android System WebView updates independently of the OS, through Play. In practice most devices run a recent build, but not all: managed devices with delayed update policies, devices in regions with limited Play services, and a long tail of old hardware run older WebViews. The baseline is **Android System WebView 114+**. Below that, Sollar refuses to run mini apps rather than running them unpredictably — a mini app failing in a way its developer never saw is worse than one that does not start. ## User Data policy Google's User Data policy requires prominent disclosure and consent for sensitive data access, and it applies to what happens inside mini apps. For you this means the permission prompt and the `purpose` string are not just Sollar's mechanism — they are how Sollar meets an obligation on Android. A `purpose` that does not describe what the app actually does with the data is a policy problem, not only a review finding. ## Checklist for Sollar's own Play submission 1. No mini app path loads native code. Everything is interpreted JavaScript with mediated access. 2. The CSP floor is enforced by the validator; no mini app can load a remote script. 3. No purchase flow of any kind inside the app, and no steering to one. 4. The Data safety declaration matches the permission scopes mini apps can hold. 5. Prominent disclosure and consent for every sensitive scope, at the point of use. 6. The listing describes the mini app model plainly. An unexplained superapp invites the wrong policy reading, exactly as it does on iOS. ============================================================================== # Build with AI Source: https://miniapp.sollar.com/ai/ ============================================================================== A company should be able to point an AI coding agent at this site and get a working mini app of its own ERP, without a specialist who already knows the platform. That is a design goal of the documentation, not an afterthought — and it is why every page here states its constraints in prose rather than assuming background knowledge. ## Point an agent at the docs Give the agent this line: ``` Read https://miniapp.sollar.com/llms.txt and build a Sollar mini app that . ``` | Artifact | What it is | |---|---| | [`/llms.txt`](/llms.txt) | An index of the whole site, plus the rules an agent needs before writing a line | | [`/llms-full.txt`](/llms-full.txt) | Every page concatenated, for an agent with room for it | | `.md` | The Markdown source of that page — e.g. [`/reference/manifest.md`](/reference/manifest.md) | | [`/schemas/manifest.schema.json`](/schemas/manifest.schema.json) | JSON Schema 2020-12 for the manifest | | [`/schemas/sollar.d.ts`](/schemas/sollar.d.ts) | The canonical bridge type definitions | The `.md` twins are generated **from the source Markdown**, not scraped back out of rendered HTML. An agent fetching `/reference/manifest.md` gets what the author wrote, with the tables intact. ## Scaffold a project ```sh sollar create-mini-app acme-erp --template react cd acme-erp ``` The scaffold writes two files that matter to agents: ``` AGENTS.md ← the instructions CLAUDE.md ← one line: @AGENTS.md ``` **Why two.** `AGENTS.md` is the filename Codex, Cursor, Jules, Factory and Aider look for. Claude Code reads `CLAUDE.md` and does **not** read `AGENTS.md`. Rather than maintaining the same instructions twice and letting them drift, `CLAUDE.md` imports the other: ```markdown title="CLAUDE.md" @AGENTS.md ``` One file is the source; the other is a pointer. Never duplicate the content by hand — a duplicated instruction file is worse than none, because half of it will be a year out of date and nothing will say which half. See [AGENTS.md](/ai/agents-md/) for what to put in it. ## What an agent gets wrong without being told Every one of these has a page that explains it; an agent that has not read that page will get it wrong in a way that looks plausible. Put them in your `AGENTS.md`. **It will invent bridge methods.** `sollar.getLocation()`, `sollar.camera.take()`, `sollar.requestPayment()` — all reasonable-looking, none real. The bridge covers only Sollar's own domain; device capabilities are standard Web APIs. → [Bridge API](/reference/bridge-api/) **It will trust the client.** `sollar.identity.get()` returns an `app_user_id`, and an agent will happily authorise against it. It is UI convenience. Authorisation happens on the server, against the token. → [Authentication](/guides/authentication/) **It will post to a room from the server.** The obvious design for "notify the channel when the order is approved" is a server-side call, and it does not work in an encrypted room — which, in the Enterprise and Sovereign tiers, is every room by default. → [Messaging and rooms](/guides/messaging-and-rooms/) **It will fetch a host that is not in the manifest.** The network allowlist is enforced natively and the failure is `ERR_HOST_NOT_ALLOWED`. Adding a host means a new version. → [Manifest](/reference/manifest/) **It will write a permission check in JavaScript.** Permission decisions are made in native code against the signed manifest. A JavaScript check is not a check. → [Permissions](/guides/permissions/) **It will guess the version.** Do not let it. Run `sollar --version` and read [the baseline](/reference/baseline/). ## Verify, do not trust An agent will produce a mini app that looks right. Two commands decide whether it is: ```sh sollar validate --strict # the same checks the submission gate runs sollar test --conformance # the ES2022 floor, the CSS baseline, every bridge method ``` `--strict` applies the store review checks — purpose strings, justifiable hosts, SBOM, no remote scripts, no `eval`. Running it in CI turns a review rejection three days from now into a red build in ninety seconds. ## Writing actions an agent uses correctly The other half of "build with AI": your mini app's `actions[]` are tools that *other* agents will call. Writing a description is engineering, and the rules are in [Agent actions](/guides/agent-actions/): - Make implicit context explicit — "only the current user's queue, never another's". - Fully qualify parameter names — `purchase_order_id`, not `id`. - State units and formats — `amount_minor`, `currency_code` in ISO 4217. - Say what is irreversible. It is what makes an agent stop and ask. - Return errors that teach: *"Order 4471 was already approved on 2026-08-30 by another approver"* beats `ERR_CONFLICT`, which teaches nothing and invites a retry. - Prefer a few coarse actions to many fine ones. ## An honest note about this site The platform described here is **specified, not built**. An agent reading these pages is reading a design contract. Everything is internally consistent and evidence-backed, and none of it has been run in production or audited by an independent team. If you are generating code against it, generate against the schemas — they are machine-checkable — and expect the surface to move before launch. ============================================================================== # AGENTS.md Source: https://miniapp.sollar.com/ai/agents-md/ ============================================================================== `sollar create-mini-app` writes an `AGENTS.md`. This page explains what belongs in it and why the companion `CLAUDE.md` contains exactly one line. ## The two-file pattern ``` AGENTS.md ← every instruction CLAUDE.md ← @AGENTS.md ``` `AGENTS.md` is the filename Codex, Cursor, Jules, Factory and Aider look for. Claude Code reads `CLAUDE.md` and does **not** read `AGENTS.md`. ```markdown title="CLAUDE.md" @AGENTS.md ``` One source, one pointer. The alternative — the same instructions written into both files — drifts within a month, and nothing tells a reader which copy is current. ## What the scaffold writes ```markdown title="AGENTS.md" # Acme ERP — a Sollar mini app ## What this is A mini app that runs inside the Sollar superapp: a signed web package, served from its own synthetic origin, with capabilities declared in `manifest.json`. Reference: https://miniapp.sollar.com/llms.txt ## Rules that are not negotiable - **The client is not an authority.** `sollar.identity.get()` is for drawing UI. Every authorisation decision happens on the backend, against the token, every time. - **The manifest is the ceiling.** A permission or a host not declared in `manifest.json` cannot be obtained at runtime. Adding one means a new version, not a code change. - **No secret reaches this codebase.** No client_secret, no API key, no signing key, no refresh token — not in .env, not in a config file, not in a comment. Call `sollar.auth.getToken()` before each use. - **Device capabilities are Web APIs, not bridge methods.** Camera is `navigator.mediaDevices.getUserMedia`. Location is `navigator.geolocation`. If you are reaching for `sollar.()`, it does not exist. - **There is no payment API.** A mini app cannot take money in-app on either platform. - **The backend cannot post into an encrypted room.** Use `sollar.message.send()` from the user's client, or an agent that is a member of the room. ## Before you write code Read these, in this order: 1. https://miniapp.sollar.com/reference/manifest.md 2. https://miniapp.sollar.com/reference/bridge-api.md 3. https://miniapp.sollar.com/guides/authentication.md Do not guess the bridge version. Run `sollar --version`. ## This project - Backend: https://erp.acme.example — declared in `manifest.network.connect` - Auth mode: sollar-exchange, audience `acme-erp-backend` - Actions: see `manifest.json`. Every one needs a handler in `src/actions.js`. ## Before you say you are done npx sollar validate --strict npm run test:unit npx sollar test --conformance `validate --strict` runs the same checks as the submission gate. If it fails, the work is not done. ``` ## Why each rule is there Each line in that file corresponds to something an agent gets wrong reliably. | Rule | The failure it prevents | |---|---| | The client is not an authority | An agent reads `app_user_id` from the bridge and authorises on it. It looks correct and is the defining vulnerability of this platform class. | | The manifest is the ceiling | An agent adds a `fetch` to a new host and cannot understand why it fails. `ERR_HOST_NOT_ALLOWED` is not a bug. | | No secret in the codebase | An agent asked to "configure the API key" will write it into `.env` unless told otherwise, and it will be committed. | | Web APIs, not bridge methods | Invented methods are the most common single failure. `sollar.getLocation()` is exactly what a well-trained model expects to exist. | | No payment API | An agent that knows WeChat will reach for `requestPayment`. | | Encrypted rooms | The obvious notification design is a server-side post, and it silently does not work. | ## Keep it short An instruction file competes for context with the code the agent needs to read. A 400-line `AGENTS.md` is worse than a 60-line one, because the important rules are diluted. The test for a line: **could the agent derive this from the code?** Directory layout, dependency list, "this file handles routing" — all derivable, all wasted tokens. What belongs is what the code does not say: the magic value, the counter-intuitive library behaviour, the product decision, the lesson from an incident. ## Point at URLs, do not copy ```markdown Read https://miniapp.sollar.com/reference/bridge-api.md before calling any sollar.* method. ``` Not: ```markdown The bridge API has the following methods: sollar.identity.get() returns… ``` A copied API surface is a copy that will be wrong after the next release, sitting in a file the agent trusts. Link to the `.md` twin and it is always current. ## Add what is specific to you The scaffold cannot know your project. Add: - **Domain vocabulary.** If "approval" means something specific in your organisation, say so. - **Authority limits.** "An agent may approve up to €5,000; above that a human approves directly." - **What is irreversible.** If an action releases an order to a vendor, the agent should know before it writes the handler, not after. - **Anything an incident taught you.** These are the most valuable lines in the file and the ones nobody writes down. ## What not to put in it - **Secrets.** Ever, in any form, including "the key is in 1Password item X" written as a value. - **Duplicated documentation.** Link to it. - **Directory listings.** The agent can run `ls`. - **Instructions that contradict the platform.** "Skip validation to save time" produces a mini app that fails at submission, having wasted the time it saved several times over. ============================================================================== # Mini app index Source: https://miniapp.sollar.com/apps/ ============================================================================== This page is the public index of mini apps available in the Sollar superapp, published to satisfy Apple's App Store Review Guideline 4.7.4: > *You must provide an index of software and metadata available in your app. It must include > universal links that lead to all of the software offered in your app.* ## Status **The Sollar mini app store has not launched.** No mini apps are published, so this index is empty. When the store opens, each publicly available mini app will be listed here with its name, vendor, category, content rating, and a universal link of the form `https://sollar.com/app/` that opens it in Sollar. | Mini app | Vendor | Category | Rating | Link | |---|---|---|---|---| | *(none published)* | | | | | ## What is not listed here **Tenant-private mini apps do not appear in this index.** A tenant-private mini app is provisioned by a customer's administrator for that organisation's own staff, is visible only inside that organisation, and is never offered to the public. It is configuration of a customer's deployment, analogous to software distributed through mobile device management. That reading of Guideline 4.7.4 is our position and it is recorded as an open question rather than a settled one — see [Distribution](/platform/distribution/). ## Reporting a mini app Every mini app can be reported and blocked from inside Sollar itself, in the host's own interface, without depending on the mini app to provide the control. Reports go to Sollar and to the vendor contact declared in the mini app's manifest. For a mini app listed here, the vendor contact will appear on its listing. ## For developers To publish here, see [Publish](/start/publish/) and [Review policy](/platform/review-policy/). Publishing to the public catalogue requires a verified entity and passes Sollar review; publishing privately to your own organisation requires neither.