Skip to article
PlanToCodeDocsGet the app

HandbookRelay & accounts

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

The browser parks the code at the regional server, and only the polling app can redeem it
The browser parks the code at the regional server, and only the polling app can redeem itTime runs from left to right. The app opens the browser at the regional server’s initiate-login, which stores a state key in Redis and redirects to Auth0. The app starts polling right away and gets 204 while you sign in. Auth0 sends the browser to the callback with the code. The callback consumes the state key and parks the code under the app’s polling ID, so a replayed callback gets 401. A poll reads the parked code without consuming it, so when a reply is lost the next poll gets the same code. The app redeems the code once at Auth0 with its verifier, then calls finalize-login, which returns the PlanToCode JWT and deletes the poll key. Nothing goes from the browser to the app.
Appdesktop or phone
Browser
Auth0
Regional APIkeys in Redis
state key
poll key
you sign in
pid · csrf
code · csrf, SET NX
polls from the start: 204 until the code is parked
opens initiate-login with challenge, pid, csrf
reply lost
callback with the code
consumed
code + verifier, once
finalize-login: JWT back
deleted
replayed callback: 401
The app gives up after at most 60 polls, about 2 to 2.5 minutes. A code parked later expires unused after 30 minutes.
Nothing goes from the browser to the app, and the verifier leaves the app only for Auth0.
  • 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.

Your device passes Auth0’s tokens to the server once and keeps a PlanToCode JWT
Your device passes Auth0’s tokens to the server once and keeps a PlanToCode JWTAt sign-in the app exchanges the code at Auth0 for an access token and a refresh token, posts both once to finalize-login, and keeps neither. The regional server stores the refresh token encrypted and exchanges it with Auth0 itself whenever a device refreshes. Each device keeps its own PlanToCode JWT, valid for 7 days and refreshable for 24 hours after it expires. The ChatGPT sign-in stays in the desktop’s profile folder, where the Codex child uses it for every agent turn. Phones receive only its name, email, plan, and usage limits over the relay.
Auth0
Your devices
Regional server
Auth0 tokens: used once, not kept
PlanToCode JWTkeychain or Keystore, one per device
ChatGPT sign-indesktop profile folder
Auth0 refresh tokenusers table, AES-256-GCM
OpenAI
access + refresh token, once
refresh token for new tokens, each refresh
finalize-login, once
new JWT, one per device
refresh with the JWT
PlanToCode JWT lifetime
valid 7 days
24 h grace: refresh still works
name, email, plan, limits
the relay passes them to phones
every agent turn, via the Codex child
  • 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 pointPurpose
GET /auth/auth0/initiate-loginBegin account login.
GET /auth/auth0/callbackAuth0 redirects the browser here. Park the code and send the browser to a confirmation page.
GET /auth0/poll-statusNon-consuming poll; 204 with Cache-Control: no-store until the code is parked.
POST /auth0/finalize-loginValidate 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-tokenRefresh 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.