A Shopify app has two names. One is its listing slug, the tail of its App Store URL. The other is its admin handle, the segment a merchant's own admin uses inside admin.shopify.com/store/<store>/apps/<handle>/app/<page>. They are set separately, they can be the same string, and they often nearly are.
That last property is worth an article. Two vocabularies that overlap almost completely give you nothing to notice when you reach for the wrong one, and a value that is a plausible spelling of the right one is the kind every reviewer waves through. Here is which URL wants which name, how the seven apps we run build their links so the wrong one cannot get in, and the rule underneath it that applies well beyond Shopify.
Which URL Wants Which Name
A deep link in an email, a statement, an alert, has to use the admin handle. If it uses the listing slug instead, the admin answers with the polite Shopify equivalent of nothing here: There's no page at this address. The link identifies the correct app. It is still the wrong identifier for that URL.
The listing slug belongs on exactly one kind of link, the one that points at the public App Store page. Everything that lands inside a merchant's admin wants the handle. Writing that down, in the place the URL is built, is the first of the three rules at the end of this article.
How the Mailers Build a Link Now
The mailer in each app builds its link from an environment variable, falling back to a literal written into the code. That design is fine. What matters is what goes into the variable and the literal, and where each value came from.
Three layers carry the same verified value. The deploy script writes the handle on every deploy, so a fresh environment cannot start from a guess. The literal in each mailer is the same handle, so the fallback is as good as the variable. And a fleet-wide test records, for each of the seven apps, which real admin the handle was read from and on which day. A redeploy cannot regress it, because there is no unverified value anywhere in the chain to fall back to.
The rule this encodes
A value copied from your own configuration is a claim, not a verification, until something outside the configuration has agreed with it. For a URL, the only thing outside is the page loading.
Why the Suffix Stayed
One detail is instructive, because the obvious tidy-up would have been wrong. One of the verified handles carries a numeric suffix, which looks like an artefact of a development store and tempting to strip. It is not per-store. The suffix is global to the app: the same store carries two different apps with the same base name and different suffixes, and a third-party app on it shows the same pattern. Keeping the suffix was the right call, and it was only knowable by looking at a real admin, which is the rule again from the other side.
Why the Test Exists
Before the table existed, three of the seven mailers built their links from the other namespace. One used its fallback literal, and the literal was the listing slug, which reads as obviously right because it is the name on the store listing. Two had a handle deployed that was neither the slug nor the handle but a plausible hyphenated spelling reasoned into the deploy script on the day each mailer shipped. Every one of those links named the right store, the right app and the right page, and clicked through to nothing. The first was found from a real statement by the one person who clicked the button rather than reading it; the other two by refusing to stop at one app and checking all seven against a live admin the next day.
The property that hides it
A wrong value that looks nothing like the right one gets caught in review. A wrong value that is a plausible spelling of the right one gets waved through by every reviewer, every time, including the reviewer who wrote the test. Overlapping namespaces manufacture exactly that kind of wrong value at scale.
Change Detection Is Not Verification
There had been a fleet-wide test all along. It pinned the deployed handle for every app, and it proved something real: that the deploy script and the test agreed. But both had been copied from the same guess. A test that checks configuration against configuration is a change detector, and a useful one, but it is not a verification of anything, and calling it one is how a wrong value acquires a green tick.
The table that replaced it is not a bigger test. It is a different kind of claim. The number of apps in the table is the number of apps we have, and every value in it has been observed working in the place a merchant would click it. The test says, per row, what that place was.
What Was Not the Problem
Not Shopify. Two namespaces with overlapping vocabularies is a normal fact about a platform that has a public catalogue and a private admin, and both names are visible to anyone who looks in the right place. The lesson is about where a value comes from, not about the platform that defines it.
Nor the fallback design. A literal in the code behind an environment variable is a reasonable belt and braces. Belt and braces only help when both hold a verified value, which is why the fix went into all three layers at once rather than into whichever one happened to be read first.
Three Things Worth Stealing
- When a platform has two names for one thing, write down which one each URL wants. Not in your head. In the place the URL is built, as a comment that names the other namespace and says why it is wrong here.
- Verify identifiers where the user will use them. A deep link is verified by loading it in the admin it is meant for, in a real session. Reading it, however carefully, checks spelling.
- Separate change detection from verification in your tests, and label which is which. A pinned value is a fine change detector. It becomes a verification only when its source is something outside your own configuration, and the test should say what that source was.
None of the three is clever, and all three are cheap. A link is verified by the page loading, in the admin it was built for, and by nothing else.
The Other Shelf: Seven Shopify Apps
The fleet these mailers belong to, and the problem each app was built for.
The Seven Apps