Skip to content

Auth

Passkeys_

Let users sign up and sign in with a passkey, a key pair stored on their device and unlocked with Face ID, Touch ID, a fingerprint, or a device PIN, on the web, Android, and Apple platforms.

9 min read

Raw

A passkey replaces a password with a key pair that lives on the user's device or in their password manager, such as iCloud Keychain or Google Password Manager. The user unlocks it with Face ID, Touch ID, a fingerprint, or the device PIN. The private key never leaves the device, and the public key that Appwrite stores is useless to an attacker. A passkey only works on the domain it was created for, so a look-alike phishing site cannot use it.

Passkeys follow the WebAuthn standard. Your app gets a challenge from Appwrite and hands it to the device. The user unlocks a passkey to answer it, and your app sends the signed answer back to Appwrite. One passkey works on your website and in your Android and iOS apps, because they all belong to the same domain.

Appwrite sends a challenge to your app, the user's device signs it with a passkey, and your app sends the signed answer back to Appwrite
Appwrite sends a challenge to your app, the user's device signs it with a passkey, and your app sends the signed answer back to Appwrite

Sign-in is usernameless. The user picks a passkey, and Appwrite finds the account it belongs to, so your sign-in screen needs no email field.

Passkeys are available on Appwrite Cloud and on self-hosted Appwrite 2.5 or later.

How it works

Passkeys are bound to a relying party ID: the domain of your app, such as example.com. The browser or the operating system only offers a passkey on that domain and the apps that the domain trusts.

Appwrite works out where each passkey may be used from your project's platforms. You do not maintain a separate list of origins.

Platform Origin Appwrite accepts Setup
Webhttps://app.example.comA web platform on the relying party ID or one of its subdomains.
Androidandroid:apk-key-hash:<hash>An Android platform with the SHA-256 fingerprints of your signing keys, and an assetlinks.json file on the relying party ID.
Applehttps://example.comAn Apple platform, an associated domain, and an apple-app-site-association file on the relying party ID.
Local developmenthttp://localhost:<port>A localhost web platform.

A native app needs a domain even if you have no website. The domain only has to serve the two .well-known files over HTTPS.

Turn on passkeys

The Enable passkeys dialog suggesting a domain from the project's web platforms
The Enable passkeys dialog suggesting a domain from the project's web platforms

  1. In the Appwrite Console, open your project and go to Auth, then Settings.
  2. Turn on Passkey.
  3. Choose the domain passkeys belong to. The dialog suggests domains from your web platforms, or you can type another one.
  4. Select Enable.

Use the registrable domain, such as example.com, rather than a subdomain like app.example.com. A passkey created on example.com also works on every subdomain that is a web platform, so you can add subdomains later without asking users to create new passkeys.

To review the setup, go to Auth > Policies > Passkeys. The Where passkeys work list shows every origin Appwrite accepts, computed from your platforms.

The relying party card listing where passkeys work
The relying party card listing where passkeys work

You can also configure passkeys with a server SDK and an API key with the policies.write and project.write scopes.

Node.js
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<PROJECT_ID>') // Your project ID
.setKey('<API_KEY>'); // Your secret API key
const project = new sdk.Project(client);
await project.updatePasskeyPolicy({ rpId: 'example.com' });
await project.updateAuthMethod({ methodId: 'passkey', enabled: true });

Local development

An app on http://localhost:5173 signed in with a passkey against Appwrite Cloud
An app on http://localhost:5173 signed in with a passkey against Appwrite Cloud

Add localhost as a web platform, and passkeys work on http://localhost on any port. Requests from localhost get their own relying party, localhost, so you can develop without a domain and without touching the production relying party ID.

Passkeys created on localhost only work on localhost. They are separate from your production passkeys, but they still count as registered passkeys: once one exists, Appwrite refuses to change the relying party ID until it is deleted. Set your production relying party ID before you test on localhost, and use localhost, not 127.0.0.1.

Set up your platforms

Web

Add a web platform for each hostname your app runs on, such as example.com and app.example.com. The hostname must be the relying party ID or one of its subdomains. A wildcard platform such as *.example.com covers every subdomain, but not example.com itself.

Browsers need no other setup. Passkeys work on HTTPS origins on the default port, and on http://localhost on any port for development.

Android

The Update app drawer with signing certificate fingerprints for an Android app
The Update app drawer with signing certificate fingerprints for an Android app

An Android app does not sign in from a web origin. Android identifies it by a hash of the certificate the app is signed with, so Appwrite needs that certificate's fingerprint.

  1. In the Appwrite Console, go to Apps and select your Android app. Add one if you have not yet.
  2. Under Signing certificate fingerprints, paste the SHA-256 fingerprint of each key that signs your app, one per line.
  3. Select Update.

Most apps have more than one signing key:

  • Debug builds are signed with the debug keystore on each developer's machine. Run ./gradlew signingReport in your Android project, which prints the SHA-256 fingerprint for every build variant.
  • Builds from Google Play are re-signed by Google with the app signing key. In Play Console, open App integrity, select Play app signing, and copy the SHA-256 fingerprint of the App signing key certificate.
  • Builds you install yourself carry the key that signed them, such as your upload key.

Android also checks that your domain trusts the app before it creates or uses a passkey. Host this file at https://example.com/.well-known/assetlinks.json, on your relying party ID, with your package name and the same fingerprints:

JSON
[
{
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": [
"14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
]
}
}
]

Add the Credential Manager library to your app, which shows the system passkey sheet:

Kotlin
dependencies {
implementation("androidx.credentials:credentials:1.6.0")
implementation("androidx.credentials:credentials-play-services-auth:1.6.0")
implementation("com.google.code.gson:gson:2.14.0")
}

