Skip to content

Minting session tokens with the backend SDK

The backend SDK is how your server turns a project’s secret key into a session token for each person using your app. It is one small client with three calls, and it is the only piece of GenUI Portal code that ever sees the secret key.

Your application has two tiers, and each gets its own SDK. The backend SDK runs on your server, holds the secret key, and mints session tokens. The frontend SDK runs in your app, holds a session token, and sends turns. The secret key never crosses that line, so the backend SDK is a server-only package: keep it on your server and never bundle it into your client app.

Three calls cover the whole job.

  • Mint an anonymous token for a person who is not signed in. The first call starts a new anonymous identity and an A/B arm. Pass both back on later calls, and the same device keeps the same identity and the same arm across launches.
  • Mint a user token for a signed-in person, naming them with a stable user id of your choosing. The platform never learns anything else about them.
  • Refresh a token before it expires, for the same person, keeping their arm. Tokens live 30 minutes.

Every call returns the token string plus the values your frontend must persist: the assignment id, and for anonymous users the anonymous subject. See Authentication for how keys and tokens fit together.

  1. Create a secret key on your project’s Connect page and store it where only your server can read it, such as a secret manager or an environment variable. Copy the organization id from the same page.

  2. Construct one client at startup with the organization id and the secret key. It talks to the GenUI Portal production API by default, so no endpoint is needed; pass a baseUrl only to reach another environment. Construct it once and reuse it. No call takes the secret as an argument. The client refuses a key that does not start with gnp_sk_, so a mixed-up environment variable fails at startup rather than on the first request.

    import 'dart:io';
    import 'package:genui_portal_backend/genui_portal_backend.dart';
    final client = GenUiPortalClient(
    organizationId: 'org_…',
    secretKey: Platform.environment['GENUI_SECRET_KEY']!,
    );
    import { GenUiPortalClient } from '@genui-portal/backend';
    const client = new GenUiPortalClient({
    organizationId: 'org_…',
    secretKey: process.env.GENUI_SECRET_KEY!,
    });
  3. Mint a token when a person arrives. Put this behind an endpoint of your own that your frontend calls, and apply your own authentication to it first. For a signed-in person, the subject is their id in your system.

    final token = await client.mintUserToken(
    subject: user.id,
    assignmentId: request.priorAssignmentId, // null on first contact
    );
    // token.token, token.assignmentId, token.expiresAt (computed for you from the platform's expiresIn)
    const token = await client.mintUserToken({
    subject: user.id,
    assignmentId: request.priorAssignmentId, // undefined on first contact
    });

    For someone who is not signed in, mint an anonymous token, passing back whatever the frontend persisted last time.

    final token = await client.mintAnonymousToken(
    anonymousSubject: request.priorAnonymousSubject,
    assignmentId: request.priorAssignmentId,
    );
    // token.anonymousSubject and token.assignmentId: send both to the frontend to persist
    const token = await client.mintAnonymousToken({
    anonymousSubject: request.priorAnonymousSubject,
    assignmentId: request.priorAssignmentId,
    });
  4. Return the token to your frontend along with the assignment id and, for anonymous users, the anonymous subject. Your frontend passes the token on every turn and persists the other two.

  5. Refresh before expiry. When your frontend reports a token is about to expire, or a turn came back unauthorized, mint a fresh one for the same person. Pass the token object you got back last time, and the SDK re-mints for the same subject or anonymous subject with the same arm.

    final fresh = await client.refreshToken(token);
    const fresh = await client.refreshToken(token);

The SDK makes exactly one request per call and never retries on its own. If a call fails, your handler decides whether to try again, so a burst of failures cannot turn into a burst of mints.

Every failure is a typed error with a stable code, so your handler can branch without parsing messages. In Dart they are subtypes of GenUiPortalClientException, sealed so a switch covers every case. In TypeScript they are instances of GenUiPortalClientError with a code field. The secret key never appears in an error message.

  • invalid_secret_key (HTTP 401). The key is wrong, was rotated more than 24 hours ago, or belongs to a different organization than the endpoint. Check the key and the organization id on Connect. Retrying will not help.
  • anonymous_rate_limited (HTTP 429). This project’s cap on anonymous mints for the current window was reached. Signed-in mints are never capped. Back off and fall back to your static experience for that user.
  • invalid_request (HTTP 400). Usually a missing subject on a user mint, or a malformed organization id. This is a bug in the calling code, not a runtime condition.
  • unavailable (HTTP 5xx). The platform could not mint. Usually transient. Your handler may retry with backoff.
  • network. The request never got an answer: connection refused, reset, or timed out. The SDK’s default timeout is 10 seconds and is configurable when you construct the client.

Two things the SDK will not do, on purpose. It will not read your environment or a config file for the secret; you pass it in. And it is server-only: the Dart package imports dart:io, so a Flutter web build that imports it fails to compile, and the Node package is marked server-only in its exports, so a bundler refuses it for the browser. Keep it on your server and never bundle it into your client app.