A plan limit has three surfaces

Gating a paid tier is three decisions: what gets served, what gets accepted, and what gets reported. The costly failure is those three drifting apart.

Three parallel glass channels stack vertically, with the middle channel glowing orange

Enforcing a paid tier is three decisions rather than one: what gets served, what gets accepted, and what gets reported. The check inside the bundle is rarely what breaks. What breaks is a panel telling a customer their badge is hidden while every visitor to their site still sees it.

The report is a third answer, and nobody gates it

Two of the three decisions get attention because both have an obvious adversary. The third one has no adversary at all, which is why it is usually written by whoever was closest to the database.

A report answers a question a customer asks about their own account: is my badge hidden, is my CSS live, is recording on. That question has two possible sources. One is the flag the customer set. The other is the value the server actually compiled into the configuration the page received. Reading the first is the shorter query and it is a lie in the shape of a feature.

SurfaceThe question it settlesWhat decides it
ServedWhich configuration the script in the page receivesThe plan, resolved while the configuration is compiled
AcceptedWhich uploads from that script are storedThe same plan, read a second time at ingest
ReportedWhat the customer is told is true of their own siteThe served answer, if anyone remembered to ask for it

We shipped that lie and then closed it. A tool that reads back a project’s capture privacy and branding posture, and describes itself in its own schema as the effective posture, read the stored branding flag. A project whose plan did not grant white labelling was told its badge was hidden while every visitor of its site saw it. The fix was not a better query but a single exported definition of the answer, called by the code that compiles the widget configuration and by the reporting tool, so there is no second reading to drift.

The stored value stays editable and stays visible through the settings write path, and that placement is the point. A stored toggle records what somebody wants; a report of the effective posture records what the page does. Keeping both readable, and labelling which is which, is what lets a support conversation end in one message instead of three.

A downgrade should stop serving a configuration, not delete it

The tempting way to make every reader agree is to write through on the billing event: clear the custom CSS when the plan drops, flip the branding flag back, and there is only one value left to read. It is correct by construction and it is the wrong trade, because a failed payment that clears in two days has just destroyed work the customer will have to do again.

The alternative costs one adjective. The entitlement pass is a projection on the read path and never touches stored settings. A downgraded organization keeps the CSS and the toggles it authored, stops being served them, and gets its widget back on upgrade with nothing to re-enter. Billing events are reversible; a delete is not, and the two should not share a code path.

That trade has a limit worth planning for rather than hiding. A customer’s editor then shows a value their site is not using, and unless that editor says so, the misreport returns in the one screen a customer looks at most. A read-path gate obliges every surface that renders a stored setting to also render whether it is being served.

What gets served has to be compiled whole

Anything the script decides for itself is decided by whoever owns the page it runs in, so a badge hidden in the browser is a checkbox in devtools. That premise is the least interesting part of the design. What matters is that withholding a capability from a compiled configuration is rarely one field.

A widget theme is not carried alone. It travels with a digest and with a precompiled block that lets the launcher paint before the runtime arrives, which is the same load-path discipline the host isolation contract is built around. The widget re-derives both from the theme it was handed and falls back to its own default theme when either disagrees. So a gate that blanks the custom CSS and forgets the digest withholds no feature at all. It resets the customer’s design on every page that embeds the feedback widget, which is a far worse outcome than the one the gate was written to prevent.

The precompiled block is also a second door onto the same CSS, because it projects launcher declarations out of it. It has to be recompiled from the masked theme rather than passed through from the original. The masking pass therefore returns the input object untouched when nothing was withheld, and recompiles only when something was, which keeps the expensive path off the request that runs once per page load of every embedding site.

What gets accepted is the same decision, made a second time

The read path and the write path answering differently is the failure that costs real money and real trust. A widget told to record while ingest quietly discards what it sends will capture a reporter’s entire session, upload it from the customer’s page, and throw it away on arrival. So both halves are written as one rule in one place, with the accepting half shared by every route that takes a chunk, rather than as branches that look consistent on the day somebody writes them.

The refusal itself is worth being deliberate about. An ungranted session replay chunk gets a 204, not a 403. The widget was already configured not to send it, so anything still arriving is a stale configuration lease on a page somebody left open in a tab. Refusing it loudly prints errors in a stranger’s console on the customer’s own site and buys nothing, since the enforcement is that the bytes are never stored and never counted.

A silent drop has one problem: it is indistinguishable from a successful upload. So the reason for the refusal rides back in a response header, named in the CORS exposure list because a header not exposed there is a header the recorder cannot read cross-origin. The recorder writes the fact into the session’s own evidence, and the ticket carries a recording that is missing for a stated reason rather than one that is missing. Who owns that gap is then a triage question with an answer in it.

One ordering inside that refusal is not arbitrary. The project’s own recording switch is tested before the organization’s plan, because the switch is something a person in the room can change from the project’s settings, while the plan belongs to whoever signs for the account and is usually nobody in the queue.

A restriction nothing enforces is the same bug facing the other way

Every capability in the catalogue is classified a second time, by something with no relation to any tier: whether a server seam actually refuses it. Marked as enforced means a gate exists and the feature stops. Marked as universal means nothing consults the flag, so every tier has the feature whatever its row says.

Some universal entries are deliberate. A capability that is the product’s whole premise is not an upsell, and theming is granted to every tier on purpose, because a free widget that looks wrong on a customer’s site is a worse advertisement than no widget. What makes the classification worth keeping in production code rather than in a test is the other case: a comparison table is a reporting surface too, and publishing a capability as withheld when no seam withholds it announces a restriction that never happens, to a prospect instead of a customer.

One capability sat in the universal column on the strength of nobody having asked for it yet. That reason stopped holding the moment an operator could move a tier’s flags without a deploy, which is the general rule underneath all of this. The three surfaces stay honest only while one computation feeds all of them, and every place a tier can change without a release is a place where three answers can start disagreeing quietly.

Common questions

Where should the plan check for an embedded widget actually run?

While the configuration is compiled on the server, so the script in the page receives only the capabilities the plan grants. A check that lives inside the bundle reads a value the page owner can edit, and every branch downstream of it is theirs to decide.

What happens to a customer's custom CSS when their plan is downgraded?

Nothing is deleted, if the gate is written as a read-path projection. The stored theme keeps the CSS and the toggles the customer authored, the server stops serving them to the widget, and an upgrade restores the widget they already had.

Why does a settings screen sometimes disagree with what visitors actually see?

Because the screen reads the stored flag while the visitor is served the compiled one. A toggle set to hide a badge records an intention, and only the configuration that reached the page can answer whether the badge is hidden.

Should a server refuse an ungranted upload with 403 or with 204?

204 with a machine-readable reason in a response header, for fire-and-forget telemetry a script posts without waiting for an answer. A 403 prints an error in a visitor's console on the customer's own site, and the enforcement is that the bytes are never stored.

How do you stop a pricing table from advertising a restriction that does not exist?

Classify every capability by whether a server seam actually refuses it, and keep that classification in production code rather than in a test. A flag nothing consults grants the feature to every tier whatever the comparison table says about it.

Why does masking a single theme field break a customer's whole design?

A theme usually travels with a digest and a precompiled block derived from it, and a widget that re-derives both will fall back to its own default when they disagree. Masking one field without recompiling the rest resets the design on every embedding page.

The Notarus team

The people who build Notarus. We write up the decisions behind the product when the reasoning is worth more than the changelog line.

Keep reading

Reports with the evidence attached.

Start free, one script tag.

<script src="…/widget.js" data-key="pb_pub_…">