Engineering Journal
Ginexys
Ginexys

Gate at the Export Moment, Not the Usage Moment

2026-06-02

TLDR

A gate that fires at feature access converts curious users into bounces. A gate that fires at export converts users who already got value into accounts. Moving the gate requires solving one concrete engineering problem: the auth modal needs to work in three different environments with the same call.

Repo: tools/pdf-processor

The Problem Class

Usage-based gates make sense when the value is in the usage itself: API calls, AI tokens, compute time. The user gets value with each use, so gating each use is coherent.

Tool-based gates are different. The value is in the output: the extracted table, the formatted CSV, the generated diagram. A user who uses the tool ten times and exports nothing has gotten no value. A user who uses the tool twice and then exports their work has gotten exactly the value the tool was designed for.

Gating at usage for a tool product means blocking users who are in the middle of getting value, at the most frustrating possible moment. The correct gate is at output.


The Engineering Problem

Moving a gate from "before feature runs" to "before file downloads" is straightforward in isolation. The hard part: every export button across every tool now needs an auth check, and the check needs to work in three environments that have different auth mechanisms.

Inside an OS shell (iframe): the parent frame owns the session. The tool cannot access the parent's auth state directly. It must ask.

Inside a VS Code extension (webview): the extension manages auth via the OS keychain. The page has no browser session to check.

Standalone page (direct URL, no parent frame): standard browser session. The tool can open an inline modal directly.

The same export button must handle all three. The caller should not care which environment it is in.


The Three-Path checkAuth()

async function checkAuth() {
  // Path 1: VS Code extension
  if (window.CwsBridge && window.CwsBridge.isEmbedded) {
    return { signedIn: true, tier: 'free' };
  }

// Path 2: iframe inside OS shell if (window !== window.parent) { return new Promise((resolve) => { const timer = setTimeout(() => resolve({ signedIn: true }), 5000); window.addEventListener('message', function handler(e) { if (e.data?.type !== 'gx:auth-response') return; clearTimeout(timer); window.removeEventListener('message', handler); if (!e.data.signedIn) { // shell opens its own modal; tool waits for gx:signed-in event window.addEventListener('gx:signed-in', () => resolve({ signedIn: true, tier: e.data.tier }), { once: true }); window.addEventListener('gx:auth-modal-closed', () => resolve({ signedIn: false }), { once: true }); } else { resolve({ signedIn: true, tier: e.data.tier }); } }, { once: true }); window.parent.postMessage({ type: 'gx:request-auth' }, '*'); }); }

// Path 3: standalone page return new Promise((resolve) => { 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', () => resolve({ signedIn: false }), { once: true }); }); }

The 5-second timeout on the shell path prevents a hung parent from permanently blocking exports. The tool falls back to allowing the action after the timeout.


The Shared Auth Modal

Path 3 requires an inline modal that works on any page, with no dependency on the shell. The auth UI used to be embedded in the shell's own component. Standalone pages had no modal at all and fell back to a page redirect, destroying all in-memory work.

Extracting the modal into a standalone component solves both problems. The component loads as a plain script, injects once into document.body, and exposes a simple API:

window.GxAuth.open({ context, onSignIn })
window.GxAuth.close()
window.GxAuth.hasSession()

All element IDs inside the modal use a dedicated prefix to avoid collisions on pages where both the modal and the shell load. The shell's own auth UI was replaced with a one-line delegation to the same component, removing ~140 lines of duplicated DOM construction.


Where the Gate Lives

Every export handler wraps its original action with the auth check:

async function onExportClick(e, originalHandler) {
  const result = await checkAuth();
  if (!result.signedIn) return;
  originalHandler(e);
}

The user runs the tool freely. The first time they click export, the check fires. If they are already signed in, it resolves immediately. If not, the appropriate modal or prompt appears for their environment. If they sign in and click export again, the check resolves from the cached session immediately.


Tradeoffs

Corrections made inside a tool session are in-memory only. If a user fails the auth check, dismisses the modal, and then navigates away, their work is gone. The gate creates an implicit pressure: sign in before you invest too much in a session. This is the intended behavior, but it means the tool needs to be fast enough at setup that users have not invested heavily before they hit the gate.


What to Watch For

The modal script must load before any gate script on the same page. On shell pages, the shell calls GxAuth.open() through delegation. If GxAuth is undefined at that moment, the call silently does nothing. Load order: modal script first, with defer, above the gate and shell scripts in the HTML.

Read this post in the full Engineering Journal →