bkwld/decoy
Decoy is a Laravel package for building custom admin interfaces and CMSs. It provides model-driven admin controllers, form and listing builders, localization, and a clean Vue-powered UI, so you can manage content without a separate CMS.
Decoy is a Laravel CMS where the model is the source of truth. Before touching any UI you should understand three layers: model metadata ($admin), a thin admin controller extending Bkwld\Decoy\Controllers\Base, and the published config in config/decoy.php.
Minimal setup:
composer require bkwld/decoy
php artisan vendor:publish --provider="Bkwld\Decoy\DecoyServiceProvider"
php artisan migrate
Publishing gives you config/decoy.php, Decoy migrations, lang files, and the admin assets under public/. Register Bkwld\Decoy\DecoyServiceProvider and the Decoy facade if you are on a pre-auto-discovery Laravel. Most versions also ship an installer (e.g. php artisan decoy:install) that scaffolds your admin user, routes/admin.php, and an app/Http/Controllers/Admin directory — run it if available, then read the generated files before editing anything.
Where to look first, in order:
config/decoy.php — routes prefix (default /admin), locales, permissions, middleware, upload paths.User model — Decoy expects roles/permissions there; add the suggested trait/columns.routes/admin.php — this is where controllers are registered and the admin nav is composed.$admin / $columns shape your version expects.First real use case: a single Article model with title, slug, body, and a hero image.
// app/Article.php
class Article extends Bkwld\Decoy\Models\Base
{
protected $admin = [
'title' => 'title',
'description' => 'summary',
'columns' => ['title', 'slug', 'updated_at'],
'search' => ['title', 'body'],
'rules' => ['title' => 'required', 'body' => 'required'],
'images' => ['hero' => ['width' => 1200, 'height' => 630, 'method' => 'crop']],
];
}
// app/Http/Controllers/Admin/ArticlesController.php
class ArticlesController extends Bkwld\Decoy\Controllers\Base
{
protected $title = 'Articles';
protected $description = 'News and posts';
}
Register the controller in routes/admin.php (typically Decoy::routes([...]) or a controller list), then visit /admin and log in. If the listing appears but no form does, Decoy is auto-generating fields from the DB schema — that is expected for a first pass.
Keep controllers thin, models fat. Day-to-day Decoy work is mostly editing $admin on models. Listing columns, search fields, default sort, validation, upload handling, and relationship metadata belong on the model. Controllers should only carry $title, $description, custom $filters, and one-off form customization.
Form customization with Former. Decoy builds forms from DB introspection, but the idiomatic override is a form() method on the controller returning a Former form. Use Decoy's field classes rather than raw Former where possible: Image, File, Checkbox, Select, Redactor, Wysiwyg, Video. They wire up Croppa paths, previews, and asset cleanup automatically.
protected function form($model)
{
return parent::form($model)
->withUpload('hero') // uses the images config on the model
->redactor('body')
->text('title')->required();
}
Relationships. Has-many and belongs-to-many are declared in $admin['many'] (or equivalent on your version) so Decoy renders a multiselect/attached-records editor. Always pair this with a Cloner-aware duplicate() so cloning a parent does not orphan children. Decoy uses bkwld/cloner; add cloneable to the model's admin metadata and override duplicate() only when relationships need special handling.
Ordering and drag-drop. Set 'sortable' => true and include the trait (Bkwld\Decoy\Models\Traits\Orderable or the $admin['ordering'] key, depending on version). This requires an integer position column and the position value to be persisted on reorder — verify after your first drag.
Validation scopes. Put shared rules on the model ($rules or $admin['rules']) and context-specific rules (create vs update) on the controller or a $admin['validation'] array. This keeps API and admin validation aligned.
Permissions. Decoy's auth is role/permission based via config/decoy.php. Add the role and permission columns to your User model, seed a developer/super-admin role, and then gate controllers. Nav items you don't want shown should be conditionally registered in routes/admin.php rather than hidden with CSS.
Localization. If decoy.locales has more than one entry, Decoy renders a locale switcher and expects translated columns (typically a translations table or JSON columns, depending on the trait you enable). Turning locales on after shipping data means writing a backfill migration — do it before launch.
Navigation and extension points. Customize the sidebar in routes/admin.php when registering controllers; add external links or grouped sections there. For deeper extension, use Former macros, Decoy middleware, and the Decoy facade for helpers inside views (Decoy::isLocalized(), URL helpers). Keep custom Blade overrides in resources/views/vendor/decoy so vendor:publish upgrades don't clobber them.
The package is from the Laravel 5.x era (last release 2018). Before anything else, confirm your Laravel and PHP versions. On Laravel 9+/PHP 8 you will hit Illuminate\Foundation and container signature changes; expect to fork the package or pin the app to Laravel 5.5–5.8. This is the single biggest decision in any Decoy assessment — budget for it explicitly.
Re-publish after upgrades. vendor:publish --provider="Bkwld\Decoy\DecoyServiceProvider" --force overwrites admin assets and config. Diff config/decoy.php before committing; teams routinely lose customizations this way.
Croppa has its own cache directory. Uploads write to public/uploads (or your configured path) but Croppa generates thumbnails on demand in a cache folder outside git. On deploy, either warm the cache or ensure permissions; run php artisan croppa:prune periodically. Store the path in the database, not an absolute URL, or environment changes will break every image.
$admin cache and route cache. Decoy's metadata is inexpensive but the model file is loaded on every admin request. If you use php artisan route:cache and see stale nav or 404s after adding a controller, clear both config and route caches — the admin routes are dynamically registered, so a cached route table can miss new controllers.
Naming conventions drive everything. ArticlesController maps to Article and route admin/articles. When you deviate, set $model explicitly. Silent failures to resolve a model appear as blank listings, not exceptions.
Slugs and unique columns. Decoy will happily let you save a duplicate slug if your rules don't cover it. Add a Rule::unique scoped to the model and handle soft-deleted records.
Sortable requires a real column. Adding 'sortable' => true without a position integer column throws on the listing page. Add the migration first.
Cloning and translations are explicit. Decoy won't copy relations or translations for you. Test duplication on a record with attached media and children before declaring it done.
Debug the generated form. If a field is missing, dump the Former instance in form() and inspect. Most "Decoy is broken" reports are a column that isn't in $fillable, a cast mismatch, or a field name that doesn't match the DB column.
Permission defaults are permissive. Seed and enforce a super-admin role in staging; otherwise a fresh install lets every authenticated user into the admin.
Don't fight the conventions. When you need something Decoy doesn't model well (workflow states, audit logs), add it as a normal Laravel layer behind the controller rather than trying to force it into $admin metadata.
How can I help you explore Laravel packages today?