Accounts, auth, and regions
The Auth0 PKCE handoff through Redis, the PlanToCode JWT and its refresh, what a region change invalidates, how ChatGPT profiles relate to transcripts, and which data leaves your computer.
Checked against the source on 17 September 2026
On this page
How the native sign-in gets back into the app
- State keyauth0:state:{state}
Written by initiate-login with the polling ID and CSRF token, and deleted by the callback as it parks the code. A replayed callback finds nothing and gets 401.
- Poll keyauth0:poll:{pid} · SET NX
Polls read it without consuming it, so after a lost reply the next poll gets the same code. finalize-login deletes it after issuing the JWT, and both keys expire after 30 minutes.
- PollingGET /auth0/poll-status
Phones poll every 2 seconds, up to 60 times. The desktop polls every 2 seconds for about a minute, then every 3, and stops after 60 polls or 150 seconds.
- Verifiercode_verifier
Created with the polling ID and kept in app memory. Only Auth0’s token endpoint receives it, once, so a parked or intercepted code is useless without it.
- Access tokenfinalize-login · auth0_id_token
The app gets it from its own code exchange and posts it once to finalize-login, in a field still named auth0_id_token. The server validates it, fetches userinfo with it, and stores it nowhere.
- Refresh tokenusers.auth0_refresh_token
Passes through the app once. The server stores it encrypted, exchanges it with Auth0 using its own client credentials, and keeps the rotated token even when the new tokens then fail validation.
- PlanToCode JWTJWT_ACCESS_TOKEN_DURATION_DAYS = 7
Issued per device with a device_id claim. Up to 24 hours after it expires, refresh-app-token still trades it for a new one. After that you sign in again.
- ChatGPT sign-incodex --auth-home
Created by the Codex child’s own login and kept in the desktop profile folder it reads. Phones get the profile’s name, email, plan, and usage limits over the relay, never the sign-in itself.
| HTTP entry point | Purpose |
|---|---|
| GET /auth/auth0/initiate-login | Begin account login. |
| GET /auth/auth0/callback | Auth0 redirects the browser here. Park the code and send the browser to a confirmation page. |
| GET /auth0/poll-status | Non-consuming poll; 204 with Cache-Control: no-store until the code is parked. |
| POST /auth0/finalize-login | Validate the Auth0 token and client ID, require a verified email, issue the PlanToCode JWT, grant first-login credits, then delete the poll key. |
| POST /api/auth0/refresh-app-token | Refresh the PlanToCode app token. The only route that accepts an expired token. |
Consuming the state key and deleting the poll key each run as one Lua script in Redis. The store is Redis rather than memory because every API process in the region shares it.
The PlanToCode JWT is HS256 with a shared secret, issuer plantocode, audience plantocode-api, and scope read write rpc. Its jti is checked against a revocation table on every request and again at the WebSocket upgrade, its device_id claim is compared to the X-Device-ID header, and it lives 7 days by default. The refresh route accepts a token for up to 24 hours after expiry, with issuer, audience, device binding, scopes, and revocation still enforced, runs in a per-user serialized transaction, and refuses to mint a token if the Auth0 subject or email no longer matches the stored user. The WebSocket upgrade captures a per-user registration fence before its revocation check, so a logout that completes between the HTTP check and relay registration invalidates the registration.
The server’s optional server-client credential pair is needed for server-side Auth0 refresh exchange. Configure the native app’s Auth0 domain, native client ID, audience, and callback settings together. The selected API region and the iOS authServerURL are distinct configuration paths, so verify both when creating a local environment.
What a region change or sign-out invalidates
Changing the region or signing out ends the previous attempt and clears its credentials. A late result from an old attempt cannot restore an account, because every result is bound to the attempt and the origin that started it.
Desktop login, logout, token refresh, and session reconciliation share one mutation lock, and reconciliation discards results when the token or origin changed during a request. Callback replay is rejected, and handoff acknowledgement is idempotent.
Local execution still uses remote services
Tools and file edits run on the desktop, and a model request still sends the task context to OpenAI. Mobile control sends requests and the returned workspace content through the regional server. Dictation sends a recording to the transcription service you configured, and the agent’s Gemini tools send text or video to Gemini under your key.
So “desktop-owned” describes where execution and files live. It is neither an offline mode nor an end-to-end encryption claim.