---
title: "Models & schema — Approvals for Laravel | Roundly"
description: "The four Eloquent models, their tables, relations and scopes, the morph key type, and how to swap in your own subclasses."
url: https://roundly-consulting.com/open-source/docs/approvals-for-laravel/models-and-schema
language: en
---

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

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

# Models & schema

Four models back the package. Each uses soft deletes, ships a factory and is swappable via config:

| Config key | Model | Table |
| --- | --- | --- |
| `model` | Approval | approvals |
| `request_model` | ApprovalRequest | approval\_requests |
| `stage_model` | ApprovalRequestStage | approval\_request\_stages |
| `delegation_model` | ApprovalDelegation | approval\_delegations |

## Tables

| Table | Columns |
| --- | --- |
| `approvals` | id, actor (morph), approvable (morph), status (default approved), reason, approval\_request (nullable morph), decision\_scope, live (nullable), decided\_by (nullable morph), weight (default 1), approval\_request\_stage\_id, decided\_at, expires\_at, timestamps, deleted\_at — unique index approvals\_live\_decision\_unique |
| `approval_requests` | id, subject (nullable morph), rule, quorum, required\_approvers, approvers (nullable json), status (default pending), staged (default false), reject\_on\_stage\_rejection (default true), workflow, resolved\_at, expires\_at, timestamps, deleted\_at |
| `approval_request_stages` | id, approval\_request\_id, position, name, rule, quorum, required\_approvers, approvers (nullable json), status (default pending), opened\_at, cleared\_at, timestamps, deleted\_at |
| `approval_delegations` | id, delegator (morph), delegate (morph), starts\_at, ends\_at, revoked\_at, timestamps, deleted\_at |

decision\_scope and live back the one-live-decision-per-slot rule: the unique index covers the approvable, the actor, decision\_scope (the request and stage, empty for a standalone decision) and live, which turns NULL once a decision is withdrawn, superseded, expired or soft-deleted. On MySQL and PostgreSQL NULLs never collide in a unique index, so retired rows pile up freely; SQL Server treats them as equal, so it is not supported. The approvers JSON column holds the named approvers with the weight each carried at open — NULL when a request or stage names nobody.

The composite index approvals\_actor\_approvable\_status\_index covers actor\_id, actor\_type, approvable\_id and approvable\_type — despite its name not status, which has its own index. Up to 1.1.0 it included status, which pushed it past MySQL’s 3072-byte key limit under utf8mb4; an install that ran that older migration keeps the five-column index, which serves the same queries (see Installation).

## Relations & scopes

- Approval — actor(), approvable(), decidedBy() and approvalRequest() (morph-to) and stage() (belongs-to). Scopes: pending(), approved(), rejected(), expired(), active() (pending or approved), live() (each slot’s live decision), inForce(?$moment) and expiringBefore($moment). Methods: isInForce(?$moment) and wasDelegated().
- ApprovalRequest — subject() (morph-to), decisions() (morph-many) and stages() (has-many, ordered by position). Methods: namedApprovers(), approversFor(?$stage), hasNamedApprover($model), currentStage(), resolve(), isOverdue(?$moment), lapseIfOverdue(?$moment) and approvalProgress().
- ApprovalRequestStage — request() (belongs-to) and decisions() (has-many). Methods: namedApprovers(), hasNamedApprover($model), isOpen() and markOpened().
- ApprovalDelegation — delegator() and delegate() (morph-to). Scopes active(?$moment) and inForceOrScheduled(?$moment); methods isActiveAt(?$moment) and revoke(?$at).

```php
use RoundlyConsulting\Approvals\Models\Approval;
use RoundlyConsulting\Approvals\Models\ApprovalDelegation;

Approval::query()->approved()->inForce()->whereMorphedTo('approvable', $post)->get();
Approval::query()->live()->whereMorphedTo('actor', $user)->get();   // each slot's live decision
Approval::query()->active()->expiringBefore(now())->get();

ApprovalDelegation::query()->active()->whereMorphedTo('delegate', $assistant)->get();
ApprovalDelegation::query()->inForceOrScheduled()->whereMorphedTo('delegator', $manager)->get();
```

## Morph key type

key\_type types the seven polymorphic id columns — actor, approvable, approval\_request and decided\_by on approvals, subject on approval\_requests, and delegator and delegate on approval\_delegations. Your morph targets must share one key type:

| key\_type | Morph id column | Use when your models… |
| --- | --- | --- |
| `bigint` | bigint | use Laravel’s default auto-incrementing keys (the default) |
| `uuid` | uuid | use HasUuids |
| `ulid` | ulid (26 chars) | use HasUlids |

```bash
APPROVALS_KEY_TYPE=uuid   # bigint (default), uuid or ulid — set before you migrate
```

Unset or blank reads as bigint; any other value throws InvalidConfigurationException when the migrations run, so a typo never silently builds bigint columns for UUID- or ULID-keyed models. The package’s own ids — including approval\_request\_stage\_id — are always auto-incrementing. That is why, on PostgreSQL with uuid, migration 0007 makes approval\_request\_id a bigint: it points at the bigint approval\_requests.id.

Nothing checks key\_type against the models it points at, including models another package owns. requests-for-laravel’s Request is one: its id type, requests.primary\_key\_type, must equal approvals.key\_type. Neither package checks the pair at boot or at migrate time, so a mismatch shows up only when a write fails — on PostgreSQL, the first request with approvers or the first decision fails (invalid input syntax for type uuid), while MySQL and SQLite store the value without an error. Set both before you migrate.

## Swapping models

Point any model key at your own subclass. The package resolves the configured class everywhere — actions, traits and relations included:

```php
// config/approvals.php
'model' => App\Models\Approval::class,
'request_model' => App\Models\ApprovalRequest::class,
'stage_model' => App\Models\ApprovalRequestStage::class,
'delegation_model' => App\Models\ApprovalDelegation::class,
```

```php
namespace App\Models;

use RoundlyConsulting\Approvals\Models\Approval as BaseApproval;

class Approval extends BaseApproval
{
    // your relations, scopes and casts
}
```

A class that doesn’t extend the expected base model throws InvalidApprovalModelException.

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