Client Tokens
Actor access tokens are long-lived credentials. They are the right tool for servers, agents, and devices you control, but they should never be shipped to a browser: anyone who opens devtools can read the token and reuse it until you revoke it.
Client tokens solve this. Your backend mints a short-lived JWT (signed with a project-level signing key) and hands it to the browser. The JWT points at an actor; all permissions still resolve from that actor on the NoLag side. If a client token leaks, it expires on its own within minutes.
How It Works
- You create a signing key for your project (once) and store it on your backend as
NOLAG_SIGNING_KEY. - You create an actor for each of your users and store the actor's public
keyIdagainst your user record. The actor's access token itself is not needed for the browser flow, so you can discard it. - When a user opens your app, your backend mints a JWT: signed with the signing key, naming the user's actor
keyId, expiring in a few minutes. - The browser connects with that JWT. NoLag verifies the signature, resolves the actor, and applies exactly the same permissions the actor would have with its access token.
No NoLag API call is needed to mint a token. Signing happens entirely on your backend with a standard JWT library.
Creating a Signing Key
- Log in to the NoLag Dashboard
- Navigate to your project
- Go to Settings then Signing Keys
- Create a key and copy it (shown only once on creation)
The key has the format sk_live_<keyId>.<secret>, the same shape as other NoLag credentials. The part before the dot is public and goes in the JWT header as kid. The part after the dot is the HS256 signing secret. Store the whole string as NOLAG_SIGNING_KEY on your backend and never expose it to a client.
Signing keys can also be managed with the REST API at /v1/signing-keys.
Creating an Actor per User
Create an actor when a user signs up (or lazily on first use) and save only the public keyId on your user record:
const response = await fetch('https://api.nolag.app/v1/actors', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOLAG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: `User ${user.id}`,
actorType: 'user',
metadata: { userId: user.id },
}),
})
const actor = await response.json()
// Save actor.keyId (e.g. "at_live_abc123def456") on your user record.
// The accessToken is not needed for the browser flow; discard it.
await db.users.update(user.id, { nolagKeyId: actor.keyId })
The keyId is a public identifier, safe to store in your database without encryption. Actor management endpoints (get, update, delete) accept the keyId in place of the actor UUID, so it is the only value you need to keep.
Minting a Client Token
Mint a fresh token whenever your frontend asks for one. Use any standard JWT library:
import jwt from 'jsonwebtoken'
const [kid, secret] = process.env.NOLAG_SIGNING_KEY.split('.')
app.get('/api/nolag-token', requireLogin, (req, res) => {
const token = jwt.sign(
{ sub: req.user.nolagKeyId },
secret,
{ algorithm: 'HS256', keyid: kid, expiresIn: '15m' },
)
res.json({ token })
})Token Rules
| Field | Requirement |
|---|---|
Header alg | Must be HS256. Other algorithms are rejected. |
Header kid | Your signing key's public id (sk_live_...). Required. |
Claim sub | The actor's public keyId (at_live_...). Required. The actor must belong to the same project as the signing key. |
Claim exp | Required. At most 1 hour in the future. We recommend 15 minutes. |
Clock skew of up to 60 seconds is tolerated on exp. Any token that fails a check is rejected with a generic authentication error.
Connecting from the Browser
Pass a token provider function to the JS SDK instead of a token string. The SDK calls it on every connect and reconnect, so each attempt uses a freshly minted token:
import { NoLag } from '@nolag/js-sdk/browser'
const client = NoLag(async () => {
const res = await fetch('/api/nolag-token')
const { token } = await res.json()
return token
})
await client.connect()
With a token provider, the SDK also refreshes proactively: shortly before the token expires it mints a fresh one and renews the credentials over the live connection, with no disconnect and no resubscribe. Against older brokers (or on transient failures) it falls back to a quick reconnect where the server restores all subscriptions. Your application code does not need to handle expiry either way.
If a connection does outlive its token (for example the provider became unreachable), the server closes it with code 4003 and reason token_expired. The SDK reacts by minting a fresh token and reconnecting immediately.
Revoking Access
- Revoke one user: delete or disable their actor (by
keyId) via the dashboard or REST API. New client tokens naming that actor stop working immediately, and any live connection is disconnected by the periodic revalidation check. - Token expiry: a leaked client token is only useful until its
exp, at most 1 hour and typically 15 minutes. - Disable a signing key: stops all new tokens signed with it from verifying. Connections already established run until their token expires.
Rotating a Signing Key
Multiple signing keys can be active at once, so rotation has no downtime:
- Create a new signing key in the dashboard.
- Deploy the new
NOLAG_SIGNING_KEYto your backend. - Disable the old key. Tokens minted with it stop verifying; users pick up tokens from the new key on their next refresh.
- Delete the old key once you are confident nothing still uses it.
Which Credential Where
| Credential | Lives on | Lifetime | Use for |
|---|---|---|---|
Actor access token (at_live_...) | Your servers, devices, agents | Long-lived | Backend services, IoT, server-side SDK use |
Signing key (sk_live_...) | Your backend only | Until rotated | Minting client tokens |
| Client token (JWT) | Browser or mobile app | Minutes | Untrusted client connections |
API key (nlg_live_...) | Your backend only | Long-lived | REST API management calls |