›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-approval
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Workflows and stages
  • Choosing the workflow
  • Approvers and resolvers
  • Requests and state
  • Conditional stages

Approval flow

  • Submitting for approval
  • Recording decisions
  • Attribute drafts
  • Returns and resubmission
  • Bulk decisions

Assignments and deadlines

  • Delegation and reassignment
  • SLAs and escalation

Reading and authorization

  • Eligibility and authorization
  • Queries and timelines
  • Events and listeners
  • Notifications
  • Notification content

Operations

  • Workflow definitions
  • Audit trail and evidence
  • Console commands
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Workflows and stages
  • Choosing the workflow
  • Approvers and resolvers
  • Requests and state
  • Conditional stages

Approval flow

  • Submitting for approval
  • Recording decisions
  • Attribute drafts
  • Returns and resubmission
  • Bulk decisions

Assignments and deadlines

  • Delegation and reassignment
  • SLAs and escalation

Reading and authorization

  • Eligibility and authorization
  • Queries and timelines
  • Events and listeners
  • Notifications
  • Notification content

Operations

  • Workflow definitions
  • Audit trail and evidence
  • Console commands
  • Testing
  • Troubleshooting

byrcsc/laravel-approval · 1.x

Audit trail and evidence.

Verify approval actions and attachment evidence through an append-only hash chain.

Every decision and administrative change leaves an action behind. The history is application-enforced append-only and tamper-evident within a stated trust boundary. It is not immutable, tamper-proof, or externally anchored.

Action types

use ByRcsc\LaravelApproval\Enums\ActionType;
ActionWritten when
submittedA request is created
approvedAn approval is recorded
rejectedA rejection is recorded
returnedA request is returned for revision
withdrawnA requester withdraws
cancelledAn administrator cancels
system_closedThe engine closes a request
supersededA conflicted request is replaced by a revision
resubmittedA replacement request is created
delegatedAn approver hands an assignment on
delegation_endedA standing delegation hands an assignment back
reassignedAn administrator rewrites an assignment
resyncedA live request's assignments are rebuilt
escalatedAn overdue stage's policy runs
remindedAn overdue reminder is emitted
stage_skippedA condition turned a stage off at submission
appliedA draft is written onto the record
conflictedA draft could not be applied
guard_bypassedGuard mode was bypassed through the escape hatch
attachment_addedEvidence is attached
commentedA comment is recorded on its own

Some actions have no human actor. The system writes escalated, delegation_ended, applied, and conflicted. The distinction matters. "The deadline passed and the stage was approved" is a different fact from "an approver approved it".

What an action stores

foreach ($request->actions as $action) {
    $action->action;         // ActionType
    $action->actor_label;    // readable name, captured at write time
    $action->comment;
    $action->metadata;
    $action->hash;
    $action->previous_hash;
    $action->created_at;
}

The package captures actor_label when it writes the action and never refreshes it. The trail still names the actor after the account is renamed or deleted. The morph key alone identifies nobody once the row it points at is deleted, and "who approved this?" must still have an answer then.

There is no updated_at. The ApprovalAction model refuses updates and deletes.

The hash chain

Each action's SHA-256 hash covers its own content and the hash of the previous action on the same request. The request row stores the trusted chain head. The package locks the head and advances it atomically on every append, so concurrent writers cannot fork the chain. Because the request stores the head, removing the final action is as visible as removing one from the middle.

The chain makes an edit, deletion, reordering, or insertion detectable.

The hash is unkeyed and stored beside the data it covers, so anyone who can write to the database directly can recompute a consistent chain. The guarantee holds against accidental and application-level modification, not against a database administrator.

The ApprovalAction and ApprovalAttachment models enforce append-only writes. A direct query-builder or SQL write bypasses the models. For that reason, the schema also uses restrictOnDelete from actions and attachments to requests, so erasing history requires a deliberate delete against those tables.

Deleting a stage sets the action's stage_id to null instead of deleting the action, so the actions taken on that stage survive. The null reference also breaks that action's hash, so the verifier reports the loss.

Attachment evidence

use ByRcsc\LaravelApproval\Data\AttachmentEvidence;

Approval::attach(new AttachmentEvidence(
    request: $request,
    disk: 's3',
    path: 'quotes/PO-1001.pdf',
    checksum: hash_file('sha256', $localPath),
    name: 'Supplier quote.pdf',
    mimeType: 'application/pdf',
    size: $bytes,
    metadata: ['source' => 'supplier-portal'],
    uploader: $requester,
    action: $approvalAction,
));

The package records a reference into a host-owned filesystem disk, never bytes. Storing and serving the file stays with your application.

The package writes the checksum into the audit action that records the attachment. The hash chain then covers both the file's content and its name.

action is optional and links the file to a specific decision, such as a signature image on an approval. Omit it for a document that belongs to the whole request. The constructor rejects an action from a different request.

The constructor validates its own arguments:

ProblemException
Blank disk or pathAttachmentException::blankLocation()
Checksum is not 64 hex charactersAttachmentException::invalidChecksum()
Negative sizeAttachmentException::invalidSize()
Action belongs to another requestAttachmentException::actionNotOnRequest()

Verifying

php artisan approval:verify
php artisan approval:verify 01JBK…

The command lists every finding and exits non-zero when it finds any. Approval::verifyTrail($only) runs the same check from code, for applications that add verification to their own monitoring:

$findings = Approval::verifyTrail();

// [['request' => '01JBK…', 'subject' => 'action #44', 'problem' => '…'], …]

The verifier checks for these findings, in this order:

  1. An action whose previous-hash does not point at the action before it, because history was removed, reordered, or inserted.
  2. An action whose content no longer matches its own hash, because someone edited it in place.
  3. A final action that does not match the request's recorded chain head.
  4. An attachment inconsistent with the history: a row no action recorded, a recorded file whose row is gone, or a row whose location or checksum differs from the metadata recorded at attach time.

The verifier reports findings and never repairs them. The operator decides what a broken chain means.

What a clean result covers

  • Action content, and the chain linkage between actions.
  • The request's recorded chain head.
  • Attachment rows.
  • The attachment metadata recorded at attach time, including the checksum the application supplied.

What it does not cover

  • Request state, draft attributes, workflow stage snapshots, and assignment rows. The hash chain covers none of these.
  • The stored files themselves. The verifier never reads their bytes or recomputes their checksums. It treats a checksum row that matches its recorded action as valid. The current verifier does not read storage disks.

The package provides no cryptographic signatures, HMAC protection, external hash anchoring, or write-once storage. If you need those protections, add them on top of the trail.

Running it on a schedule

Schedule::command('approval:verify')->dailyAt('03:00');

The command reads requests in chunks, so you can verify a large trail in one pass. Send its failures to the tool that monitors your scheduler's exit codes.

What to read next

  • Events and listeners to react to actions after transactions commit.
  • Console commands to schedule audit verification.
  • Troubleshooting to interpret verification failures.
PreviousWorkflow definitionsNextConsole commands
View source

On this page

  1. Action types
  2. What an action stores
  3. The hash chain
  4. Attachment evidence
  5. Verifying
  6. What a clean result covers
  7. What it does not cover
  8. Running it on a schedule
  9. What to read next