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

  1. You create a signing key for your project (once) and store it on your backend as NOLAG_SIGNING_KEY.
  2. You create an actor for each of your users and store the actor's public keyId against your user record. The actor's access token itself is not needed for the browser flow, so you can discard it.
  3. 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.
  4. 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

  1. Log in to the NoLag Dashboard
  2. Navigate to your project
  3. Go to Settings then Signing Keys
  4. 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

FieldRequirement
Header algMust be HS256. Other algorithms are rejected.
Header kidYour signing key's public id (sk_live_...). Required.
Claim subThe actor's public keyId (at_live_...). Required. The actor must belong to the same project as the signing key.
Claim expRequired. 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:

  1. Create a new signing key in the dashboard.
  2. Deploy the new NOLAG_SIGNING_KEY to your backend.
  3. Disable the old key. Tokens minted with it stop verifying; users pick up tokens from the new key on their next refresh.
  4. Delete the old key once you are confident nothing still uses it.

Which Credential Where

CredentialLives onLifetimeUse for
Actor access token (at_live_...)Your servers, devices, agentsLong-livedBackend services, IoT, server-side SDK use
Signing key (sk_live_...)Your backend onlyUntil rotatedMinting client tokens
Client token (JWT)Browser or mobile appMinutesUntrusted client connections
API key (nlg_live_...)Your backend onlyLong-livedREST API management calls

Next Steps