Web apps
A web app is a page you host that opens inside the Banga app. The window.banga bridge
connects it to the phone: who the user is, a signed token for your backend, a native button, and posting cards.
The manifest
{
"contract": 1,
"name": "Trip Planner",
"description": "Plan a trip together.",
"scopes": ["profile.name", "post.cards"],
"web": {
"url": "https://trips.example.com/banga",
"origins": ["https://trips.example.com"],
"launchMode": "sheet"
}
}
| Field | Rules |
|---|---|
web.url | The page that opens, https. |
web.origins | Up to 10 https origins, such as
https://trips.example.com, with no path. Must include the origin of web.url. The bridge
exists only on pages from these origins. A link to anywhere else asks the user and opens in the browser. |
web.launchMode | full (the default) opens a full-screen page;
sheet a tall bottom sheet; compact a short one. The page can ask to go full screen. |
A web app can also have a webhook, commands and card templates. Its cards' open_app buttons open it,
optionally at a path and with a payload.
The bridge
Every method returns a promise. A failed call rejects with {code, message?}.
<script>
async function start() {
if (!window.banga) {
// Not inside the app, for example opened in a desktop browser: show a normal sign-in.
return;
}
const context = await banga.getContext();
const user = await banga.getUser();
document.getElementById("hello").textContent = "Hello " + (user.name || "there");
// Prove who the user is to your own backend.
const token = await banga.requestAuthToken();
await fetch("/api/session", { method: "POST", headers: { Authorization: "Bearer " + token } });
await banga.mainButton.set({ text: "Share plan", visible: true });
banga.mainButton.onClick(async () => {
const { posted } = await banga.postCard({
blocks: [{ type: "title", text: "Weekend in Lisbon" }, { type: "text", text: "Friday to Sunday" }],
actions: [{ type: "open_app", label: "Open plan", path: "/plans/42" }]
});
if (posted) await banga.haptic("success");
});
}
window.addEventListener("load", start);
</script>
| Method | Result |
|---|---|
banga.version | "1", the bridge contract. |
getContext() | {appId, launch: {source, payload?}, viewport, theme: {scheme, accent,
surface}, locale, roomType?}. launch.payload is the payload of the open_app
button that opened the page. roomType is present when it was opened from a room; the room itself
is never revealed. |
getUser() | {id, name?, language?}. id is the same per-app id
your webhook receives. The rest follows the scopes. Use it for display; trust only a verified token. |
requestAuthToken() | A session token for your backend. See below. |
postCard(card) | {posted}. Checks the card, shows the user a preview, lets
them pick a room when the page was not opened from one, and posts it once they confirm. posted is
false when they decline. Needs the post.cards scope. |
mainButton.set({text, enabled?, progress?, visible?}) | Shows a native button at the
bottom. mainButton.onClick(fn) runs fn when it is tapped. |
viewport.expand() | Turns a sheet into a full-screen page.
viewport.onChange(fn) runs fn with the new mode. |
openLink(url) | Asks the user, then opens an https link in the browser. |
close() | Closes the page. |
haptic(kind) | A vibration: light, medium, success or
error. |
on("back", fn) | Runs fn when the user presses back and your page goes back
in its own history. |
requestPayment({amount, currency, reference, description}) | Asks the user to pay your app
(scope payments): a native sheet the page cannot draw or change, then a signed
payment.completed event to your webhook. Resolves with {status: "completed" | "declined" |
"expired", paymentId}; rejects with scope_denied, not_enabled or
rate_limited (5 asks per launch). Amounts are strings; the reference is unique per user. See
Payments. Arrives with the next app release; until then the
method rejects with unsupported. |
Error codes: unsupported (no such method), scope_denied, rate_limited,
invalid_card, invalid_url, invalid_kind, token_failed,
origin_denied and failed. Each time the page is opened, it may ask for a token up to 10
times (getUser() may use one) and post up to 20 cards.
On the web version of Banga, apps open in a new browser tab without the bridge. Check for
window.banga and fall back to your own sign-in when it is missing.
Session tokens
requestAuthToken() returns a JSON Web Token, valid for 5 minutes and signed by the platform with
Ed25519. Send it to your backend and verify it there before trusting who the user is.
| Claim | Value |
|---|---|
iss | banga-apps |
aud | Your app's id. |
sub | The user's per-app id. |
scope | The scopes the user granted, separated by spaces. |
iat, exp | Issued and expiry times, in seconds. |
jti | A unique id for the token. |
The header is {"alg": "EdDSA", "typ": "JWT", "kid": ...}. The public keys are a standard JSON Web Key
Set at https://dev.banga.chat/api/apps/signing-keys, which you may cache for a day. Any JOSE library verifies the
token. In Node.js:
// npm install jose
import { createRemoteJWKSet, jwtVerify } from "jose";
const keys = createRemoteJWKSet(new URL("https://dev.banga.chat/api/apps/signing-keys"));
export async function verifySession(token) {
const { payload } = await jwtVerify(token, keys, {
issuer: "banga-apps",
audience: process.env.BANGA_APP_ID,
algorithms: ["EdDSA"]
});
return { userId: payload.sub, scopes: payload.scope ? payload.scope.split(" ") : [] };
}