Host Reference Patterns

Optional reading: This page describes reusable integration patterns for Laravel hosts that embed DBFlow into an existing ERP or admin product. It is not required to get started. Complete Host Integration and Filament UI first.

Alpha notice: Patterns below reflect lessons from real host pilots (including enterprise ERP integrations). Class names are illustrative — adapt naming to your application.

The hosted demo platform is the canonical tutorial environment. This page complements it when you need:

  • Product-level feature flags and pilot rollouts
  • Coexistence with a legacy approval engine
  • Code-first definitions and the Standard Filament definition editor
  • Host RBAC mapped to PermissionChecker
  • Business guards on confirm / post / fulfill actions

When to use these patterns

Situation Pattern to adopt
DBFlow is optional or rolled out per module Feature gate
Definitions live in PHP providers Service provider boot chain
Operators see an empty definition editor after sync Code workflow draft seeder
Ops needs a repeatable deploy step Artisan sync command
Filament must respect host permissions Permission checker
Definition editor shows permission-style assignees Assignee options resolver
confirm() must wait for workflow completion Business guard bridge
Submit starts workflow from an existing resource Resource action bridge

Recommended module layout

Keep DBFlow wiring out of generic controllers and fat Filament resources:

app/
├── Providers/
│   └── AppDbflowServiceProvider.php      # register providers, resolvers, hooks; sync; seed drafts
├── Console/Commands/
│   └── SyncAppDbflowWorkflowsCommand.php # deploy / ops entrypoint
├── Services/Dbflow/
│   ├── CodeWorkflowDraftService.php      # optional: seed drafts after code sync
│   └── PurchaseOrderWorkflowService.php  # start / cancel helpers per domain
└── Support/Dbflow/
    ├── AppDbflowPilot.php                # single feature-gate helper
    ├── AppDbflowWorkflowKeys.php         # workflow + resolver key constants
    ├── Definitions/
    │   └── PurchaseOrderApprovalWorkflowDefinitionProvider.php
    ├── AssigneeResolvers/
    │   └── PurchaseOrderApproverAssigneeResolver.php
    ├── PurchaseOrderApprovalWorkflowHooks.php
    └── Filament/
        ├── AppDbflowPermissionChecker.php
        ├── AppDbflowWorkflowableLabelResolver.php
        └── AppDbflowPermissionAssigneeOptionsResolver.php

app/Support/PluginIntegration/            # optional boundary for legacy engine coexistence
    ├── DbflowFilamentBridge.php          # Filament header actions on business resources
    └── PurchaseOrderApprovalGuardBridge.php

Filament panel registration stays in PanelProvider — gate DBFlowFilamentPanel::register($panel) with the same pilot helper.

Boot order

Host app boots
    │
    ├─► AppDbflowServiceProvider (if pilot / feature enabled)
    │       register WorkflowDefinitionProvider(s)
    │       register AssigneeResolver key(s)
    │       register WorkflowHooks
    │       SyncWorkflowDefinitions::handle()
    │       CodeWorkflowDraftService::seedMissingDrafts()   [optional]
    │
    ├─► AdminPanelProvider
    │       DBFlowFilamentPanel::register($panel)             [if pilot enabled]
    │
    └─► Business request
            Resource action → DBFlow::start()
            My Tasks       → DBFlow::approve() / reject()
            confirm()      → GuardBridge checks terminal workflow state

Core does not auto-run sync or draft seeding. The host owns when those actions run (boot, deploy hook, or Artisan).

Feature gate

Wrap all DBFlow surfaces — service provider boot, Filament panel registration, resource actions, and business guards — behind one helper:

final class AppDbflowPilot
{
    public static function isEnabled(): bool
    {
        return (bool) config('app.dbflow.enabled', false)
            && (bool) config('dbflow.enabled', true)
            && (bool) config('dbflow-filament.enabled', true);
    }

    public static function purchaseOrder(): bool
    {
        return self::isEnabled()
            && (bool) config('app.dbflow.purchase_order_pilot', false);
    }
}

