Engineering Journal
Ginexys
Ginexys

Gate at the Export Moment, Not the Usage Moment

2026-06-02

TLDR

Gating a developer tool at the moment of feature exploration frustrates curious users before they discover value. Gating at the export/download button converts users who have already achieved value into active accounts. To implement output-based gating across three environments (standalone browser pages, OS shell iframes, and VS Code extensions), we engineered a single, environment-agnostic checkAuth() routine.

Host EnvironmentAuth State ResolverUser Experience FlowTimeout Safety Guard
VS Code Webviewwindow.CwsBridge.isEmbeddedAutomatic pass (Handled via OS Keychain)None required
OS Shell (<iframe>)window.parent.postMessage bridgeTriggers parent shell auth modal5-Second fallback timeout
Standalone Pagewindow.GxAuth.open() modalInjects standalone inline modalModal dismissal listener

Early usage paywalls interrupt exploration and drive bounces

Usage-based paywalls make total sense for API consumption or cloud infrastructure where every request costs raw server compute. But for client-side developer tools (such as PDF Processors, table formatters, and diagram editors), the value is not created when the user runs the tool. It is created when they export the finished result.

Gating users after two test runs interrupts their exploratory evaluation at the worst possible moment. If a developer processes three test files to see if your tool handles edge cases and gets hit with a login wall on test three, they bounce.

Moving the paywall to the export button solves this cleanly: users experiment freely, and the auth prompt fires only when they attempt to download their finished work.


Multi-platform clients isolate credential environments

Moving the gate to the export button created a challenging integration problem: every export button in every tool had to resolve auth state across three host environments that manage credentials differently:

  1. Inside an OS shell (<iframe>): The parent desktop window manages the session cookie; child iframe tools cannot read parent memory.
  2. Inside a VS Code webview: The extension host manages auth via the OS Keychain (context.secrets); there is no browser cookie store.
  3. On a standalone web page: The page has direct access to browser session state and must open an inline modal directly.
Without a unified abstraction, every export button would require messy environment-specific branching logic.

Single checkAuth handlers abstract platform dependencies

We built a single async checkAuth() function that abstracts environment checks into three paths:

                      [User Clicks Export Button]
                                  |
                                  v
                             [checkAuth()]
                                  |
                                  v
                          Host Environment?
                           /      |      \
         VS Code Extension        |       Standalone Page
                /                 |              \
               v                  v               v
       [Always allow]        [OS Shell Iframe]   [Open GxAuth modal]
      (Handled via IDE)      (postMessage to      (Inline login form)
               \              parent window)              /
                \                 |                      /
                 v                v                     v
                +----------------------------------------+
                |         Execute Export Download        |
                +----------------------------------------+
async function checkAuth() {
  // Path 1: VS Code Extension Host
  if (window.CwsBridge && window.CwsBridge.isEmbedded) {
    return { signedIn: true, tier: "free" };
  }

// Path 2: Embedded inside OS Shell Iframe if (window !== window.parent) { return new Promise((resolve) => { // 5-second safety timer in case parent frame hangs 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) { // Parent shell displays its auth modal; wait for sign-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 Web 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 }, ); }); }

Event wrapper gating secures download triggers

Export buttons wrap their original callback with checkAuth():

async function onExportClick(e, originalHandler) {
  const result = await checkAuth();
  if (!result.signedIn) return;
  originalHandler(e);
}
Rule of thumb: Gate at the moment of value extraction (download/export), not during free-form exploration. Wrap all export triggers in a unified, environment-agnostic auth checker.
Read this post in the full Engineering Journal →