Skip to content

Why this is written before the sweep, not during it


rank: decision outranks: [] doc_id: PTV-DEC-0018 title: What counts as a credential store, for the purposes of N8N-27 date: 2026-08-17 status: open — awaiting human countersign owner: petrova-codes scaffold: PTV-SCF-0001 phase: P4 supersedes: none

Section titled “rank: decision outranks: [] doc_id: PTV-DEC-0018 title: What counts as a credential store, for the purposes of N8N-27 date: 2026-08-17 status: open — awaiting human countersign owner: petrova-codes scaffold: PTV-SCF-0001 phase: P4 supersedes: none”

Why this is written before the sweep, not during it

Section titled “Why this is written before the sweep, not during it”

N8N-27 exists because three stores were swept and a fourth was not, and the fourth was invisible to every coverage figure computed inside the three (PTV-FND-0038). The obvious repair is to enumerate stores rather than values.

The non-obvious failure mode is that “store” has no fixed meaning. VAULT qualified because it holds a credential the platform environment does not. So does a config file with a key in it, a database column, a keychain call, a hardcoded literal, and a secret fetched from another service at request time. If that boundary is settled repo by repo while reading, the resulting list is shaped by what the reader happened to notice — which is the same sweep-blindness in a new population, arrived at by a new route.

So the boundary is fixed here, in advance, and the sweep is measured against it. Where the definition turns out to be wrong, it is superseded by a new decision doc (MR-7), not widened mid-pass.

A credential store is any location, reachable by a running deployment, from which that deployment obtains a secret it did not receive through its platform environment variables.

Three clauses, each load-bearing:

  • reachable by a running deployment — excludes a developer’s laptop, a password manager, a .env.example. Those are exposure surfaces and matter elsewhere; they are not places a production call gets its key from.
  • obtains — the deployment reads it at runtime or build time. A file that exists in the repo and is never read is not a store, it is litter.
  • did not receive through its platform environment variables — the three swept stores are, by construction, the platform-environment path. This clause is what makes the definition additive to the existing sweep rather than overlapping it.
ClassExampleWhy it qualifies
Application-managed vaultrocky-console VAULT (vault-secrets.json, AES under ROCKY_VAULT_MASTER_KEY)The instance that raised N8N-27
Secret in a committed or deployed filea key literal in config.json, settings.yaml, a seed scriptRead at runtime; invisible to platform enumeration
Hardcoded literal in sourceconst KEY = "n8n_api_..."The limiting case; still a place a call gets a key
Database-resident credentiala secrets/credentials/connections tableRead at runtime; no platform surface shows it
Runtime fetch from another servicea call to a secrets manager, another app’s API, an OIDC exchangeThe credential exists at call time and never appears at rest
Sidecar or mounted secret filea Docker/Railway mounted volume, /run/secrets/...Reachable, read, not an env var

Out of scope, and why — so the exclusions are auditable

Section titled “Out of scope, and why — so the exclusions are auditable”
ClassWhy excluded
Platform environment variables (Railway, Vercel, GitHub Actions)Already swept. Including them double-counts and hides whether N8N-27 added anything
Local developer files (~/code/env/**, shell profiles)Not reachable by a running deployment. Covered by N8N-04
.env.example / templates with empty or placeholder valuesNo secret present
Third-party services holding their own copy (e.g. an n8n credential record)Not a store PETROVA can enumerate; a separate problem
CI caches, build logs, error trackersLeak surfaces, not sources. Real, and a different item

The last two are deliberately excluded rather than forgotten. Recording an exclusion is what makes the negative result mean something later.

The pattern that defeats single-sweep reasoning

Section titled “The pattern that defeats single-sweep reasoning”

rocky-console reads its key from VAULT with a process.env fallback (N8N_BOOTSTRAP_KEY, undeclared on that project):

try { return await getSecretValue("n8n_api_key", "relay"); } // store
catch { const b = process.env.N8N_BOOTSTRAP_KEY; if (b) return b; } // platform

A repo with that shape appears in a platform sweep and holds a store, and neither sweep alone establishes which path is live. The platform sweep sees a variable and concludes the key comes from there; the store sweep sees a vault and concludes the same about it. Both can be wrong simultaneously — here, both paths are dead and the service cannot authenticate at all.

Any repo exhibiting vault-primary-with-env-fallback, or any other multi-source resolution, gets both questions asked and the resolution order recorded. This is a required output of the sweep, not a note.

  1. The repo denominator, measured, with skips itemised and reasoned. The store-denominator error came from three known stores with no record of how “known” was established; reproducing that at the repo layer is the one outcome that would make this whole item pointless.
  2. Per store found: repo, path, class from the table above, what credential it holds if determinable, and whether it is reachable in a live deployment or only in principle.
  3. Multi-source resolutions, per the section above.
  4. What the instrument cannot see, stated as a limit rather than a clean negative. Reading code finds stores that are declared in code. It does not find a credential written into a database by a human through a UI, and it does not find one fetched from a service whose name is itself in a secret.

Two enumerations of “the estate” disagree, measured today:

PTV-FND-0032 (2026-08-17, GitHub Actions sweep) : 30 orgs, 465 repos
this enumeration (2026-08-17) : 40 orgs, 458 repos in orgs
524 repos accessible in total

Forty organisations, not thirty. Fewer repos inside them than the earlier count of repos, while the total accessible is higher than both. These cannot all be the same population, and no reconciliation is offered here because none has been measured.

This is recorded before the sweep runs so that its coverage figure is read against a denominator known to be contested, rather than presented as a fraction of a number nobody has checked. Establishing which population is correct is part of N8N-27, not a precondition someone waves through.

  • Human: FLIGHT<alex@devarno.com:2026-09-22> — the boundary above is the one N8N-27 is measured against

Unchecked, this decision is open and the sweep’s results are provisional against it.