DBFlowFilamentPanel reads only dbflow-filament.enabled. A host-level gate prevents half-enabled stacks where UI is visible but definitions were never registered.

Use per-module flags (purchase_order_pilot) so one document type can pilot DBFlow while others keep a legacy engine.

Service provider boot chain

Centralize Core registration in a dedicated provider; early-return when the pilot is off:

public function boot(): void
{
    if (! AppDbflowPilot::isEnabled()) {
        return;
    }

    DBFlow::registerDefinitionProvider($registry, new PurchaseOrderApprovalWorkflowDefinitionProvider());
    DBFlow::registerAssigneeResolver($registry, 'purchase_order_approver', $resolver);
    DBFlow::registerWorkflowHooks($registry, 'purchase_order_approval', PurchaseOrderApprovalWorkflowHooks::class);

    if (config('app.dbflow.auto_sync_definitions', true)) {
        app(SyncWorkflowDefinitions::class)->handle();
    }

    if (config('app.dbflow.seed_code_workflow_drafts', true)) {
        app(CodeWorkflowDraftService::class)->seedMissingDraftsForCodeWorkflows();
    }
}

Register the provider in bootstrap/providers.php.

Code workflow draft seeder

After SyncWorkflowDefinitions, code-owned workflows are published but often have no draft. The Standard WorkflowResource editor requires Workflow::hasDraft().

Extract seeding into a small service that iterates registered providers:

final class CodeWorkflowDraftService
{
    public function seedMissingDraftsForCodeWorkflows(): array
    {
        $seeded = [];

        foreach ($this->definitionRegistry->providers() as $provider) {
            $workflow = Workflow::query()->where('key', $provider->key())->first();

            if (! $workflow || $this->isUiOwned($workflow)) {
                continue;
            }

            if ($workflow->hasDraft() || ! $workflow->hasPublishedVersion()) {
                continue;
            }

            $definition = $workflow->currentDefinition();
            // merge key, name, description from workflow row …
            $this->saveWorkflowDraft->handle($workflow, $definition);
            $seeded[] = $workflow->key;
        }

        return $seeded;
    }
}

Rules:

  • Skip source = ui workflows — do not overwrite UI-authored drafts.
  • Skip workflows that already have a draft.
  • Run after every sync in boot, deploy, or your Artisan command.

See also Filament UI → Code-synced workflows.

Artisan sync command

Wrap SyncWorkflowDefinitions for operators and CI:

final class SyncAppDbflowWorkflowsCommand extends Command
{
    protected $signature = 'app:dbflow-sync-workflows';

    public function handle(SyncWorkflowDefinitions $sync, CodeWorkflowDraftService $drafts): int
    {
        if (! AppDbflowPilot::isEnabled()) {
            $this->warn('DBFlow pilot disabled — skipped.');

            return self::SUCCESS;
        }

        $summary = $sync->handle();
        // print created / updated / unchanged keys …

        foreach ($drafts->seedMissingDraftsForCodeWorkflows() as $key) {
            $this->line("Draft seeded: {$key}");
        }

        return self::SUCCESS;
    }
}

Core ships no bundled sync command — this is host responsibility.

Permission checker

Replace AllowAllPermissionChecker before any shared environment. Map dbflow.* abilities to your existing RBAC:

final class AppDbflowPermissionChecker implements PermissionChecker
{
    public function can(mixed $user, string $ability, mixed $record = null): bool
    {
        return match ($ability) {
            'dbflow.tasks.view',
            'dbflow.tasks.approve',
            'dbflow.tasks.reject' => $this->permissions->allows($user, 'documents.approve'),
            'dbflow.workflow_instances.view',
            'dbflow.workflow_instances.view_any' => $this->permissions->allows($user, 'documents.view'),
            'dbflow.definitions.view',
            'dbflow.definitions.update',
            'dbflow.definitions.publish' => $this->permissions->allows($user, 'system.settings'),
            default => false,
        };
    }
}

