docs
/
Client Integration

Authentication

Application credentials, the tenant header, and how a signed-in visitor's identity is carried to AppEngine without exposing anything to the browser.

There are two identities in almost every request, and keeping them distinct is the whole model:

IdentityWho it isHeader
The applicationYour server, acting as itselfAuthorization: Bearer <jwt> or apiKey
The visitorThe customer using your sitex-client-authorization

Your server always authenticates as the application. When a visitor is signed in, their token rides alongside as x-client-authorization — so AppEngine knows which application is asking and on whose behalf.

The visitor's token never becomes the Authorization header on a server-to-server call, and the application's token never reaches the browser. They are separate credentials with separate lifetimes.

See AppEngine → Conventions for the full header table.

The tenant

Every request needs orgid. It is the tenant, and without it AppEngine rejects the call:

Organization ID is required. Pass orgid as a header, query param, or body field.

For multi-domain hosting — one deployment serving many customer domains — the tenant is resolved from the domain instead, using domainAsOrg: true, shared-org-id and x-client-host.

Application tokens

A server-side client fetches an application token once and reuses it:

async getHeaderWithToken() {
  if (!this.token) {
    this.token = await this.getAppToken();
  }
  const init = this.getBaseHeader();
  init.headers['Authorization'] = `Bearer ${this.token}`;
  return deepCopy(init);
}

Because the token is cached for the life of the process, expiry has to be handled on the way back out. On a 401 the client clears the token, re-authenticates and replays the request once:

catch (err) {
  if (this.renewTries < 2 && err?.response?.status === 401) {
    this.token = null;
    await this.getHeaderWithToken();
    return await this.processRequestServer(method, path, data, /* … */);
  }
  throw err;
}

A cached token is a per-process liability. If a token goes bad in a way that is not a 401 — a 403, a network fault during startup, a malformed response — the retry never triggers and that process stays unable to reach AppEngine for its entire lifetime, while still answering requests. Make the failure loud: return 503 from your own health check rather than serving a degraded page with a 200.

Client telemetry

x-client-info carries a JSON blob describing the visitor's request — host, protocol, referrer, user agent, language, timezone, screen size, device id. When it is present alongside an orgid, AppEngine writes a web_visit record fire-and-forget.

Build it on the server from the incoming request headers. It is plain header parsing — no network, no lookups:

const clientInfo = await getRequestInfo(request.headers);
clientInfo.timezone = request.headers.get('x-client-timezone');

// Prefer origin/referer for the real visitor-facing domain — the browser
// sets these, and behind a proxy `host` may be an internal service name.
const origin = request.headers.get('origin');
if (origin) clientInfo.host = new URL(origin).host;
In a mobile app the shape is different

There is no origin to proxy through, so the app authenticates as an application and carries the person's token itself. The Flutter client handles both tokens for you — including the sign-in that comes back asking for a verification code.

Signing a visitor in

The visitor's session is established against your own origin, not AppEngine:

POST /api/user/customer/login      →  your server  →  AppEngine

Your server stores the resulting token where the browser cannot read it as a bearer credential, and attaches it as x-client-authorization on subsequent calls. The browser only ever holds a session with you.

Never put a token in a URL. Query strings are logged by proxies, kept in browser history, and leak through Referer on every outbound link.