Two prefixes decide who owns a credential
Whether a credential is organisation-wide or per-app has to be visible in its name, because the name is what every manifest and resolver decides from.
A publishing credential belongs to the whole organisation: every application ships through the same store account, so they all legitimately use the same one. A signing key belongs to exactly one application and nothing else may touch it.
Those two facts live, in practice, in the credential's name.
The case this post is about
Two naming shapes, which decide what a project is allowed to hold:
<organisation>-<suffix> shared across the whole organisation
<application>-<suffix> belongs to exactly one application
A signing key for the organisation's store account is the first shape. An app's own upload keystore is the second. Those are not two conventions for writing the same thing down — the prefix is what stops one application resolving a credential that belongs to the whole organisation.
Before the rule, a manifest in one project referenced an alias prefixed with a different organisation's name, and it resolved. This post follows that distinction, because it reads as documentation and it is the only thing standing between a project and a credential it should never be able to see.
The symptom
What goes wrong is not dramatic. A project manifest lists the aliases it needs, somebody adds a line, and the credential resolves. It works on the first try, which is the problem — there was no moment where anything looked wrong.
The alias they added belonged to a different application. Both applications are in the same vault, both are shipped by the same people, and the alias was plausible. Nothing in the name said this one is scoped to a single app, so nothing in the request said you are reaching outside your boundary. The resolver did what it does: looked up the name, returned a path, handed over a credential.

The second shape is the reverse, and it is more common and less harmful. A credential that genuinely is organisation-wide gets copied per application, because nothing said it was shared. Now the same publishing account has three aliases, three vault entries, three rotation schedules — and when it is rotated, two of them are stale and the applications using them fail at release time, individually, weeks apart.
The third is naming drift of the ordinary kind. One alias is written in one casing, another in a different one, a third is prefixed with a different organisation entirely because it was copied from a sibling project. None of these break anything immediately. They break the tooling that wants to reason about aliases, and they break it silently by simply not matching.
The fourth only shows up at rotation. When a credential is compromised, the question you urgently need answered is which projects hold this. If the name does not encode scope, that question is a search across every manifest, and a search that returns nothing is indistinguishable from a search that found nothing because the name was spelled differently.
Why the obvious fix failed
Document the ownership. The usual first answer and the usual failure: the document is not in the path. A manifest is edited by a person reading other manifest lines, not by a person reading the naming guide, and the thing that is actually consulted is the list of aliases that already exist.
Keep a separate ownership table. Better, because it is machine-readable, and it has a join problem. Now the alias and its scope live in two places, and the alias is the only one that appears at the point of use. Anything that fails to consult the table — a human eye, a quick grep, a new script — sees a name with no scope in it, which is exactly where we started.
Enforce it with permissions instead. The right instinct, and it answers a different question. Vault permissions control who can read a credential, which is a property of people and machines. What is missing here is which project may legitimately declare a dependency on it — a design-time question, asked while editing a manifest, long before any permission is evaluated. Permissions would catch the access and leave the manifest wrong.
Rely on review. A reviewer looking at a one-line manifest addition sees a plausible alias in a list of plausible aliases. Spotting the problem requires knowing which of those names is app-scoped, which is precisely the fact the name failed to carry. You cannot review your way out of a missing field.
The actual mechanism
Put the scope in the name, where every consumer already looks.
<organisation>-<suffix> shared across the whole organisation
<application>-<suffix> belongs to exactly one application
That is the entire scheme and the value is not in the format. It is that the name is the one piece of information that travels everywhere — into the manifest, into the resolver call, into the registry row, into the log line, into the error message, into the conversation. Anything you encode there is present at every point where a decision gets made about the credential; anything you encode elsewhere is present only where somebody remembered to look it up.

