HUB.KI DocsCustom apps
HUB.KI Docs
Custom apps

Calling HUB.KI from your backend

Use the app's scoped token to authenticate the signed-in user to your own backend.

View as Markdown

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();
FieldUse
iamUrlBase URL of the identity service — its JWKS verifies the token.
organizationIdThe active organization.
tokenA short‑lived, app‑scoped bearer token (see below).
coreUrlBase 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 Origin is null. The sandbox has no stable origin, so your backend must CORS-allow Origin: null (e.g. reply Access-Control-Allow-Origin: null). Use bearer tokens, not cookies — credentialed requests can't use a null allow-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:

ClaimExpected value
token_usecustom_app
audurn:hub:custom-app:<organizationId>:<appId>
app_idyour app's appId
subthe user's stable id
tenant_idthe organization id
scopespace‑delimited scopes the org approved for your app
email, namepresent 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/aud are 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 / getToken when you need one.
  • api:access is opt‑in: an organization admin must approve it before getConnection returns 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.