Facia Docs
For AI Open Facia
Build on Facia Beta

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 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"
}
ClaimMeaning
issAlways facia
audYour app id. Check it, so a token for another app can't be replayed at yours
iat, expIssued at and expiry, in seconds. Tokens last 5 minutes
bizThe business's handle (or its id if it has none). A listed app installed by several businesses tells them apart by this
subThe 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
aliasThe client's random two-word name at this business, the same one other clients see in groups. For staff, it's their display name
tierguest, verified, vip or spam. Staff read as verified
levelTheir level. 0 when levels are off
coinsTheir balance when the tab opened. 0 for guests. Use coins.balance for a live number
roleclient or team (owner, admin, manager or agent looking at your tab)
schemelight or dark, what their device shows right now
langThe 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 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.
View as Markdown