Engineering Journal
Ginexys
Ginexys

The Export Gate: Why We Moved Auth to the Download Moment

2026-06-02

TLDR

We moved the auth gate from feature access (run counter) to the export moment (download button). A shared GxAuth modal component replaced three separate redirect-based auth flows. The result: users get full tool access free, sign-in fires when they are ready to save their work.

Repo: tools/pdf-processor

The Old Model

The analyze gate fired before running the extraction pipeline. A guest counter tracked how many times a user had run analysis. On hitting the limit, a paywall overlay replaced the canvas.

The problem: the gate fired at usage, not at value capture. A user who ran analysis three times and found exactly what they needed left without ever being asked to sign in. A user who ran analysis twice on a document they wanted to keep got blocked before they could extract value. The gate was in the wrong place.


The Export Gate Model

Gate at the download moment. Every export button in every tool checks auth before writing the file. Users run tools freely. The sign-in prompt appears when they click Export, Download, or Save.

// GxGate.onExportClick
async function onExportClick(e, originalHandler) {
  const result = await checkAuth();
  if (!result.signedIn) return; // swallow the action
  originalHandler(e);
  incrementExportCount(toolKey); // star toast on milestone
}

The gate is invisible during tool use. It appears exactly once, at the moment when the user has demonstrated enough intent to export something.


Three Auth Paths

The checkAuth() function handles three environments:

Inside the OS shell: tools run in iframes. The iframe posts gx:request-auth to the parent. The shell checks _authUser. If signed in, it replies immediately. If not, it opens the auth modal and replies with signedIn: false. The tool receives the response and swallows or proceeds.

// tool iframe
window.parent.postMessage({ type: 'gx:request-auth' }, '*');
// waits for gx:auth-response from parent

Inside the VS Code extension: window.CwsBridge.isEmbedded is true. The extension manages auth. Return signedIn: true immediately.

if (window.CwsBridge && window.CwsBridge.isEmbedded) {
  return Promise.resolve({ signedIn: true, tier: 'free' });
}

Standalone page: no parent frame, no extension. Open the GxAuth inline modal directly.


GxAuth: Shared Auth Modal

The auth modal used to live in os-shell.js as 140 lines of DOM construction. Every standalone tool that needed auth redirected to a sign-in page, which destroyed the tool's in-memory state.

ginexys-auth.js is a shared component loaded as a plain script. It injects the modal into document.body once, manages Supabase sign-in via Google and GitHub OAuth, and dispatches events the caller listens to:

window.GxAuth.open({
  context: 'Sign in to save and export your work.',
  onSignIn: (user) => resolve({ signedIn: true, tier: user.tier }),
});

window.addEventListener('gx:auth-modal-closed', function handler() { window.removeEventListener('gx:auth-modal-closed', handler); resolve({ signedIn: false, tier: 'guest' }); }, { once: true });

The modal uses gxauth- prefixed IDs throughout to avoid collision with the shell's own modal elements. OAuth return is handled via visibilitychange: when the user completes GitHub/Google auth in a new tab and returns to the tool tab, the modal reads the session and closes.

The OS shell no longer builds a modal itself. _openAuthModal() and _closeAuthModal() now delegate to GxAuth.open() and GxAuth.close().


Tradeoffs

The gx:request-auth response from the shell sends signedIn: false when the user is anonymous and immediately opens the auth modal. The tool's export is swallowed. If the user signs in via the modal and then tries to export again, the gate fires a second time. That second check hits the CwsBridge path (shell now has _authUser) and resolves immediately. No double modal.

The 5-second timeout on the postMessage response means a hung shell does not permanently block export for embedded tools. The tool falls back to signedIn: true after the timeout.


What to Watch For

ginexys-auth.js must load before os-shell.js on the shell page. Shell init calls GxAuth.open() during _openAuthModal(). If GxAuth is undefined at that moment, the shell silently does nothing and the user sees no modal. Load order: ginexys-auth.js first, deferred.

Read this post in the full Engineering Journal →