# When you cannot use or have lost your passkey

> Which environments can use passkeys, how to avoid getting locked out, and how to regain admin access with a manual change in D1.

The admin screen accepts only passkeys. This page covers which environments can use them, how to avoid getting locked out, and how to [regain admin access](#regain-admin-access) if you lose every passkey. How sign-in works is in [Passkey sign-in](https://goodnight-web.dev/docs/how-it-works/passkeys.md).

## Can you use a passkey?

Current browsers and operating systems mostly can. On Windows, what works depends on the OS version and on where the passkey is saved.

- **Chrome on Windows 10 or later can save passkeys in Google Password Manager.** They are available on every device where you are signed in to Chrome or Android with the same Google Account. This does not need Windows Hello. See [Manage passkeys in Chrome](https://support.google.com/chrome/answer/13168025) and [Passkey support on Android and Chrome](https://developers.google.com/identity/passkeys/supported-environments).
- **Passkey management and passkey autofill in Chrome need Windows 11, version 22H2 or later.** Microsoft says passkeys can be used in all supported versions of Windows, and that the native management screen starts with Windows 11 22H2. See [Support for passkeys in Windows](https://learn.microsoft.com/en-us/windows/security/identity-protection/passkeys/).
- **A passkey saved only in Windows Hello is not synced or backed up.** If the PC is lost or Windows is reinstalled, you cannot recover it.

If your computer cannot create or sync a passkey on its own, use one of these instead:

- **A password manager that stores passkeys**, such as 1Password or Bitwarden.
- **A phone or tablet.** When Chrome asks, choose **Use a phone or tablet** and scan the QR code. The passkey is saved on the phone. On Windows, both devices need Bluetooth and an internet connection.
- **A USB security key.** The admin screen requires user verification, so the key needs a PIN. The passkey stays on the key, so if you lose the key, use the recovery steps below.

## Avoid getting locked out

- **Save the passkey where it syncs.** When your browser asks where to save the passkey during setup, pick iCloud Keychain, Google Password Manager, or a password manager such as 1Password or Bitwarden.
- **Add your domain first.** A passkey is bound to one domain, its relying party ID. A passkey registered on a `workers.dev` address does not work on your own domain. Add your domain before you [create the first admin](https://goodnight-web.dev/docs/how-it-works/first-admin.md).
- **There is no second passkey yet.** The admin screen has no page to add one. A page for it is planned, with no date.

## Regain admin access [#regain-admin-access]

If you cannot sign in with any passkey, you can reopen setup by changing the D1 database by hand. Use this after a lost device, a reinstalled OS, a deleted passkey, or a move to another domain.

> [!WARNING]
>   This needs access to the Cloudflare account that owns the Worker and its
>   database. That account is the root of trust: anyone with it can run these
>   steps. See [Security model](https://goodnight-web.dev/docs/security.md#database-access).

The change deletes every session and every passkey, so all admins are signed out and none of the old passkeys work. Inquiries and settings other than `setup_closed_at` are not touched.

You need:

- Wrangler, signed in to your Cloudflare account and run in your project directory. `DB` in the command below is the D1 binding name in `wrangler.jsonc`. The database name works too.
- An address that is in `ADMIN_EMAILS`.

<Steps>

<Step>

### Have the setup token ready

The setup screen accepts `SETUP_TOKEN` when it is set, and `APP_SECRET` otherwise.

- **Your site uses `APP_SECRET` and no `SETUP_TOKEN`:** there is nothing to set. Have the value of `APP_SECRET` ready.
- **You no longer have a copy of `APP_SECRET`:** do not change it, because a new value changes IP hashes. Set `SETUP_TOKEN` for the recovery instead, with the commands below.
- **Your site has no `APP_SECRET` and you deleted `SETUP_TOKEN` after the first setup:** set `SETUP_TOKEN` again with the commands below. Without a setup token, the setup screen reports `APP_SECRET` as missing; setting either one works.

`SETUP_TOKEN` must be 32 characters or more. You can also set it in the Cloudflare dashboard.

```sh
openssl rand -base64 32
npx wrangler secret put SETUP_TOKEN
```

</Step>

<Step>

### Clear passkeys, sessions, and the setup lock

```sh
npx wrangler d1 execute DB --remote --command "DELETE FROM passkey; DELETE FROM session; DELETE FROM settings WHERE key = 'setup_closed_at';"
```

Run all three statements, in this order:

- **Passkeys go first**, so the old passkey cannot sign in and create a new session while the command runs. Setup is still closed at this point.
- **Sessions go next**, so no old session is left when setup opens. A session that survives can register a passkey on the setup screen without the setup token.
- **The setup lock goes last**, which opens setup.
- **Deleting only the passkeys does not work.** `settings.setup_closed_at` stays, so setup stays closed.
- **Deleting only `setup_closed_at` does not work.** Setup stays closed while any passkey exists, and the old passkey still signs in.

</Step>

<Step>

### Run setup again

Open `https://<your domain>/admin/` on the domain you want the passkey bound to. If Cloudflare Access is in front of `/admin`, sign in through Access first. Enter the setup token (`SETUP_TOKEN` if it is set, otherwise `APP_SECRET`) and an address from `ADMIN_EMAILS`, select **Continue**, and register a passkey in a provider that syncs.

With the same address as before, setup reuses the existing user. With another address from `ADMIN_EMAILS`, setup creates a new user, and the old user stays in the database without a passkey and cannot use the admin screen.

Once the new passkey is saved, setup closes again and `/api/setup` returns 403.

</Step>

<Step>

### Clean up

If you set `SETUP_TOKEN` for the recovery, delete it again. Do not delete `APP_SECRET`. Also delete the old passkey for this site from your password manager or browser. If it is still listed and you choose it, sign-in fails with "this passkey is not registered for this site".

</Step>

</Steps>

## What was verified

These steps were run against a local D1 database with `wrangler dev` and a virtual authenticator in Chrome. The statements were later put in the order shown above (passkeys before sessions); the reordered command was run with `wrangler d1 execute --local` and in the server's tests (`passkey-recovery.http.test.ts`) with the same results. After the change, setup was open, a new passkey could be registered with the same address, and the admin API worked again. Earlier sessions got 401, and the old passkey could not sign in. The two partial deletions above did not reopen setup. The server package's automated tests run the same statements on a site with `APP_SECRET` only, on a site with the four separate secrets, and with `SETUP_TOKEN` set for the recovery and deleted afterwards.

These steps were not run against a deployed Cloudflare Worker or with `--remote`. A move from one domain to another could not be tested locally either, because the local check can run only on `localhost`.
