---
title: "Database schema & model — Attributes for Laravel | Roundly"
description: "The attributes and revisions tables, owner key types for UUID and ULID models, and swapping in your own Attribute model."
url: https://roundly-consulting.com/open-source/docs/attributes-for-laravel/database-schema
language: en
---

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

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

# Database schema & model

Every attribute is one row in a single polymorphic table — no column per attribute, no migration per new field. Table name from table:

| Column | Type | Notes |
| --- | --- | --- |
| `id` | bigint | Auto-incrementing key. |
| `owner_type, owner_id` | morph, nullable | The owning model. owner\_id is typed by key\_type. |
| `name` | string | Attribute name. |
| `value` | text, nullable | Storage string — JSON for arrays, UTC ISO-8601 for datetimes, ciphertext when encrypted. |
| `value_type` | string, nullable | AttributeType value (defaults to string). |
| `is_encrypted` | boolean | Whether value holds ciphertext. |
| `unique_hash` | string(64), nullable, unique | Hash of a value under a unique definition — a keyed blind index for encrypted values; null otherwise. Hidden when the model is serialized. |
| `meta` | jsonb, nullable | Metadata, cast to a Collection. |
| `created_at, updated_at, deleted_at` | timestamps | Soft deletes — removals are recoverable until pruned. |

attributes carries a unique index on (owner\_type, owner\_id, name) — soft-deleted rows included — so each owner holds exactly one row per attribute name: attaching an existing name updates that row in place, and attaching a detached name restores its row with a fresh value and meta. unique\_hash has its own unique index, which makes unique definitions a database guarantee (see Unique, required & defaults). The attribute\_revisions table (history.table) holds the optional audit trail, indexed on the same three columns — see History & audit trail.

## Owner key type

key\_type types the owner\_id column on both tables, so attributes can hang off models with non-integer keys. All your owner models must share one key type:

| key\_type | Owner columns | Use when owner models… |
| --- | --- | --- |
| `bigint` | nullableMorphs | use Laravel’s default auto-increment keys (the default; id is accepted as an alias) |
| `uuid` | nullableUuidMorphs | use HasUuids |
| `ulid` | nullableUlidMorphs | use HasUlids |

Set it before you run the migrations:

```bash
ATTRIBUTES_KEY_TYPE=uuid
```

```php
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Attributes\Contracts\HasAttributes as HasAttributesContract;
use RoundlyConsulting\Attributes\Traits\HasAttributes;

class Product extends Model implements HasAttributesContract
{
    use HasAttributes;
    use HasUuids; // pairs with ATTRIBUTES_KEY_TYPE=uuid
}
```

The value is case-insensitive. An unset or blank key means bigint; an unrecognized value throws InvalidConfigurationException when the migrations run instead of falling back.

## Swapping the attribute model

Point attributes.model at your own subclass to add casts, scopes or model events. Every read, write, scope, command and Attribute::collect() resolves the model through this key:

```php
// app/Models/CustomAttribute.php
namespace App\Models;

use RoundlyConsulting\Attributes\Models\Attribute;

class CustomAttribute extends Attribute
{
    protected static function booted(): void
    {
        parent::booted(); // keeps the package's unique-slot and definition hooks

        static::saved(function (self $attribute): void {
            // host-side model events, casts or scopes
        });
    }
}

// config/attributes.php
'model' => App\Models\CustomAttribute::class,
```

The class must be RoundlyConsulting\\Attributes\\Models\\Attribute or a subclass of it — anything else throws the toolkit’s InvalidConfigurationException naming the key; a foreign class is never silently replaced with the packaged one. The subclass inherits the attributes.table lookup, so there is no $table to set. If it overrides booted(), call parent::booted() first: the packaged model uses it to free a soft-deleted value’s unique slot, take the slot back on restore and apply the definition to a value set before its name and owner.

## 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/attributes-for-laravel.md)
