Storage Adapters
A storage adapter is the bridge between webauthn-server-buildkit and your database. The library never assumes a particular store — it defines a small StorageAdapter interface with four sections (users, credentials, challenges, sessions) and ships an in-memory implementation for development.
The built-in memory adapter
import { MemoryStorageAdapter } from 'webauthn-server-buildkit';
const storage = new MemoryStorageAdapter();
MemoryStorageAdapter keeps everything in process memory. It is ideal for local development, examples, and tests, but state is lost on restart and is not shared across instances — so it is not for production.
The StorageAdapter interface
You implement four groups of async methods:
interface StorageAdapter {
users: {
findById(id: string | number): Promise<UserModel | null>;
findByUsername(username: string): Promise<UserModel | null>;
create(user: Omit<UserModel, 'id'>): Promise<UserModel>;
update(id: string | number, updates: Partial<UserModel>): Promise<UserModel | null>;
delete(id: string | number): Promise<boolean>;
};
credentials: {
findById(id: Base64URLString): Promise<WebAuthnCredential | null>;
findByUserId(userId: string | number): Promise<WebAuthnCredential[]>;
findByWebAuthnUserId(webAuthnUserId: Base64URLString): Promise<WebAuthnCredential[]>;
create(credential: Omit<WebAuthnCredential, 'createdAt'>): Promise<WebAuthnCredential>;
updateCounter(id: Base64URLString, counter: number): Promise<boolean>;
updateLastUsed(id: Base64URLString): Promise<boolean>;
delete(id: Base64URLString): Promise<boolean>;
deleteByUserId(userId: string | number): Promise<boolean>;
};
challenges: {
create(challenge: ChallengeData): Promise<boolean>;
find(challenge: string): Promise<ChallengeData | null>;
delete(challenge: string): Promise<boolean>;
deleteExpired(): Promise<boolean>;
};
sessions: {
create(sessionId: string, data: SessionData): Promise<boolean>;
find(sessionId: string): Promise<SessionData | null>;
update(sessionId: string, data: Partial<SessionData>): Promise<boolean>;
delete(sessionId: string): Promise<boolean>;
deleteExpired(): Promise<boolean>;
deleteByUserId(userId: string | number): Promise<boolean>;
};
}
Implementation notes
credentials.findByIdis the hot path on authentication — index the credential ID column.credentials.findByWebAuthnUserIdpowers usernameless login — indexwebAuthnUserIDtoo.challenges.find/challenges.deleteback the single-use challenge enforcement. Storing anexpiresAtand honoring it letsdeleteExpiredandcleanup()keep the table small.publicKeyis aUint8Array— store it asbytea(Postgres), aBuffer/binary field (MongoDB), or base64url text. Round-trip it exactly.- Return
null(not a thrown error) fromfind*methods when a record is absent; the library treatsnullas "not found".
Reference adapter examples
The package repository ships full reference adapters you can adapt:
- PostgreSQL —
docs/examples/storage-adapters/postgresql-adapter.ts - Redis (sessions/challenges) —
docs/examples/storage-adapters/redis-session-adapter.ts - MongoDB —
docs/examples/storage-adapters/mongodb-adapter.ts
See them in the source repository.
Frequently asked questions
Can I split storage across databases?
Yes. Nothing requires all four sections to live in one store — a common pattern is durable user/credential records in Postgres with ephemeral challenges and sessions in Redis. As long as each method honors its contract, mix freely.
Do I have to implement every method?
Implement every method your flows actually call. The full ceremony plus sessions uses all of them; a registration-only prototype might leave session methods as simple stubs. The TypeScript interface lists the complete surface.
How do I delete a user's data on account deletion?
Call credentials.deleteByUserId and sessions.deleteByUserId, then users.delete. This removes their passkeys and active sessions in one sweep.