[Guide] Build a Password Manager (Route Vault + Scope) - implementation notes
The Password Manager guide uses a clear architecture: vault-backed storage plus a mirror surface for UI, scope-based configuration for posture (environment, features, maybe policy), and stored-derived outputs like a "health score" so the UI stays consistent and explainable. I'm curious how people made this feel safe and debuggable without turning everything into security theater.
How did you design the vault + mirror boundary so UI is fast but secrets still feel protected? What did scope control in practice (policy, environment, risk posture), and how did you keep it explicit for users/support? Did you store a derived health score, and what inputs did you consider part of that contract? What evidence/log lines were most helpful when debugging "why is this entry flagged" or "why can't I access this item"?
Comments (22)
Back to latestVault + mirror is the only way I’ve seen a secrets UI feel responsive without doing something reckless.
The mirror needs to be a strictly limited projection, otherwise you just copied secrets into another store.
We treated every vault read/write as an intent with an explicit lane and we log the contract transitions:
txt
[tips] intent=vault:unlock lane=pending reason=user:submit scope=policy:strict
[tips] intent=vault:unlock lane=ok reason=key:verified mirrorRev=12
[tips] derive=healthScore reason=mirror:update mirrorRev=12 score=0.72
Then we render data-vault-lane, data-mirror-rev, and data-scope-policy. Without that, the system feels like magic.
Counterpoint: scope config can become a hidden global if you aren’t careful.
We render scope as user-visible copy (not just debug evidence) when it changes behavior. If policy is strict, users should know what strict means.
Yes. In a password manager, policy isn’t just implementation. It’s part of the product contract.
If policy affects access or warnings, hiding it is a UX bug.
The mirror boundary was easiest when we decided the mirror *never* contains secrets, only derived metadata.
Title, username hash, lastUpdated, risk flags, and a small preview summary that can’t reconstruct the secret.
Long-form: security UX is mostly about predictability.
If the app randomly denies access or randomly flags entries, users will either ignore it or fear it. Neither is good.
The guide’s evidence posture—lanes, reasons, and explicit scope—turns security behavior into explainable behavior.
Derived health score is useful if it has a stable story. We logged the inputs so it wasn’t a mystery:
txt
[tips] healthScore score=0.72 reason=derive inputs="reuse:1 weak:0 old:1"
The string of inputs isn’t perfect, but it makes the score auditable.
Counterpoint: scoring can turn into security theater if it’s not actionable.
We tied the score to actions (rotate password, enable 2FA) and logged the action reasons so UX was coherent.
We also made vault unlock idempotent and safe to replay after refresh. The app replays the lane and logs a replay line so it doesn’t feel like it "remembered" secrets magically:
txt
[tips] replay intent=vault:unlock lane=ok reason=refresh mirrorRev=12Long-form counterpoint: a vault UI can accidentally leak secrets through derived outputs (e.g. password length, pattern).
We audited derived fields and made a policy: derived outputs must be non-reconstructive and should be explainable in one sentence.
Docs tie-in: security posture + state/vault concepts are the backbone here.
Related: Security and Safety and State Management (Best Practices).
We used scope to model locale and policy simultaneously (scope as composite).
The key was making precedence explicit and logged, otherwise users couldn’t tell why warnings changed between environments.
Counterpoint: too much evidence can be risky if it ends up in screenshots users share.
We kept evidence behind a debug toggle and made sure the debug view never shows secret-bearing values.
Long-form: vault/mirror is also a performance posture.
If the UI reads encrypted storage directly, you’ll end up doing crypto work in render or effects and the whole app feels sluggish. Mirror avoids that by making a stable render-ready contract.
We added a "why blocked" evidence string for policy decisions (e.g. export disabled, copy disabled).
If the app blocks an action without a reason, users assume it’s broken.
Long-form: the guide reads like it’s trying to make security behavior narratable.
That’s the right goal. If behavior can be narrated with one log line and one evidence line, it’s much harder for the system to surprise users.
We also stored derived "duplicate password" flags as a separate lane, because duplicates can be transient (import flows).
Treating it as a lane prevented the UI from flickering warnings without explanation.
Counterpoint: vault unlock flows can be hostile if they’re too strict (timeouts, forced reauth).
We encoded timeouts as posture (strict vs relaxed) and made it visible. That made the tradeoff explicit rather than "the app hates me".
We used the [tips] log format for audits too: last unlock, last export attempt, last policy denial.
It turned security incidents into timelines instead of detective work.
Long-form counterpoint: a password manager is a worst-case product for "implicit state" because users are already anxious.
Explicit lanes, explicit scope, and visible reasons are not optional. They are UX fundamentals.
If you implement this, start by deciding which derived fields are allowed into the mirror.
That one decision determines whether the system feels safe and whether the rest of the architecture stays disciplined.
We also stored derived "vault health" separate from password health (unlock failures, mirror drift, token store version).
It kept the UI from conflating user behavior with system behavior.
Counterpoint: some teams will cargo-cult vault/mirror and then put secrets in the mirror anyway "for convenience".
We enforced it in code review: mirror never contains secrets, and any derived field must be justified as non-sensitive and non-reconstructive.