Three finished exporters shipped with no way to call them
TLDR: We gated three exports by swapping <button id="..."> for <div class="gx-pro-locked"> with a click interceptor. The ids went with the buttons, the click bindings had nothing to bind to, and exportAsSqlDdl and exportAsMermaidSequence became dead code that no code path could reach.
What we found
An audit of the paid tier, checking each gated feature for an implementation behind it. Three of them had complete implementations:
src/js/features/uiControls.js:1804 batchExport()
src/js/features/uiControls.js:1857 exportAsSqlDdl()
src/js/features/uiControls.js:2079 exportAsMermaidSequence()
exportAsSqlDdl reads a schema model, walks entities, emits DDL with composite keys and foreign key clauses. exportAsMermaidSequence resolves actors by x-position and emits a sequenceDiagram. Both real, both tested by hand at some point, both finished.
Neither had a single caller.
How the gate deleted them
The pattern looked reasonable. To lock a control, make it not a button:
<div class="btn gx-pro-locked" title="Export ERD as SQL DDL (Pro)">
<span class="panel-content">SQL <span class="gx-pro-badge">PRO</span></span>
<div class="gx-pro-interceptor" data-pro-feature="schema-export-sql"></div>
</div>
The interceptor is a transparent overlay that catches the click and opens the upgrade modal. It works: clicking shows the modal.
But the original <button id="exportSqlBtn"> is gone, so this in the boot script binds to nothing:
$("#exportSqlBtn").on("click", () => window.editor.exportAsSqlDdl());
jQuery does not complain when a selector matches zero elements. That is the whole failure: the binding silently becomes a no-op, and there is no state in which the function gets called, for any user, at any tier.
Someone noticed the dangling lines and left a comment instead of a question:
// sendToTafneBtn, batchExportBtn, exportSqlBtn, exportMermaidSeqBtn are now
// Pro-gated — handled by delegated interceptor below
That comment is accurate about the gate and wrong about the feature. The interceptor handles the click. It never handles the export, because unlocking was never implemented. Nothing anywhere flips a gx-pro-locked div back into a working control.
Why it survived
The gate demoed correctly. Click it, get the modal. That is what anyone testing a paywall checks, and it passed.
Nobody had Pro. No account has ever had tier = 'pro', because there is no checkout endpoint. So the unlocked path had zero users and zero test runs, and a path with no users cannot report that it does not exist.
Dead code looks like live code. exportAsSqlDdl sits in a 2,400-line file surrounded by functions that are called. Nothing marks it. Static analysis would have found it in seconds; nobody ran any.
The fix
Restore the buttons, restore the bindings.
<button class="btn export-domain-btn" data-mode="software" id="exportSqlBtn"
title="Export ERD as SQL DDL">
<span class="panel-content">SQL</span>
</button>
$("#exportSqlBtn").on("click", () => window.editor.exportAsSqlDdl());
$("#exportMermaidSeqBtn").on("click", () => window.editor.exportAsMermaidSequence());
batchExport needed only the id back; its binding had been correct all along and had simply had no element to attach to.
One of the four stayed locked, and that is the point of doing the audit rather than a blanket unlock: "Send netlist to TAFNE" is the one with no implementation behind it. A gate over nothing is a different problem, and it should not be quietly ungated into a button that does nothing.
The rule this suggests
Gate by state, not by structure. A lock that changes what an element is can break the wiring to what it does. A lock that changes an attribute cannot:
<button id="exportSqlBtn" data-pro-locked="true">
The handler stays bound; it checks the flag and either runs or opens the modal. One code path, one element, and unlocking is removing an attribute rather than rebuilding markup.
A gate needs a test for the unlocked path. Every paywall test we had asserted the modal appears. None asserted the feature runs when it should, which is the half that pays.
Grep for handlers with no element. $("#id") where no id="id" exists in any template is mechanically detectable, and it is exactly the shape this bug takes.
The wider version
We shipped three finished features that no user could reach, for months, while listing them as paid inventory. The audit that found it started from a completely different question: what fraction of each Pro feature is built.
That question is worth asking on your own product periodically, feature by feature, against the code rather than the roadmap. Ours turned up locked doors with nothing behind them, and finished rooms with no door at all.