---
title: "Passkey model & schema — Passkeys for Laravel | Roundly"
description: "The soft-deleting, polymorphic Passkey model — scopes, casts, hidden attributes, the table schema and swapping in your own model."
url: https://roundly-consulting.com/open-source/docs/passkeys-for-laravel/passkey-model
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Passkeys for Laravel](https://roundly-consulting.com/open-source/docs/passkeys-for-laravel.md)

# 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:

```php
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

| Column | Type | Notes |
| --- | --- | --- |
| `id` | bigint PK | |
| `authenticatable_type / _id` | morph key | The owning model; key type follows passkeys.key\_type. |
| `credential_id` | text | Base64url credential ID (up to ~1364 chars for roaming keys). |
| `credential_id_hash` | char(64), unique | sha-256 of credential\_id — the real lookup key. |
| `public_key` | text | Base64 of the raw COSE public key bytes. |
| `user_handle` | string, indexed | Opaque discoverable-login handle. |
| `transports` | jsonb, nullable | Reported authenticator transports. |
| `aaguid` | uuid, nullable | Authenticator model ID. |
| `sign_count` | unsigned bigint | Default 0 — the clone-detection counter; it only ever moves forward. |
| `name` | string, nullable | Cosmetic label. |
| `attestation_format` | string, nullable | Recorded statement format (none, packed, apple, …). |
| `attestation_type` | string(16), nullable | Grade of proof established (none, self, basic, anonca); null on rows registered before the column existed. |
| `backup_eligible / backup_state` | boolean | Default false. backup\_eligible is fixed at registration; backup\_state is updated by every accepted assertion. |
| `last_used_at` | timestamp, nullable | Stamped on every successful sign-in. |
| `created_at / updated_at` | timestamps | |
| `deleted_at` | soft deletes | Revoked 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:

```php
// 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:

```bash
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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/passkeys-for-laravel.md)
