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
- In the address: Facia loads your page as
https://your-app.example.com/?facia_token=<jwt>. - 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
{
"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 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. 0 when levels are off |
coins | Their balance when the tab opened. 0 for guests. Use 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
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
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
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
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)
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.balancebefore something expensive. Thecoinsclaim 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.