NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Passkeys for Laravel

Passkey model & schema

RoundlyConsulting\Passkeys\Models\Passkey is a soft-deleting, polymorphic Eloquent model for a stored WebAuthn credential. Its owner is the authenticatable morph-to relation, and three scopes cover the lookups:

use RoundlyConsulting\Passkeys\Models\Passkey;

$passkey = Passkey::query()->forCredentialId($base64UrlId)->first();
$owned   = Passkey::query()->forUserHandle($handle)->get();
$mine    = Passkey::query()->ownedBy($user)->get();

$passkey->authenticatable;   // the owning model (MorphTo)
  • forCredentialId($id) — match by credential ID through its sha-256 lookup hash.
  • forUserHandle($handle) — match by discoverable-login user handle.
  • ownedBy($owner) — exactly one owner: its morph type and its key.
  • Passkey::hashCredentialId($id) — the static sha-256 lookup hash of a (possibly long) base64url credential ID.
  • advanceSignCount($signCount, $backupState) — one conditional UPDATE that moves sign_count forward, records backup_state and stamps last_used_at; false (with the model reloaded) when the counter did not advance.
  • recordUsage($backupState) — record backup_state and stamp last_used_at without moving the counter (a flag-policy regression).
  • Casts: transports (array), sign_count (integer), backup_eligible and backup_state (boolean), last_used_at (datetime).

Schema

ColumnTypeNotes
idbigint PK
authenticatable_type / _idmorph keyThe owning model; key type follows passkeys.key_type.
credential_idtextBase64url credential ID (up to ~1364 chars for roaming keys).
credential_id_hashchar(64), uniquesha-256 of credential_id — the real lookup key.
public_keytextBase64 of the raw COSE public key bytes.
user_handlestring, indexedOpaque discoverable-login handle.
transportsjsonb, nullableReported authenticator transports.
aaguiduuid, nullableAuthenticator model ID.
sign_countunsigned bigintDefault 0 — the clone-detection counter; it only ever moves forward.
namestring, nullableCosmetic label.
attestation_formatstring, nullableRecorded statement format (none, packed, apple, …).
attestation_typestring(16), nullableGrade of proof established (none, self, basic, anonca); null on rows registered before the column existed.
backup_eligible / backup_statebooleanDefault false. backup_eligible is fixed at registration; backup_state is updated by every accepted assertion.
last_used_attimestamp, nullableStamped on every successful sign-in.
created_at / updated_attimestamps
deleted_atsoft deletesRevoked credentials are soft-deleted.

The unique index sits on the fixed-length credential_id_hash rather than the variable-length credential_id, so roaming-key IDs are stored in full and the index fits every database’s key-length limit.

Using your own model

Point passkeys.model at a subclass of Passkey. The package resolves the configured class everywhere — the passkeys() relation, both ceremonies and the factory — so registration hands your class back and its model events fire. A class that is not Passkey or a subclass of it throws InvalidConfigurationException naming the key — it is never silently replaced by the packaged model. Keep the scopes, casts, hidden attributes and hashCredentialId() intact; the ceremonies rely on them:

// app/Models/Credential.php
use RoundlyConsulting\Passkeys\Models\Passkey;

class Credential extends Passkey
{
    // your accessors, relationships and model events
}

// config/passkeys.php
'model' => App\Models\Credential::class,

UUID and ULID owners

The authenticatable morph column defaults to bigint. If the models that own passkeys use UUID or ULID primary keys, set the key type before migrating — every owning model must share one key type, and an unrecognised value throws InvalidConfigurationException rather than silently building bigint keys:

PASSKEYS_KEY_TYPE=uuid   # bigint (default), uuid or ulid

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.