@ambasada/miniapp
The page side of the Ambasada app's mini-app shell. Call ready() when your first screen is drawn, read the
launch context, respect the safe areas and the host bar, open links and other mini apps, ask for haptics,
silent sign-in and notifications. Outside the Ambasada app every call is a safe no-op, so the same page is
also a plain website. Zero dependencies, ~3 KB.
Install
npm install @ambasada/miniapp
import { ready, context, on } from "@ambasada/miniapp";
ready(); // after the first render: the shell drops the splash
Or, with no build step, the <script> build the hub serves (defines window.ambasada):
<script src="https://hub.ambasada.id/miniapp.js" defer></script>
<script>addEventListener("DOMContentLoaded", () => window.ambasada?.ready?.())</script>
The ambasada init templates ship both forms, and ambasada check warns when a page uses neither.
API
| Call | Inside the app | Outside |
|---|---|---|
ready() | tells the shell the page is drawn (once; idempotent); sets the CSS variables | sets the CSS variables to 0 |
shell() | the injected window.ambasadaShell (version ≥ 2) | null |
isInAmbasada() | true (also for older builds that only set the AmbasadaChat/ user agent) | false |
context() | { app, platform, safeArea, hostBar, theme, locale, user, location }, with defaults for what an older shell omits | the defaults |
capabilities() | what this shell build supports (version 2: openUrl, close) | [] |
applyCssVars() | writes --ambasada-safe-area-{top,bottom,left,right} and --ambasada-host-bar on <html> and toggles the in-ambasada class | same, with zeros |
openUrl(url) | the system browser (verified apps) or a confirm sheet | a new tab |
openApp(id, path?) | another mini app on top of this one | https://ambasada.id/a/<id><path> in a new tab |
close() | closes this mini app | history.back() |
share({ url?, text? }) | the system share sheet | the Web Share API, or nothing |
haptics.impact(style?), .notification(style?), .selection() | haptics | nothing |
setTitle(title) | the host bar's title and document.title | document.title |
auth.getToken() | a 60 s JWT (aud = your origin or app id, sub = the Ambasada user id), for installed apps whose Install granted identity; verify it against https://hub.ambasada.id/.well-known/jwks.json (auth.verifyShellToken in @ambasada/sdk) | rejects: fall back to Sign in with Ambasada |
auth.signInSilently({ endpoint?, me?, timeoutMs? }) | on open: when nobody is signed in, getToken() and POST it to /api/auth/shell, which sets your session; resolves the user. Once per page load, 5 s at most | null, never rejects: show "Sign in with Ambasada" |
auth.canSignInSilently() | inside the app, a shell that mints tokens, a person in context.user | false |
notifications.request() | the shell's permission sheet → { enabled }; your webhook gets notifications_enabled | { enabled: false } |
on("context", fn) / on("notification", fn) | a new context (safe areas, theme) / a notification tap { id, url }; returns the unsubscribe | never fires |
mockEnv(overrides?) | local development: installs a version 3 shell, records messages, answers requests, returns { messages, emit, uninstall } | same (ambasada dev uses it) |
Layout: pad your first screen by var(--ambasada-safe-area-top, 0px) + var(--ambasada-host-bar, 0px) at the top
and var(--ambasada-safe-area-bottom, 0px) at the bottom. The 0px fallbacks keep the page right on the web.
The bridge protocol
The native app injects window.ambasadaShell before the page loads:
{ version: 3, platform: "ios" | "android" | "web", capabilities?: Capability[],
app: { id, tier } | null, safeArea: { top, bottom, left, right }, hostBar: number,
theme: { accent, scheme: "light" }, locale, user: { id, name, avatar } | null, location: LaunchLocation }
Version 2 shells (the current app) carry only version, platform and safeArea; the SDK falls back to the
v2 messages (chrome, open) for them.
Page → app: one JSON message per call, through webkit.messageHandlers.ambasada.postMessage(obj) on iOS or
window.ambasada.postMessage(json) on Android:
| Message | Fields |
|---|---|
ready | |
openUrl | url |
openApp | app, path? |
close | |
share | url?, text? |
haptic | kind: impact | notification | selection, style? |
title | title |
request | id, method: auth.getToken | notifications.request |
chrome (v2) | header?, tabs?, title?, back?, statusBar? |
open (v2) | url, mode: in-app | external | sheet |
App → page: CustomEvents dispatched on window:
| Event | detail |
|---|---|
ambasada:reply | { id, result } or { id, error }, answering a request (the SDK gives up after 15 s) |
ambasada:context | a (partial) new ShellContext; the SDK merges it into window.ambasadaShell and re-applies the CSS variables |
ambasada:notification | { id, url }, a notification tapped while this app is open |
The <script> build merges into an existing window.ambasada (Android's bridge object) instead of replacing
it, so window.ambasada.postMessage keeps working next to window.ambasada.ready().
Build
node scripts/build.mjs writes dist/index.js (ESM), dist/miniapp.js (classic script, global ambasada) and
dist/index.d.ts. The hub serves dist/miniapp.js at https://hub.ambasada.id/miniapp.js.
MIT, CENTYSO.