Bind via permission_checker_class in config/dbflow-filament.php. Ability names are listed in Permissions.

Assignee options resolver

For assignees.type: permission in definitions, Core resolves user IDs through DBFlow::registerAssigneeResolver($key, …). Filament needs a separate UI contract so authors see friendly labels:

Layer Key example Class
Core runtime purchase_order_approver PurchaseOrderApproverAssigneeResolver
Filament editor same string AppDbflowPermissionAssigneeOptionsResolver

The JSON value must match the registry key — it is not your Spatie permission name.

Business guard bridge

Core does not block confirm(), payment capture, or inventory posting. Add a thin bridge your domain service calls before mutating state:

final class PurchaseOrderApprovalGuardBridge
{
    public static function canConfirm(PurchaseOrder $order): bool
    {
        if (! AppDbflowPilot::purchaseOrder()) {
            return true; // legacy path or DBFlow off
        }

        if ($order->status !== 'draft') {
            return true;
        }

        $instance = $order->latestWorkflowInstance('purchase_order_approval');

        return $instance?->status === WorkflowInstanceStatus::Approved;
    }

    public static function assertCanConfirm(PurchaseOrder $order): void
    {
        if (! self::canConfirm($order)) {
            throw ValidationException::withMessages([
                'status' => 'Complete workflow approval before confirming.',
            ]);
        }
    }
}

Call assertCanConfirm() from PurchaseOrderService::confirm() (or equivalent). Gate Filament actions with the same rules.

Resource action bridge

Keep Filament resource classes thin. A bridge class registers submit actions and hides legacy approval UI when the pilot is active:

// In EditPurchaseOrder::getHeaderActions()
if (AppDbflowPilot::purchaseOrder()) {
    return DbflowFilamentBridge::headerActions($this);
}

return LegacyApprovalBridge::headerActions($this);

Submit actions call DBFlow::start($workflowKey, $record, $user) and guard visibility with hasRunningWorkflow($key). See Filament Resource Actions.

Implement WorkflowRouteResolvable::getWorkflowShowUrl() on the model so My Tasks and instance pages link back to your resource.

Case study: purchase order pilot

An ERP host integrated DBFlow for purchase order approval during alpha:

Piece Responsibility
AppDbflowPilot DBFLOW_ENABLED + module pilot flag + Filament toggle
PurchaseOrderApprovalWorkflowDefinitionProvider Single approval node, permission assignee → resolver key
PurchaseOrderApproverAssigneeResolver Returns user IDs with document edit permission
PurchaseOrderApprovalWorkflowHooks Maps approved / rejected / cancelled to order status columns
CodeWorkflowDraftService Fills Filament editor after code sync
AppDbflowPermissionChecker Maps dbflow.* to ERP permission constants
PurchaseOrderApprovalGuardBridge Blocks confirm() until workflow reaches approved
DbflowFilamentBridge Submit-for-approval on edit page; mutually exclusive with legacy light approval

This is one module in a larger product — copy the shape, not every class name.

What not to copy blindly

  • Dual engines without mutual exclusion — hide legacy submit/approve UI when DBFlow pilot is on.
  • Publishing from Filament on code-owned workflows — document who owns the definition (code vs UI) before operators publish.
  • Skipping permission_checker_class — demo-friendly defaults are unsafe in production.
  • Guarding only in Filament — enforce confirm/post rules in domain services too.
  • Product licensing, plugin marketplaces, or install wizards — out of scope for DBFlow docs.

Checklist (host product)

  1. Feature gate covers provider boot, panel registration, and bridges.
  2. Workflow keys and resolver keys live in one constants class.
  3. Sync runs on deploy; optional draft seeder runs after sync.
  4. permission_checker_class and permission_assignee_options_resolver_class are bound.
  5. Resource submit → DBFlow::start(); approve/reject → My Tasks (or equivalent with actor).
  6. Downstream mutations use a guard bridge + hooks for status columns.
  7. Feature tests cover start, approve, guard failure, and pilot off fallback.

What's next

Something wrong? Open an issue on GitHub