The examples below use Gson to convert between the JSON strings Credential Manager works with and the maps the Appwrite SDK takes.

Apple

On iOS and macOS, the operating system confirms that your app may use your domain's passkeys, and the app signs in from the relying party ID itself. Appwrite accepts https://example.com once the project has an Apple platform.

  1. In the Appwrite Console, add an Apple platform with your app's bundle ID.
  2. In Xcode, open your target's Signing & Capabilities, add Associated Domains, and add webcredentials:example.com.
  3. Host this file at https://example.com/.well-known/apple-app-site-association, with your team ID and bundle ID:
JSON
{
"webcredentials": {
"apps": ["ABCDE12345.com.example.app"]
}
}

Apple fetches the file through its CDN, so changes can take a while to reach devices. During development, add ?mode=developer to the entitlement, such as webcredentials:example.com?mode=developer, to fetch it directly from your server.

Register a passkey

An Android app creating an account, then saving a passkey for it
An Android app creating an account, then saving a passkey for it

A passkey is added to an account the user is already signed in to. For a new user, create the account and a session first, then register the passkey. For an existing user, offer to add a passkey after they sign in.

Registration takes three steps:

  1. Call createPasskey(). Appwrite returns a passkeyId and the publicKey creation options.
  2. Pass the options to the platform's passkey API. The user confirms with a biometric or the device PIN.
  3. Send the resulting credential to updatePasskeyVerification(). Appwrite verifies it and stores the passkey.

The session must have signed in, or completed an MFA challenge, within the last 10 minutes, and step 3 must use the same session as step 1. The challenge expires after 5 minutes, and each user can have up to 10 passkeys, counting registrations still in progress.

JavaScript
import { Client, Account } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<PROJECT_ID>'); // Your project ID
const account = new Account(client);
const challenge = await account.createPasskey({ name: 'MacBook' });
const credential = await navigator.credentials.create({
publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(challenge.publicKey),
});
const passkey = await account.updatePasskeyVerification({
passkeyId: challenge.passkeyId,
credential: credential.toJSON(),
});

Sign in with a passkey

An Android app signing in with a saved passkey from the system sheet
An Android app signing in with a saved passkey from the system sheet

Sign-in needs no email or password. The user picks one of their passkeys for your domain, and Appwrite finds the account it belongs to.

  1. Call createPasskeyToken(). Appwrite returns a challenge with its $id and the publicKey request options.
  2. Pass the options to the platform's passkey API. The user picks a passkey and confirms.
  3. Send the credential to updatePasskeyToken(). Appwrite verifies it and returns a token with a userId and secret.
  4. Exchange the token for a session with createSession(), as with any token.

The token expires after one minute, and each challenge can only be used once. Each IP address can start and complete 10 passkey sign-ins per hour.

JavaScript
import { Client, Account } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<PROJECT_ID>'); // Your project ID
const account = new Account(client);
const challenge = await account.createPasskeyToken();
const credential = await navigator.credentials.get({
publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(challenge.publicKey),
});
const token = await account.updatePasskeyToken({
challengeId: challenge.$id,
credential: credential.toJSON(),
});
const session = await account.createSession({
userId: token.userId,
secret: token.secret,
});

Manage passkeys

A security settings page listing two passkeys with when each was added and last used
A security settings page listing two passkeys with when each was added and last used

Signed-in users can list, rename, and delete their own passkeys, for example from a security settings screen. Each passkey records its name and when it was last used. Like registration, deleting a passkey needs a session that signed in, or completed MFA, within the last 10 minutes.

import { Client, Account } from "appwrite";
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<PROJECT_ID>'); // Your project ID
const account = new Account(client);
// List the passkeys on the account
const { passkeys } = await account.listPasskeys();
// Rename a passkey
await account.updatePasskey({ passkeyId: passkeys[0].$id, name: 'Work laptop' });
// Delete a passkey
await account.deletePasskey({ passkeyId: passkeys[0].$id });

On the server, the Users service has the same operations for any user: users.listPasskeys(), users.getPasskey(), users.updatePasskey() and users.deletePasskey(). Use them to remove a lost device's passkey on a user's behalf. Deleting a user also deletes their passkeys.

Errors

Error Cause
user_auth_method_unsupportedPasskeys are turned off, or the project has no relying party ID with a matching platform.
user_reauthentication_requiredRegistering or deleting a passkey needs a session that signed in, or completed MFA, within the last 10 minutes.
user_invalid_tokenThe challenge expired after 5 minutes or was already used, registration finished from a different session, the passkey settings changed during the ceremony, or the sign-in token expired after one minute. Start again.
user_passkey_invalidThe credential failed verification, for example because the origin is not one Appwrite accepts or the signature does not match.
user_passkey_limit_exceededThe user already has 10 passkeys, counting registrations still in progress.
user_passkey_already_existsThis credential, or a passkey with this ID, is already registered.
user_passkey_not_foundNo passkey with this ID belongs to the user, or the registration is no longer pending.
user_blockedThe passkey belongs to a blocked user.
general_argument_invalidYou tried to change the relying party ID while users have passkeys, including localhost passkeys and registrations in progress.
general_rate_limit_exceededToo many passkey requests. Registration allows 10 per user and sign-in 10 per IP address each hour.

When registration or sign-in fails with user_passkey_invalid from a native app, compare the origin the platform sent with Where passkeys work under Auth > Policies > Passkeys. On Android, a missing origin usually means the app was signed with a key whose fingerprint is not on the Android platform.

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.