Calling HUB.KI from your backend
Use the app's scoped token to authenticate the signed-in user to your own backend.
Some apps need a server of their own — to run privileged logic, hold secrets, or
integrate a third‑party service. With the api:access scope, your app can hand
its backend a short‑lived token that proves who the signed‑in user is, which
organization they're in, and which scopes they granted your app.
This token is scoped to your app — it is not the user's HUB.KI session token and cannot act as the user across HUB.KI. It is an identity + scope assertion for your backend to verify. See What the token can do.
What you get
const { coreUrl, iamUrl, organizationId, token } = await hub.getConnection();| Field | Use |
|---|---|
iamUrl | Base URL of the identity service — its JWKS verifies the token. |
organizationId | The active organization. |
token | A short‑lived, app‑scoped bearer token (see below). |
coreUrl | Base URL of the HUB.KI core API. |
Reaching your backend
The sandbox blocks all network requests by default. Before your app can fetch
its backend, list that backend's origin in the manifest's
connectOrigins and have an
organization admin approve it — otherwise the request is blocked by the app's
Content-Security-Policy.
Two more things to know about requests from the sandbox:
- The request's
Originisnull. The sandbox has no stable origin, so your backend must CORS-allowOrigin: null(e.g. replyAccess-Control-Allow-Origin: null). Use bearer tokens, not cookies — credentialed requests can't use anullallow-origin. - Approved origins are for
fetch, not navigation. Only declared, approved origins are reachable; treat approval as the trust boundary for where the app may send data.
The flow
The app sends the token to your backend; your backend verifies it against HUB.KI's identity service and now trusts the user's identity and granted scopes.
sequenceDiagram
participant App as App (iframe)
participant Host as HUB.KI host
participant Backend as Your backend
participant Iam as HUB.KI identity
App->>Host: hub.getConnection()
Host-->>App: iamUrl, organizationId, token
App->>Backend: your request + token + organizationId
Backend->>Iam: fetch JWKS (cached)
Backend->>Backend: verify signature, aud, token_use, exp
Backend-->>App: result (now trusts user + scopes)Typically you validate the token once to establish your own session (a cookie or your own JWT), then serve subsequent requests from that — the HUB.KI token is a bootstrap credential, not your per‑request session.
Validating the token
Verify the JWT against the identity service's JWKS at ${iamUrl}/api/auth/jwks,
then check its claims:
| Claim | Expected value |
|---|---|
token_use | custom_app |
aud | urn:hub:custom-app:<organizationId>:<appId> |
app_id | your app's appId |
sub | the user's stable id |
tenant_id | the organization id |
scope | space‑delimited scopes the org approved for your app |
email, name | present only if user:read was granted |
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL(`${iamUrl}/api/auth/jwks`));
const { payload } = await jwtVerify(token, jwks, {
audience: `urn:hub:custom-app:${organizationId}:${appId}`,
});
if (payload.token_use !== "custom_app") throw new Error("wrong token type");
// payload.sub, payload.scope, payload.email are now trusted.What the token can do
- It authenticates the user to your services and tells you which scopes the org approved. That's its job.
- It cannot act as the user on HUB.KI's own APIs. Its
token_use/audare app‑scoped, so HUB.KI's user‑facing endpoints reject it. This is deliberate: a leaked app token can't be replayed against the user's account — it only carries the identity + scopes you already see.
Keeping the token fresh
Tokens are short‑lived. The host mints a fresh one before expiry and pushes each new value — subscribe once and always hold a current token instead of polling.
sequenceDiagram
participant Host as HUB.KI host
participant App as App (iframe)
App->>Host: hub.onToken(handler)
Host-->>App: current token (immediately)
Note over Host: ~60s before expiry
Host->>Host: mint a fresh app token
Host-->>App: hub/token/changed (new token)let token = await hub.getToken();
hub.onToken((next) => {
token = next; // send the fresh token to your backend before it expires
});Security
- The token is app‑scoped and short‑lived — treat it as a credential; never log it or expose it to third parties.
- Don't persist it. Re‑read it from
onToken/getTokenwhen you need one. api:accessis opt‑in: an organization admin must approve it beforegetConnectionreturns a token.- If your backend needs to call HUB.KI while the user is offline, this browser‑delivered token cannot help — that requires a separate server‑to‑server credential, which isn't part of this flow.