Most stores that gate age do it with a snippet in the theme. A modal appears, the shopper clicks a button, a cookie is set, and the modal stops appearing. It is fast to add, costs nothing, and for a lot of merchants it is genuinely the right amount of effort.
This is not an argument that the snippet is wrong or that it fails some rule. It is a comparison of what each approach can know, because that is the actual difference, and it decides which one you want long before any question of law comes up.
Where the Decision Is Made
A theme snippet runs in the shopper's browser. Everything it knows, the shopper controls: the click, the cookie, the page state, all of it. That is not a criticism of any particular snippet. It is what running in the browser means.
AgeProof takes the decision on the server. The storefront gate posts to an app-proxy endpoint, and the comment sitting above that endpoint states the assumption it is built on, together with the three consequences of it. This is the file, unedited:
// EVERYTHING IN THE BODY IS BUYER-CONTROLLED. This endpoint is reachable from any
// storefront console — `authenticate.public.appProxy` proves only that Shopify forwarded
// the request, never that the sender answered the gate. So the decision is taken here,
// from data this server owns, and the body is treated as a claim rather than a verdict:
//
// * the verdict is fail-CLOSED — only the literal "verified" is even a claim of a pass,
// and an absent, empty or unparseable body is a block rather than a default;
// * `minAge` is read from the shop's own Setting (plus its per-country region rule),
// never from the request, so a shopper cannot lower their own bar;
// * a claimed age must clear that bar on this side before a token exists.
What those three amount to
Each one closes a way the answer could be borrowed from the caller. Fail closed means silence is a refusal, so the cheapest thing anyone can send — nothing at all — is also the one guaranteed to fail. The bar is the merchant's means the number being cleared is a fact about the shop, so no request can lower it. The check happens here means the comparison is ours rather than a result we were handed. Any one of the three missing puts the decision back in the browser.
The Part That Makes It Real at Checkout
A gate that only hides a page is a gate you can walk around by not loading the page. What connects AgeProof's gate to the order is that clearing it mints a signed token, and that token is returned for a pass and never for a block, from one place in one file. The comment above it puts the point plainly: the signed proof the checkout validation function demands can only come from actually clearing the gate.
That is the structural difference. A theme snippet can hide a product and set a cookie; it cannot put a condition on the order itself, because the order is not decided in the browser. Ours can, and the reason it can is that the thing checkout demands is a proof that only the server issues.
We Learned This the Hard Way
The version of this that did not work
Before August, the endpoint decided with body.result === "blocked" ? "blocked" : "verified", which reads every value that is not the literal string “blocked” as a pass, an absent one included. So an empty body was a pass. The minimum age came straight off the request, and the claimed age was recorded and never compared to anything. A single line of JavaScript in a console — a bare POST with no body at all — minted a real ninety-day token. Fixed on 9 August, and the fix is the whole list above: fail closed, read the bar from the shop, check the claim server-side. We are telling you because a comparison that only lists our strengths is not a comparison, and because the specific way it was wrong is the specific way most homemade gates are wrong.
Country, and the Honest Version of It
Region rules need to know where a shopper is, and that is where most of the interesting failure lives. Two things worth knowing about how ours answers.
Unknown is a real answer. The lookup returns nothing when it cannot tell, and the caller is required to fall back to the strictest rule the merchant configured rather than the most permissive. A gate that guesses in the shopper's favour when it is unsure is a gate that fails open on exactly the requests you would most want it to be careful about.
The address is read the way the request actually arrives. The forwarding header is read by counting from the end, past the hops we added ourselves, rather than trusting the front of it. Too few entries means the request did not arrive the way an app-proxy request arrives, and the answer is again we do not know rather than a guess. We are not going to publish the arithmetic, and you should be wary of any product that tells you its rate-limit and address-parsing constants in a marketing page.
Why the lookup reads two different record shapes
Because a schema mismatch fails the quiet way. If the database layout and the reader disagree, every lookup returns nothing, every shopper becomes unknown, and the only visible symptom is that region rules stop applying. Reading both common layouts means swapping the underlying database cannot reintroduce that. This is written down in the code with its own comment, and it exists because a similar silent failure had already happened once.
What the Snippet Still Wins
Honestly, several things.
- Nothing to install and nothing to pay. If the requirement is a visible notice rather than an enforced condition, that is a real answer.
- Total design control. A snippet is your markup. No app's styling to work around.
- No dependency. One fewer thing that can be uninstalled, mispriced or discontinued.
The trade is that all three of those come from it being yours and local, and the limits come from exactly the same place. It can present, remember and hide. It cannot decide, because deciding requires knowing something the shopper did not tell you.
Choosing Between Them
One question settles it
Does the outcome need to be true, or does it need to be shown? If you need a visitor to see a notice before browsing, a snippet does that and the rest of this article is beside the point. If an order must not complete unless the gate was actually cleared, a browser cannot give you that, and no amount of snippet polish changes it. Everything else — region rules, logging, what happens when the country is unknown — follows from which of those two you are answering.
What we will not do is tell you which one your situation requires. Age verification obligations vary by product, by jurisdiction and by how you sell, they are not settled in most places, and a software vendor telling you what the law expects of you is a vendor selling you something. That question is for your own counsel. What we can tell you is precisely what each approach is able to know, which is what this article is.
If you want the same treatment applied to the rest of the shelf, seven apps and the problem each one was built for is the overview, AccessGuard versus accessibility overlays is the same argument about a different surface, and Merchant Guard versus Google's own diagnostics is the version where the other side is a first-party tool. If you would rather see something running before you read anything, four free scans and what each one cannot answer is written entirely around the limits.