Three properties make it enforceable rather than aspirational.
The prefix must match the asking project. A manifest in application A may declare an application-scoped alias only if the prefix is A's. That is a string comparison, it needs no registry lookup, and it is the check that stops the cross-application reach. The failure the post opened with becomes impossible to express.
Which suffixes are organisation-scoped is itself declared. Otherwise the prefix rule is circular:
you cannot tell whether <something>-publishing-account should be organisation or application scoped
without knowing what publishing accounts are. A small canonical list — these suffixes are shared,
those are per-app — turns that into a lookup, and it is the one table worth maintaining because every
other check derives from it.
The form is fixed and dull. Lower case, hyphenated, no abbreviations that only make sense to whoever coined them. This sounds like aesthetics and it is mechanical: a rule that tooling can verify has to have exactly one spelling, and every permitted variation is a case where a check has to decide whether two strings mean the same thing. The moment that question exists, the answer is sometimes wrong.
The sub-case that bit me: an alias can be correctly formed and wrongly scoped. A name carrying an application prefix for a credential that is actually shared passes every syntactic check and is still wrong — it will get copied per application, and the rotation problem arrives on schedule. So the check has to compare the suffix against the canonical list and verify the tier agrees, not just that the shape is valid. Syntax and semantics are different gates and only one of them is easy.
The fix
NC1 shape — lowercase, hyphenated, with a prefix that is the bound org or project
NC2 registered — the alias exists in the registry
NC3 tier — an organisation suffix carries an org prefix; a per-app suffix carries an app prefix
All three as one check, run over every manifest — and NC3 is the one that catches the mistake nobody notices, because a wrongly-tiered alias is well-formed and registered:
const ORG_SUFFIXES = new Set(["firebase-sa", "play-store-sa", "appstore-p8", "match-ssh-key"]);
const APP_SUFFIXES = new Set(["upload-keystore", "supabase-url", "app-ids", "ios-plist"]);
export function checkAlias(alias: string, ctx: { org: string; project: string },
registry: Set<string>) {
// NC1 — shape
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(alias)) {
return `${alias}: not kebab-case`;
}
// NC2 — registered
if (!registry.has(alias)) return `${alias}: not in the alias registry`;
// NC3 — tier. The suffix decides which prefix is correct; a per-app value
// under an org prefix is shared by every project in the org, silently.
const suffix = [...ORG_SUFFIXES, ...APP_SUFFIXES].find((s) => alias.endsWith(s));
if (!suffix) return `${alias}: unknown suffix — add it to one of the tier sets`;
const want = ORG_SUFFIXES.has(suffix) ? ctx.org : ctx.project;
if (alias !== `${want}-${suffix}`) {
return `${alias}: ${ORG_SUFFIXES.has(suffix) ? "org" : "per-app"} suffix ` +
`'${suffix}' requires prefix '${want}' — expected ${want}-${suffix}`;
}
return null;
}
The three failures, in increasing order of how long they take to notice:
$ check-aliases
AcmeApp-firebase-sa not kebab-case
acme-stripe-key not in the alias registry
acme-upload-keystore per-app suffix 'upload-keystore' requires prefix
'storefront' — expected storefront-upload-keystore
The third is the dangerous one. It is well-formed, it is registered, it resolves, and it means every project in the organisation shares one signing keystore — which nothing reveals until two apps need different ones.
Three checks, run over every manifest, every layout file and every deployment declaration. The third is the one that carries the meaning; the first two exist so the third can be computed at all.
There is a question of where these run, and the answer is everywhere a manifest can be edited rather than only at release. A naming violation found during a release is found at the worst possible moment — the credential resolves, the build is halfway through, and the person holding it wants to ship, not to rename an alias across three files. Found at commit time it is a thirty-second fix on a line somebody is already looking at. The checks are cheap enough that the only reason to run them late is not having wired them in early.

What this buys at the point of use is that a manifest becomes readable. A list of aliases where every line states its own scope can be reviewed by someone who does not know the vault, because the question should this project have this is answerable from the line itself.
How we proved it
The violations the check found were real and unglamorous. Aliases in the wrong casing. An alias carrying a sibling organisation's prefix, inside a project that belongs to neither. Generic names with no prefix at all, which were ambiguous in exactly the way the scheme exists to prevent. None of these were failing anything at the time — they were all resolving correctly — and every one of them was a question nobody could answer from the name.
The cross-organisation prefix is the one I would point at. An alias beginning with one organisation's name, declared in a project belonging to another, had been sitting there working. It is the clearest demonstration that "it resolves" and "it is correct" are different properties, and that nothing in the resolution path was ever going to notice.
What I cannot claim: that this prevented a leak. It prevented a class of declaration that would make one easy, and it found several instances of that declaration already present. Whether any of them would have become an incident is not something I can know, and I would rather say that than imply a save I cannot evidence.
One thing the checks cannot see at all is the vault's own contents. They read manifests, layout files and deployment declarations — the places a project declares what it needs — and they say nothing about whether the credential sitting behind an alias is the one the name claims. An alias correctly prefixed for application A, pointing at a vault entry that actually holds the shared publishing account, passes all three checks and is wrong in the way that matters most. Closing that would mean the checks reaching into the vault, which is a larger piece of machinery than I have built, and it is the obvious next gap rather than a solved one.
What I have not solved: names that are correct today and wrong tomorrow. A credential can change tier — a key used by one application gets adopted by a second, and now its app-scoped name is a lie that passes every check, because the checks verify the name against the canonical list and the canonical list is what became wrong. Nothing detects that drift. It requires somebody to notice at the moment of adoption, which is the kind of dependency this post is arguing against, and I have not fixed it.

What this generalizes to
When a value is used to make a decision in several places, encode the decision inputs in the value itself rather than in a table beside it.
A name travels further than any metadata about it. It reaches code, configuration, logs, error messages, dashboards, and conversations between people who have never opened the registry. Every one of those is a place a question gets asked, and the ones answerable from the name get answered correctly by default.
The counter-pressure is real: encoding structure into identifiers is rigid, and renaming is expensive once the name is everywhere. So the test is whether the structure encodes something that genuinely does not change — scope, ownership, kind — rather than something that does, like a region, an owner's team, or a version. Put the stable facts in the name and the volatile ones in the table, and the expensive property of names becomes the thing that makes them reliable.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".