ovac / guardrails
Operational approvals engine for Laravel applications.
Fund package maintenance!
ovac4u
Requires
- php: ^8.1
- illuminate/support: ^10.0 || ^11.0
Requires (Dev)
- laravel/pint: ^1.14
- nunomaduro/collision: ^7.0 || ^8.0
- nunomaduro/larastan: ^2.9
- orchestra/testbench: ^8.0 || ^9.0
- pestphp/pest: ^2.0
- pestphp/pest-plugin-laravel: ^2.0
- phpstan/phpstan: ^1.10
This package is auto-updated.
Last update: 2025-09-20 00:04:30 UTC
README
Follow me anywhere @ovac4u | GitHub _________ _________ | Twitter | ___ |.-----.--.--.---.-.----.| | |.--.--. | Facebook | | _ || _ | | | _ | __||__ | | | | Instagram | |______||_____|\___/|___._|____| |__||_____| | Github + @ovac |_________| ovac.github.io | Facebook + @ovacposts
What is Guardrails?
Guardrails is an operational approvals engine for Laravel. Capture every high‑risk change, route it through the right reviewers, and apply the update only after the required signatures land. You decide which attributes are guarded, who can approve, and how many voices it takes to ship a change.
Why teams reach for Guardrails
- Protect critical workflows – turn dangerous writes into reviewable approval requests without rewriting business logic.
- Model or controller first – opt in with an Eloquent trait or intercept at the edge of your HTTP layer.
- Fluent, composable flows – stack steps, mix roles and permissions, count initiators, and codify escalation in code.
- Complete toolkit – ships with migrations, API routes, reviewer UI, and exhaustive docs so you can go live quickly.
- Laravel-native – PHP 8.1+, Laravel 10/11, works with Spatie permissions or Sanctum token abilities out of the box.
How it fits together
- Mark attributes as guarded or intercept controller payloads.
- Use
Flow::make()
/FlowBuilder
to describe who can sign each step. - Guardrails persists the request (
requests → steps → signatures
) and emits events as people approve or reject. - Once the final step meets its threshold, Guardrails applies the captured changes to your model for you.
Quickstart
Online Docs & Tools
- Full documentation: https://ovac.github.io/guardrails/
- Interactive Playground (flow builder): https://ovac.github.io/guardrails/playground
- AI Assistant (BYO API key, grounded in docs): https://ovac.github.io/guardrails/assistant
Install via Composer:
composer require ovac/guardrails
- Register the service provider (if not auto‑discovered):
// config/app.php OVAC\Guardrails\GuardrailsServiceProvider::class,
- Publish assets:
php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-config php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-migrations php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-views php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-assets php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-docs
- Run migrations:
php artisan migrate
60‑Second Example
Guard a model and require a quick two‑man rule (initiator + one peer):
use OVAC\Guardrails\Concerns\Guardrail; use OVAC\Guardrails\Services\Flow; class Post extends Model { use Guardrail; public function guardrailAttributes(): array { return ['published']; } public function guardrailApprovalDescription(array $dirty, string $event): string { return 'Publish flag changes require editorial approval.'; } public function guardrailApprovalFlow(array $dirty, string $event): array { return [ Flow::make() ->anyOfPermissions(['content.publish']) ->includeInitiator(true, true) ->signedBy(2, 'Editorial Review') ->build(), ]; } }
When the initiator supplies a justification (for example through a form field), pass it into the guardrails context before saving:
$post->guardrails() ->description($request->input('approval_description')) ->meta(['reason_code' => $request->input('reason_code')]); $post->fill($request->validated())->save();
Prefer controllers? Intercept without touching models:
$result = $this->guardrailIntercept($post, ['published' => true], [ 'description' => 'Editorial approval required before publishing.', 'only' => ['published'], 'extender' => Flow::make()->anyOfRoles(['editor','managing_editor'])->signedBy(1, 'Editorial Approval'), ]);
Why Teams Use It
- Approval confidence for critical data while keeping code changes small.
- Human‑readable flow rules with real‑world patterns (two‑man rule, peer review, escalation).
- Works with your auth today — Spatie permissions if present, token abilities otherwise.
Use Cases (with examples)
- Publish a blog post — one more editor must approve
Flow::make() ->anyOfPermissions(['content.publish']) // any editor with publish permission ->includeInitiator(true, true) // author counts as one approval ->signedBy(2, 'Editorial Review') // needs one more editor ->build();
- Delete a user account — two steps, different roles
Flow::make() ->anyOfRoles(['support_lead']) // support lead approves first ->signedBy(1, 'Support Approval') ->anyOfRoles(['security_officer']) // then security approves ->signedBy(1, 'Security Approval') ->build();
- Refund an order — one of finance OR operations
Flow::make() ->anyOfRoles(['finance_manager','ops_manager']) ->signedBy(1, 'Management Approval') ->build();
- Sensitive setting change — peer with same permission must co‑sign
Flow::make() ->permissions(['settings.update']) // list all required permissions ->requireAnyPermissions() // switch to any‑of ->samePermissionAsInitiator(true) // peer must share at least one ->includeInitiator(true, true) // initiator pre‑approved ->signedBy(2, 'Peer Review') ->build();
- Multi‑step escalation — ops first, then execs
Flow::make() ->anyOfPermissions(['ops.change']) ->includeInitiator(true, true) ->signedBy(2, 'Ops Review') ->anyOfRoles(['cto','cfo']) ->signedBy(1, 'Executive Sign‑off') ->build();
API (3 endpoints)
- GET
/{route_prefix}
— list pending approval requests with steps/signatures. - POST
/{route_prefix}/{request}/steps/{step}/approve
— approve a step. - POST
/{route_prefix}/{request}/steps/{step}/reject
— record a rejection signature.
UI
A minimal page at /{page_prefix}
consumes the API for reviewers. Publish and customize the Blade view.
Docs
- Start here: Documentation Index
- Or publish locally:
php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-docs
(todocs/guardrails
).
Highlights worth reading next:
- Organization Playbook
- Use Cases
- Advanced Flows (dynamic/risk‑based)
- Voting Models
- Auditing & Changelog
- Custom Controllers
- External Signing (DocuSign/DocuSeal)
- Email & SMS Verification
- Ideas & Examples (10)
- Extending Models & Migrations
- Full Testing Guide
How It Works (Data Flow)
flowchart TD Start[Change attempted] --> Guard{Guardrails guard?} Guard -- No --> ApplyNow[Apply change immediately] Guard -- Yes --> Capture[Capture request and steps] Capture --> Review{Reviewer decision} Review -- Approve --> Apply[Apply new data] Review -- Reject --> Halt[Reject request] Apply --> Done[Emit completion events] Halt --> DoneLoading
Keep approvals close to where changes happen (models) or intercept in controllers. Steps define who can sign and how many signatures you require.
Extending Migrations and Models
- Add columns to the published migrations (e.g., reason, category, workspace_id) with a new migration; the package models use
$guarded = []
, so new attributes are writable. - If you need JSON casting or extra relations, create an app model that extends the package model:
// app/Models/ApprovalRequest.php namespace App\Models; class ApprovalRequest extends \OVAC\Guardrails\Models\ApprovalRequest { protected $casts = [ 'meta' => 'array', 'reason' => 'string', ]; public function workspace() { return $this->belongsTo(Workspace::class); } }
Use events like ApprovalRequestCaptured
to populate new columns or meta. See Config Recipes and Auditing & Changelog.
Search keywords: "laravel approval workflow", "laravel multi signature approvals", "human in the loop approvals", "laravel model guarded changes", "laravel approval steps thresholds", "spatie permissions approval flow", "controller intercept approvals", "two-man rule laravel".
Documentation Website
Guardrails ships build scripts instead of doc sources so the Composer install stays slim. The GitHub Pages deployment uses scripts/docs-site/build-docs-site.js
to assemble a Docusaurus bundle from resources/docs
on demand.
- Generate a local bundle with
node scripts/docs-site/build-docs-site.js ./build/docs-site
(requires Node 20+). When previewing locally, openhttp://localhost:3000/guardrails/
after runningnpx serve ./build/docs-site
. For a root-based preview, runDOCS_BASE_URL=/ node scripts/docs-site/build-docs-site.js ./build/docs-site-local && npx serve ./build/docs-site-local
. - Configure optional environment variables (
DOCS_SITE_URL
,DOCS_BASE_URL
,DOCS_REPOSITORY_URL
,DOCS_PACKAGIST_URL
) to tune canonical URLs and metadata. Defaults infer the correct GitHub Pages paths from the current repository. - The published site exposes
/playground
and/assistant
routes for the interactive flow builder and BYO-key AI chat, both running entirely in the browser. - Tags that start with
v
automatically generate frozen documentation snapshots so visitors can browse historical releases. .github/workflows/docs-site.yml
runs on every push and tag, rebuilding the static site and deploying it to GitHub Pages (gh-pages
branch).
Tests & Coverage
Run the full suite with Pest (quiet mode by default):
composer test
Generate coverage locally when you need instrumentation or Clover output for CI:
composer test:coverage
composer test:ci # emits coverage.xml
See the Full Testing Guide for environment setup, Laravel Testbench tips, and additional testing recipes.
Support
If this package saves you time, please consider:
- Starring the repo: https://github.com/ovac/guardrails
- Sponsoring on GitHub: https://github.com/sponsors/ovac4u