# Calling HUB.KI from your backend (https://docs.hub.ki/custom-apps/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-the-token-can-do).

## What you get
```ts
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`](https://docs.hub.ki/custom-apps/manifest#network-access) 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.

```mermaid
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              |

```ts
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.

```mermaid
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)
```

```ts
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.
