> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sinemacula.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Device Tracking Configuration

> Configure the device model, table name, refresh key column, and last-seen throttle for Laravel Authentication's device tracking feature.

Device tracking associates each token issuance with a persisted device record in your database. It powers refresh-token rotation — each device row stores a hashed rotation digest that must match the presented refresh token, enabling replay detection and per-device revocation. When a device record is resolved on the bearer path, the package debounces `last_logged_in_at` writes to avoid hot-spot database pressure on high-traffic APIs.

Device tracking is **opt-in**. If you only need short-lived access tokens without refresh (the common pattern for M2M APIs), you can skip the migration and the `HasDevices` contract entirely. See [access-only mode](#access-only-mode) below.

## Config block

```php config/authentication.php theme={null}
'device' => [
    'model'                      => env('AUTHENTICATION_DEVICE_MODEL', \SineMacula\Laravel\Authentication\Models\Device::class),
    'table'                      => env('AUTHENTICATION_DEVICE_TABLE', 'devices'),
    'refresh_key_column'         => env('AUTHENTICATION_DEVICE_REFRESH_KEY_COLUMN', 'refresh_key'),
    'last_seen_throttle_seconds' => (int) env('AUTHENTICATION_DEVICE_LAST_SEEN_THROTTLE_SECONDS', 60),
],
```

## Fields

<ParamField body="model" type="string" default="SineMacula\Laravel\Authentication\Models\Device">
  The fully-qualified class name of the Eloquent model used for device persistence. Sourced from `AUTHENTICATION_DEVICE_MODEL`. The default is the package's shipped `Device` model. Swap this to a custom class when you need additional columns, relationships, or behaviour — the custom model must implement the `EloquentDevice` contract.

  ```dotenv .env theme={null}
  AUTHENTICATION_DEVICE_MODEL=App\Models\Device
  ```
</ParamField>

<ParamField body="table" type="string" default="devices">
  The database table that stores device records. Sourced from `AUTHENTICATION_DEVICE_TABLE`. Override this when your schema uses a different table name. The shipped `Device` model reads this value lazily on each instantiation, so runtime config swaps (such as in multi-tenancy scenarios or tests) take effect immediately.

  ```dotenv .env theme={null}
  AUTHENTICATION_DEVICE_TABLE=devices
  ```
</ParamField>

<ParamField body="refresh_key_column" type="string" default="refresh_key">
  The column on the device table that stores the hashed refresh rotation digest. Sourced from `AUTHENTICATION_DEVICE_REFRESH_KEY_COLUMN`. The package reads and writes this column during refresh-token rotation; it must exist in the migration and be nullable (it is `null` before a refresh token has been issued for a device).

  ```dotenv .env theme={null}
  AUTHENTICATION_DEVICE_REFRESH_KEY_COLUMN=refresh_key
  ```
</ParamField>

<ParamField body="last_seen_throttle_seconds" type="number" default="60">
  The minimum number of seconds between `last_logged_in_at` writes for a given device on the bearer path. When a bearer request resolves a persisted device, the package updates `last_logged_in_at` only if the stored value is older than this threshold. Set to `0` to write on every request (not recommended on high-traffic APIs). Set to a larger value to reduce write frequency further.

  ```dotenv .env theme={null}
  AUTHENTICATION_DEVICE_LAST_SEEN_THROTTLE_SECONDS=60
  ```
</ParamField>

## The shipped Device model

The package provides `SineMacula\Laravel\Authentication\Models\Device` as the default device adapter. Key characteristics:

* **UUID v7 primary key** — generated on insert via Laravel's `HasUuids` concern, so device IDs are time-ordered and globally unique without a sequence dependency.
* **Polymorphic `authenticatable` relation** — a `morphTo` that can point at any identity model, so one `devices` table serves multiple authenticatable types in the same app.
* **Not `final`** — you can subclass it to add columns or relationships while retaining the built-in `EloquentDevice` behaviour.

The model's fillable attributes map directly to the columns created by the published migration:

| Column                 | Type              | Notes                                            |
| ---------------------- | ----------------- | ------------------------------------------------ |
| `id`                   | `uuid`            | UUID v7, primary key                             |
| `authenticatable_type` | `string`          | Morph type                                       |
| `authenticatable_id`   | `string`          | Morph ID                                         |
| `os`                   | `string`          | Client operating system label                    |
| `refresh_key`          | `string\|null`    | Hashed rotation digest; null until first refresh |
| `revoked_at`           | `timestamp\|null` | Non-null means the device is revoked             |
| `last_logged_in_at`    | `timestamp\|null` | Debounced last-seen timestamp                    |
| `last_mfa_verified_at` | `timestamp\|null` | Optional MFA timestamp                           |

## Publishing and running the migration

```bash theme={null}
php artisan vendor:publish --tag=authentication-migrations
php artisan migrate
```

This creates the `devices` table with the schema above. If you change `device.table`, update the migration's `Schema::create` call before running it.

## Using a custom device model

To use a custom device model, create a class that implements `SineMacula\Laravel\Authentication\Contracts\EloquentDevice` and point `device.model` at it:

```php config/authentication.php (or .env) theme={null}
AUTHENTICATION_DEVICE_MODEL=App\Models\CustomDevice
```

The simplest path is to subclass the shipped model:

```php app/Models/CustomDevice.php theme={null}
use SineMacula\Laravel\Authentication\Models\Device;

class CustomDevice extends Device
{
    protected $fillable = [
        ...parent::$fillable,
        'push_token',
        'platform',
    ];
}
```

If you need a fully independent implementation, implement `EloquentDevice` directly. The contract requires the persistence accessors the package needs for rotation and last-seen tracking — refer to the contract interface for the required method signatures.

## Enabling device tracking on your identity model

To opt into device tracking and refresh-token rotation, implement `HasDevices` on your identity model and define the `devices()` morph-many relationship:

```php app/Models/User.php theme={null}
use Illuminate\Database\Eloquent\Relations\MorphMany;
use SineMacula\Laravel\Authentication\Contracts\HasDevices;
use SineMacula\Laravel\Authentication\Models\Device;

class User extends \Illuminate\Foundation\Auth\User implements Identity, Principal, HasDevices
{
    use Authenticatable, ActsAsPrincipal;

    public function devices(): MorphMany
    {
        return $this->morphMany(Device::class, 'authenticatable');
    }
}
```

Without `HasDevices`, the guard skips device resolution and refresh is unavailable.

## Access-only mode

<Note>
  Device tracking and refresh-token rotation are entirely optional. If your use case only requires short-lived access tokens — common for M2M APIs, internal service calls, and simple backends — you can skip the `devices` table and the `HasDevices` contract.

  To run in access-only mode:

  1. Publish only the config: `php artisan vendor:publish --tag=authentication-config`. Skip the `authentication-migrations` tag.
  2. Do not implement `HasDevices` on your identity model.
  3. Issue access tokens with `null` as the device argument: `Auth::jwt('api')->issueAccessToken($identity, $principal, null)`.
  4. Do not call `$guard->refresh(...)`. Clients re-authenticate when their access token expires.

  In this mode the package never touches a `devices` table. `Auth::device()` returns `null`, but the full contextual surface (`Auth::identity()`, `Auth::principal()`, `Auth::tenant()`, `Auth::type()`) still works normally. You can add the migration and refresh flow later — it is additive, not a rewrite.
</Note>
