Integrate a browser sub-product

Wire any *.xcity.one app into the unified identity, plan, and inference flow.

How to wire a new browser sub-product (anything on *.xcity.one) into the xcity-home identity + tokenhub flow.

Scope: this guide covers browser sub-products only — apps that run inside a normal Chromium / Firefox / Safari window on a *.xcity.one subdomain. For the Electron desktop app see Desktop integration.

Prerequisites

  1. Subdomain on xcity.one — your app must be served from https://<your-app>.xcity.one. Anything else needs an explicit allowlist entry in xcity-home env (XCT_CORS_EXTRA_ORIGINS=https://your-other-host.com) and a security review.
  2. HTTPS in production — the session cookie has Secure, so cookies will not attach to plain http hosts in production.
  3. User has a Xcity account — first-time visitors get redirected to xcity-home/login; we don’t ship a per-product signup form.

Step 1 — fetch the identity envelope

async function getXcityIdentity() {
  const res = await fetch('https://www.xcity.one/api/me/litellm-key', {
    credentials: 'include',
  });
  if (res.status === 401) {
    window.location.href = 'https://www.xcity.one/login?return=' +
      encodeURIComponent(window.location.href);
    return null;
  }
  return res.json() as Promise<{
    key: string;
    plan: string;
    models: string[];
    api_base: string;
  }>;
}

That single call gives you the bearer, the user’s plan, the models they can hit, and the gateway base URL.

Step 2 — make inference calls

const id = await getXcityIdentity();
if (!id) return;

const res = await fetch(`${id.api_base}/chat/completions`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${id.key}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: id.models[0],
    messages: [{ role: 'user', content: 'Hello' }],
  }),
});

Step 3 — react to plan changes

The plan field returned by /api/me/litellm-key is fresh-on-load. For long-lived sessions, re-fetch it on focus or every 5 minutes. A plan downgrade will start returning 403 from /chat/completions for models you used to have access to — handle it gracefully by re-fetching models and updating your UI.

Common mistakes

  • Forgetting credentials: 'include'. Without it the browser doesn’t send the session cookie and you’ll always get 401.
  • Hardcoding api_base. Always read it from the identity envelope. We will move the gateway hostname in the future.
  • Caching the bearer. It’s short-lived (rotated when the user revokes or plan changes). Re-fetch on 401.
  • Calling Stripe / GoTrue directly. Don’t. Sub-products own no shared secrets.

Reference implementations

  • xct-chat — the chat sub-product, MIT licensed.
  • xct-flow — agent workflow builder.

Both use the same useXcityIdentity() hook — copy it.

Last updated: