Tabs & Spaces Developers

Build

Checking who's looking

Your page gets a token: a short-lived JWT, signed by the deployment that runs the workspace. Your server verifies it, and only then trusts who's asking.

With the SDK

ts
import { fetchKeys, verifyToken } from "@tabs-and-spaces/sdk/server";

const WORKSPACE = "https://chat.acme.com"; // the deployment you serve
const keys = await fetchKeys(WORKSPACE); // cache these

export async function whoIsAsking(request: Request) {
  const token = request.headers.get("authorization")?.replace(/^Bearer /, "") ?? "";
  const { context } = await verifyToken(token, {
    keys,
    audience: "https://crm.example.com", // your app's origin
    issuer: WORKSPACE,
  });
  return context; // user, workspace, space, tab, app, locale
}

verifyToken() checks that the token:

  • is signed with one of keys;
  • is meant for your app (its audience is your origin);
  • hasn't expired;
  • comes from issuer, if you pass one.

If any check fails, it throws a TokenError. Answer 401.

It uses Web Crypto's Ed25519. That works in Node 22 and later, Deno, Bun, Cloudflare Workers and modern browsers.

Decide which workspaces you serve

If you serve one deployment, keep its address in your configuration. If you serve many (you sell your app to several companies), keep a list. For each request:

  1. read the token's iss without trusting it;
  2. look it up in your list;
  3. verify with that deployment's keys and issuer.

Keys

A deployment's public keys are at /api/apps/keys on its address:

json
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "jR2Z5Hz…",
      "kid": "q3Xr9tL2…",
      "alg": "EdDSA",
      "use": "sig"
    }
  ]
}

They're the same for every workspace on a deployment, and they change only when its owner changes its secret. Cache them. If a token names a key id (kid) you don't have, fetch the keys again, once, before refusing it.

Without the SDK

Any JWT library that supports EdDSA (Ed25519) can verify the token. Check:

  • alg is EdDSA, and kid is one of the deployment's keys;
  • aud is your origin;
  • iss is a deployment you serve;
  • exp is in the future.

See Tokens for every claim.

Tokens are short-lived

A token lasts ten minutes. In your page, call token() before each request rather than keeping one: it returns the current token, or fetches a fresh one near expiry. Don't store tokens, and don't use them as sessions. If your app needs its own session, start it after verifying a token, and keep it short.