Everything an account settings page needs. All three methods take userId
first, and check it.
userId must come from your session
Every method here verifies that the credential belongs to the account you named. Without that, knowing a credential ID would be enough to rename or delete someone else's passkey, and credential IDs travel in plain sight on every login response.
Methods
listCredentialsmethod
listCredentials(userId: string): Promise<PublicCredentialInfo[]>Every passkey on an account, shaped for display.
Returns
PublicCredentialInfo[]Sorted oldest first.
const passkeysForUser = await passkeys.listCredentials(req.session.userId);[
{
id: 'kruh1zrhU4HRYwXpxyCsG6tqknL6sIAC81dnWTtYj18',
nickname: 'MacBook Pro',
deviceType: 'multiDevice',
backedUp: true,
transports: ['internal', 'hybrid'],
aaguid: '00000000-0000-0000-0000-000000000000',
createdAt: new Date('2026-02-11T09:14:22.000Z'),
lastUsedAt: new Date('2026-08-27T05:41:14.523Z'),
},
]Why this is a separate shape from PasskeyCredential
The stored record also holds userId and publicKey. Neither belongs in a
response to the browser: the public key is not secret but it is noise, and
echoing the user handle back into page markup makes it that much easier to
harvest. PublicCredentialInfo is the subset that is safe to render, so the
safe thing is also the easy thing.
renameCredentialmethod
renameCredential(userId: string, credentialId: string, nickname: string): Promise<void>Give a passkey a human label. Truncated to 128 characters.
await passkeys.renameCredential(req.session.userId, credentialId, 'Work laptop');Why nicknames are worth offering
Authenticators do not tell you what they are. Most consumer passkeys report an all-zero AAGUID on purpose, so a settings page can only ever say "passkey, created in February". With three of them listed, a user cannot tell which one to revoke after losing a laptop, and the safe-looking move is to revoke all of them.
Prompting for a nickname right after registration, while the user still knows which device they are holding, avoids that.
Throws
unknown_credentialNo such credential, or it belongs to a different account.
deleteCredentialmethod
deleteCredential(userId: string, credentialId: string): Promise<void>Revoke a passkey.
await passkeys.deleteCredential(req.session.userId, credentialId);Why deleting the last passkey is refused
If passkeys are the only way into an account, removing the last one locks the user out permanently, with one click, from a settings screen where the consequence is not obvious.
passkify throws configuration_error rather than allowing it. If your app also
supports passwords or another factor, and being left with zero passkeys is fine,
call store.deleteCredential(id) directly; the guard lives in the server
method, not the store.
Throws
unknown_credentialNo such credential, or it belongs to a different account.
last_credentialThis is the account's only passkey. A 409, not a fault: surface it in the UI.
A settings page
Server routes, using your existing auth middleware:
app.get('/settings/passkeys', requireAuth, async (req, res) => {
res.json(await passkeys.listCredentials(req.session.userId));
});
app.patch('/settings/passkeys/:id', requireAuth, async (req, res) => {
await passkeys.renameCredential(req.session.userId, req.params.id, req.body.nickname);
res.json({ ok: true });
});
app.delete('/settings/passkeys/:id', requireAuth, async (req, res) => {
await passkeys.deleteCredential(req.session.userId, req.params.id);
res.json({ ok: true });
});Or mount the adapter with getSessionUserId and get
the same three routes for free under /passkey/credentials.
What to surface to the user
deviceType'multiDevice' | 'singleDevice'multiDevice means the credential syncs and survives losing this device.
singleDevice does not. Say it in plain language: "Synced" versus "This
device only".
backedUpbooleanWhether it is currently backed up. A multiDevice credential that is not
yet backedUp means the user has not finished setting up their keychain,
and they are one lost phone from being locked out.
lastUsedAtDate | undefinedLets a user recognise which entry is the laptop they still have. undefined
means it has never been used to sign in.
createdAtDateThe other half of the same question. Two passkeys created minutes apart are probably the same device registered twice.
aaguidstringThe authenticator model UUID, all zeroes for most consumer passkeys. Do not build UI that depends on it being meaningful.
Prompt when there is only one
The cheapest thing you can do for account recovery is notice
listCredentials(userId).length === 1 and ask the user to add a second passkey
on a different device. It costs one banner and removes the most common way
people lose access.