# The token

> The signed JWT Facia gives your app. Every claim, how long it lasts, and how to verify it in Node, Python, Go and PHP.

# The token

When a client opens your app's tab, Facia tells your page who they are with a **JSON Web Token**, signed with your **app secret** using HMAC-SHA256 (`HS256`).

## How your page gets it

1. **In the address**: Facia loads your page as `https://your-app.example.com/?facia_token=<jwt>`.
2. **By message**: after your page says it's ready, Facia posts `{ type: "facia:token", token }`, and again whenever it makes a fresh one.

`Facia.token()` in the [bridge script](https://docs.facia.chat/build/bridge) returns the latest one either way.

## Claims

```json
{
  "iss": "facia",
  "aud": "<your app id>",
  "iat": 1760599700,
  "exp": 1760600000,
  "biz": "acme",
  "sub": "cl_7f3aQ2xY9pLm0KdE",
  "alias": "Silver Fox",
  "tier": "vip",
  "level": 12,
  "coins": 240,
  "role": "client",
  "scheme": "dark",
  "lang": "en-US"
}
```

| Claim | Meaning |
| --- | --- |
| `iss` | Always `facia` |
| `aud` | Your app id. Check it, so a token for another app can't be replayed at yours |
| `iat`, `exp` | Issued at and expiry, in seconds. **Tokens last 5 minutes** |
| `biz` | The business's handle (or its id if it has none). A [listed app](https://docs.facia.chat/build/listing) installed by several businesses tells them apart by this |
| `sub` | The client's id **at this business**: `cl_` + 16 characters. Stable, so key your data by it (with `biz`). It's random and means nothing anywhere else, so two businesses can't link the same person |
| `alias` | The client's random two-word name at this business, the same one other clients see in groups. For staff, it's their display name |
| `tier` | `guest`, `verified`, `vip` or `spam`. Staff read as `verified` |
| `level` | Their [level](https://docs.facia.chat/business/levels-coins). `0` when levels are off |
| `coins` | Their balance when the tab opened. `0` for guests. Use [`coins.balance`](https://docs.facia.chat/build/partner-api#coins-balance) for a live number |
| `role` | `client` or `team` (owner, admin, manager or agent looking at your tab) |
| `scheme` | `light` or `dark`, what their device shows right now |
| `lang` | The device language, e.g. `en-US` |

## Verify it on your server

**Never trust the token in the browser.** The browser doesn't have the secret. Send the token to your server, verify it there, and start your own session if you need one longer than 5 minutes.

Check all four: the signature with your secret, the algorithm `HS256`, the issuer `facia`, and the audience = your app id. Expiry is checked automatically by these libraries.

### Node

```js
import jwt from "jsonwebtoken";

const claims = jwt.verify(token, process.env.FACIA_APP_SECRET, {
  algorithms: ["HS256"],
  issuer: "facia",
  audience: process.env.FACIA_APP_ID,
});
```

### Python

```python
import jwt, os  # pip install pyjwt

claims = jwt.decode(
    token,
    os.environ["FACIA_APP_SECRET"],
    algorithms=["HS256"],
    issuer="facia",
    audience=os.environ["FACIA_APP_ID"],
)
```

### Go

```go
import "github.com/golang-jwt/jwt/v5"

claims := jwt.MapClaims{}
_, err := jwt.ParseWithClaims(token, claims,
    func(t *jwt.Token) (any, error) { return []byte(os.Getenv("FACIA_APP_SECRET")), nil },
    jwt.WithValidMethods([]string{"HS256"}),
    jwt.WithIssuer("facia"),
    jwt.WithAudience(os.Getenv("FACIA_APP_ID")),
)
```

### PHP

```php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;  // composer require firebase/php-jwt

$claims = JWT::decode($token, new Key(getenv('FACIA_APP_SECRET'), 'HS256'));
if ($claims->iss !== 'facia' || $claims->aud !== getenv('FACIA_APP_ID')) throw new Exception('bad token');
```

### Cloudflare Workers / Deno / Bun (no library)

```js
async function verifyFacia(token, secret, appId) {
  const [h, p, s] = token.split(".");
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  const b64 = (x) => Uint8Array.from(atob(x.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
  const ok = await crypto.subtle.verify("HMAC", key, b64(s), new TextEncoder().encode(`${h}.${p}`));
  const c = JSON.parse(new TextDecoder().decode(b64(p)));
  if (!ok || JSON.parse(atob(h)).alg !== "HS256" || c.iss !== "facia" || c.aud !== appId || c.exp * 1000 < Date.now())
    throw new Error("bad token");
  return c;
}
```

## Good practice

- **Key everything by `biz` + `sub`.** Never by alias: aliases can change.
- **Don't store the token.** Verify it, then use your own session cookie.
- **Re-check coins on the server** with [`coins.balance`](https://docs.facia.chat/build/partner-api#coins-balance) before something expensive. The `coins` claim is a snapshot.
- **Handle `role: "team"`**, for example by showing staff tools or skipping coin charges for staff testing.
- **Respect `tier`.** If the business set the app to Verified only, guests won't see it at all, but check anyway before giving rewards.
