“Stateless Edge Auth: PKCE OAuth, HttpOnly Cookies, and a VS Code Handshake on Cloudflare Functions”
The Constraint
No persistent server means there is no server-side session store. Authentication has to work with stateless edge workers (Cloudflare Functions) and an external identity provider (Supabase). This setup does not allow server-side session IDs, refresh token rotation with revocation lists, or any method that needs to write state to a database on every request.
The main tradeoff is that session length is limited to the JWT lifetime, which is one hour. This is fine for a developer tool, but not suitable for a consumer product that needs background sync.
Surface 1: Browser Modal
The main sign-in method is a self-contained component that can load on any page. It supports two flows:
Email/password: signInWithPassword() returns a session. The component sends a POST request to /api/auth/session with { action: 'set', access_token, expires_in } to set an HttpOnly cookie, then deletes the Supabase localStorage keys. localStorage is just used to pass the session; the cookie is the lasting credential. The token in localStorage only exists for about 600ms.
For OAuth, signInWithOAuth() with skipBrowserRedirect: true opens the provider in a new tab. This keeps the tool’s in-memory state in the original tab. When the provider redirects to the callback page, the callback sends gx:auth-complete using BroadcastChannel. The original tab listens for this, reads the session from localStorage, removes it, and sets the cookie using the same /api/auth/session endpoint.
async function handleOAuthReturn() { if (!oauthPending) return; oauthPending = false; const token = readStoredToken(); // reads Supabase localStorage if (!token) return; // Evict immediately: cookie is the real session, localStorage was the handoff for (let i = localStorage.length - 1; i >= 0; i--) { const k = localStorage.key(i); if (k?.startsWith('sb-') && k.endsWith('-auth-token')) localStorage.removeItem(k); } await onSignInSuccess(token); }
To sync across tabs, BroadcastChannel is used as the main signal since it works even if the tab is not focused. As a backup, visibilitychange is used in case the popup closes before it can broadcast.
Surface 2: Standalone Login Page
This page is used for direct navigation and for the VS Code handshake. There is no refresh token stored: the session object is checked before storage and the refresh_token field is removed. This means the session only lasts as long as the tab is open. The HttpOnly cookie is the lasting session. If someone steals an access token, it expires in one hour. If a refresh token is stolen, it never expires.
Surface 3: VS Code Extension
Extensions cannot use a standard OAuth redirect because it would cause the webview state to be lost. Instead, the handshake uses a polling pattern:
- Extension generates a state UUID and a secret (32 random bytes, hex-encoded).
- Extension POSTs to /auth/vscode with Authorization: Bearer
. Server creates a session row with state, hashed secret, and a 5-minute expiry. - Extension opens the browser login page with ?state=
&from=vscode. - User signs in. The callback page encrypts the access token with AES-GCM and writes it to the session row.
- Extension polls /auth/vscode-poll?state=
with Authorization: Bearer every 5 seconds. - On delivery, the edge function decrypts the token, returns it, and deletes the row.
The extension saves the returned token in the OS keychain using context.secrets.store(). This maps to macOS Keychain, Windows Credential Manager, or Linux SecretService, depending on the platform.
The Session Endpoint
All three surfaces converge on the same HttpOnly cookie gx_session, set and cleared by one endpoint:
// Set const cookie = gx_session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${maxAge};
// Clear const cookie = gx_session=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0;
JavaScript cannot read HttpOnly cookies, which is an important security feature. Because of this, logging out requires a server request: setting Max-Age=0 is the only way to expire the cookie. If you only clear localStorage on the client side, the server session will stay active until the JWT expires.
The PKCE Invariant
Every authentication client uses flowType: 'pkce'. This tells Supabase to follow RFC 7636 for all OAuth flows. The client creates a code_verifier, hashes it to make a code_challenge, and sends the challenge with the authorization request. The server checks the verifier during the token exchange. If someone intercepts the authorization code, it is useless without the verifier, which never leaves the browser.
What This Cannot Do
There is no automatic token refresh. Sessions expire after one hour, and the user must re-authenticate. The right solution would be a refresh endpoint that reads the HttpOnly cookie, verifies the refresh token server-side (not stored in the browser), and issues a new access token. But this requires a persistent store for refresh tokens, which goes against the no-persistent-server rule. This is the explicit tradeoff of this architecture.