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.onesubdomain. For the Electron desktop app see Desktop integration.
Prerequisites
- Subdomain on
xcity.one— your app must be served fromhttps://<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. - HTTPS in production — the session cookie has
Secure, so cookies will not attach to plain http hosts in production. - 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 get401. - 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
Both use the same useXcityIdentity() hook — copy it.
Last updated: