---
title: "Database schema & key types — Likes for Laravel | Roundly"
description: "The single polymorphic likes table, soft deletes, UUID/ULID key types and swapping in your own Like model."
url: https://roundly-consulting.com/open-source/docs/likes-for-laravel/database-schema
language: en
---

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

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

# Database schema & key types

One polymorphic table records who liked what. Its name comes from likes.table; the columns are fixed:

| Column | Type | Notes |
| --- | --- | --- |
| `id` | auto-increment | Always an auto-incrementing key, whatever key\_type says. |
| `actor_type, actor_id` | morph pair (unique key) | The model that gave the like. actor\_id is typed by key\_type. |
| `likeable_type, likeable_id` | morph pair (unique key) | The model that received it. likeable\_id is typed by key\_type. |
| `type` | string(64), default like | The reaction type — at most 64 characters, so the unique key fits InnoDB’s index limit on MySQL and MariaDB. |
| `created_at, updated_at` | timestamps | created\_at drives the trending window. |
| `deleted_at` | soft delete | Set on unlike; a re-like restores the row. |

A unique key, likes\_actor\_likeable\_type\_unique — prefixed with your table name when you change it — covers (actor\_type, actor\_id, likeable\_type, likeable\_id, type) — soft-deleted rows included — so the database itself keeps one row per actor, likeable and reaction type, and every verb and check hits that exact lookup. The published migration in short:

```php
$name = LikesConfig::table();                     // likes.table, read strictly

// Throws for an unrecognized value, so a typo in the host's config fails
// the migration instead of quietly building bigint columns.
$keyType = KeyType::fromConfig('likes.key_type');

Schema::create($name, function (Blueprint $table) use ($name, $keyType): void {
    $table->id();
    $table->morphKey('actor', $keyType, nullable: false);     // actor_type + actor_id
    $table->morphKey('likeable', $keyType, nullable: false);  // likeable_type + likeable_id
    $table->string('type', 64)->default('like');              // 64 keeps the unique index within InnoDB's key limit
    $table->timestamps();
    $table->softDeletes();

    // One row per actor + likeable + reaction type, soft-deleted rows included.
    $table->unique(
        ['actor_type', 'actor_id', 'likeable_type', 'likeable_id', 'type'],
        $name.'_actor_likeable_type_unique',
    );
});
```

morphKey() is a Blueprint macro from the Roundly package toolkit. The service provider registers it on boot, so it exists before php artisan migrate runs.

## Key types

key\_type types both morph pairs, so actors and likeables share one key type. Pick the one that matches your models:

| key\_type | Morph columns | Use when your models… |
| --- | --- | --- |
| `bigint` | morphs() — unsigned big integer ids | use Laravel’s default auto-incrementing keys (the default) |
| `uuid` | uuidMorphs() | use HasUuids |
| `ulid` | ulidMorphs() | use HasUlids |

Set it before you publish and run the migration:

```bash
LIKES_KEY_TYPE=uuid   # bigint (default) | uuid | ulid
```

```php
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Likes\Traits\GivesLikes;
use RoundlyConsulting\Likes\Traits\HasLikes;

class User extends Model
{
    use GivesLikes, HasUuids;
}

class Post extends Model
{
    use HasLikes, HasUuids;
}
```

A blank value (LIKES\_KEY\_TYPE=) is not set and keeps bigint. Any other value throws InvalidConfigurationException when the migration runs, instead of silently building bigint columns.

## Soft deletes

Unliking soft-deletes the row, and liking again restores it instead of inserting a duplicate — each actor keeps a single row per likeable and reaction type. Checks, counts, scopes and summaries all ignore soft-deleted rows.

## Custom model & table

Extend the package model to add your own behaviour, then point the config at it:

```php
namespace App\Models;

use RoundlyConsulting\Likes\Models\Like as BaseLike;

class Like extends BaseLike
{
    // your own casts, relations or scopes
}
```

```php
// config/likes.php
'model' => App\Models\Like::class,
```

The model reads likes.table unless your subclass sets $table explicitly — an explicit $table wins, as Eloquent intends, for writes and for every scope (withLikedState(), orderByLikeScore(), orderByTrending(), likesOf() and likedItems() included). A configured class that isn’t the package model or a subclass of it throws InvalidConfigurationException naming the key — it is never silently replaced by the packaged model.

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