# Credential management

Listing, renaming and revoking passkeys from an account settings page.

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

### listCredentials
"
>

Every passkey on an account, shaped for display.

**Returns** `PublicCredentialInfo[]` — 
Sorted oldest first.

```js
const passkeysForUser = await passkeys.listCredentials(req.session.userId);
```

```js
[
  {
    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.

### renameCredential
"
>

Give a passkey a human label. Truncated to 128 characters.

```js
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.

- `unknown_credential` — 
No such credential, or it belongs to a different account.

### deleteCredential
"
>

Revoke a passkey.

```js
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.

- `unknown_credential` — 
No such credential, or it belongs to a different account.

- `last_credential` — 
This 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:

```js
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](/docs/server/adapters) 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".
  
  
- **backedUp** (`boolean`) — 
    Whether 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.
  
  
- **lastUsedAt** (`Date | undefined`) — 
    Lets a user recognise which entry is the laptop they still have. `undefined`
    means it has never been used to sign in.
  
  
- **createdAt** (`Date`) — 
    The other half of the same question. Two passkeys created minutes apart are
    probably the same device registered twice.
  
  
- **aaguid** (`string`) — 
    The 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.
