Appearance
Device authorization ​
Use this flow for a CLI or device that cannot complete a browser sign-in itself. Prerequisites: the deployment's Platform API and Supabase auth URLs, and a user who can sign in to the portal. Examples use the Supabase client from Quickstart.
Issue a code and request approval ​
createDeviceCode is public and limited to 3 requests per minute per IP:
ts
const response = await fetch(`${apiBaseUrl}/v1/public/device/code`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ client_name: "Example CLI" }),
});
if (!response.ok) throw new Error(await response.text());
const code = await response.json();
console.log(code.verification_uri_complete, code.user_code);The code expires after 900 seconds and starts with a 5-second polling interval. Display only the user code and verification URI. Keep device_code private on the requesting device. The signed-in user approves, denies or inspects the request using respondDeviceAuthorization.
Poll, then exchange the hash ​
Expected HTTP 400 polling responses are part of the flow, so handle them directly rather than using a helper that throws on every non-2xx status:
ts
let interval = code.interval;
const deadline = Date.now() + code.expires_in * 1000;
let session;
while (Date.now() < deadline) {
await new Promise(resolve => setTimeout(resolve, interval * 1000));
const response = await fetch(`${apiBaseUrl}/v1/public/device/token`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: code.device_code,
}),
});
const result = await response.json();
if (response.status === 503 && result.error === "panic_mode") {
throw new Error("Maintenance: restart authorization after the retry delay");
}
if (response.ok) {
const verified = await supabase.auth.verifyOtp({
token_hash: result.token_hash, type: "magiclink",
});
if (verified.error) throw verified.error;
session = verified.data.session;
break;
}
if (result.error === "authorization_pending" || result.error === "slow_down") {
interval = result.interval ?? interval;
continue;
}
if (result.error === "server_error") continue;
throw new Error(result.error);
}
if (!session) throw new Error("Authorization expired; request a new code");Success returns a single-use token_hash, not an access token. Exchange it within its returned 60-second lifetime. slow_down can increase the interval; the per-IP polling guard can also return it. Denied, expired, consumed or invalid codes should stop the loop rather than retry forever. A transient network failure can be retried within the deadline, but keep the polling interval.
Inspect/revoke your device authorizations through listDeviceAuthorizations and revokeDeviceAuthorization. Revocation is separate from stopping a game session.