inlayphp / inlay
PHP-first administration panels and UI components for Laravel and Inertia.
Requires
- php: ^8.3
- illuminate/console: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/filesystem: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- illuminate/validation: ^12.0 || ^13.0
- inlayphp/actions: ^0.3 || dev-main
- inlayphp/authorization: ^0.3 || dev-main
- inlayphp/core: ^0.3 || dev-main
- inlayphp/design: ^0.3 || dev-main
- inlayphp/forms: ^0.3 || dev-main
- inlayphp/infolists: ^0.3 || dev-main
- inlayphp/media: ^0.3 || dev-main
- inlayphp/media-manager: ^0.3 || dev-main
- inlayphp/notifications: ^0.3 || dev-main
- inlayphp/panels: ^0.3 || dev-main
- inlayphp/resources: ^0.3 || dev-main
- inlayphp/schemas: ^0.3 || dev-main
- inlayphp/support: ^0.3 || dev-main
- inlayphp/tables: ^0.3 || dev-main
- inlayphp/theme: ^0.3 || dev-main
- inlayphp/validation: ^0.3 || dev-main
- inlayphp/widgets: ^0.3 || dev-main
- symfony/process: ^7.2 || ^8.0
Requires (Dev)
- inlayphp/authorization-spatie: ^0.3 || dev-main
- inlayphp/imports: ^0.3 || dev-main
- inlayphp/media-spatie: ^0.3 || dev-main
- inlayphp/permission-manager: ^0.3 || dev-main
- inlayphp/tables-xlsx: ^0.3 || dev-main
- inlayphp/two-factor-authentication: ^0.3 || dev-main
- pestphp/pest: ^4.0
- phiki/phiki: ^2.0
Suggests
- inlayphp/authorization-spatie: Synchronize Inlay abilities with Spatie Laravel Permission.
- inlayphp/imports: Add validated, fault-isolated import workflows.
- inlayphp/media-spatie: Bridge the Inlay media catalog with Spatie Media Library.
- inlayphp/permission-manager: Add role, permission, assignment, and access-audit screens.
- inlayphp/tables-xlsx: Add an optional PhpSpreadsheet XLSX export driver for Inlay tables.
- inlayphp/two-factor-authentication: Add optional TOTP, recovery-code, enrollment, and panel challenge flows.
README
PHP-first, schema-driven UI for Laravel and Inertia — one server contract, rendered by React or Vue.
Inlay is a PHP-first, schema-driven UI framework for Laravel and Inertia. It brings a fluent developer experience for forms, tables, actions, infolists, resources, and admin panels to applications that use React or Vue.
Laravel defines the interface and owns authorization, validation, querying, and persistence. Inlay serializes that intent into stable, versioned contracts. Small React and Vue packages render those contracts and handle accessible browser interaction.
Laravel Resource / Form / Table
│
▼
versioned Inertia props
│
┌──────┴──────┐
▼ ▼
React renderer Vue renderer
The main goals are:
- Make ordinary CRUD interfaces fast to write from Laravel PHP.
- Keep business rules, policies, validation, and queries on the server.
- Give React and Vue the same public component contract.
- Allow applications and community packages to add renderers, fields, columns, actions, themes, and plugins without forking Inlay.
- Keep each feature independently installable.
Status
Inlay is under active pre-release development. The monorepo currently includes:
- Fluent PHP builders and versioned JSON contracts.
- Complete initial Form, Table, Schema, Action, Infolist, Import, and Panel component catalogs.
- PHP-first Resources with automatic Laravel routes and Inertia page delivery.
- Authenticated admin panels with automatic Resource routes and navigation.
- Native Laravel Gate/Policy authorization, optional Spatie role synchronization, and a Dcat-inspired permission manager.
- A secure media catalog core with private-by-default uploads, logical folders, trash/restore, and transformation hooks.
- Authorized and centrally validated create, update, and delete lifecycles.
- React 19 and Vue 3 renderers with matching behavior tests.
- Custom renderer registries, plugin manifests, asset registries, and render hooks.
- Semantic theme tokens, stable
data-slothooks, and typed class overrides. - A shared neutral base theme, polished default admin theme, accessible high-contrast mode, dark mode, and PHP-defined custom themes.
- A public
inlayphp/design/@inlayphp/designfaçade withmake:inlay-themeCSS generation for application-owned themes. - PHP-first stats, chart, and table dashboard widgets with matching React and Vue renderers.
- Opt-in, request-keyed dashboard widget caching through the Laravel cache repository, without weakening authorization or tenant scoping.
- Session- or database-backed, transport-safe notifications with optional action integration and matching React/Vue renderers.
- A Laravel 12, Inertia v3, React 19 playground using the local packages.
The API may still change before the first stable release. See Roadmap for the largest remaining compatibility areas.
Official React and Vue renderers share accessible controls through @inlayphp/ui-react and @inlayphp/ui-vue. This keeps inputs, focus treatment, custom selects, dialogs, menus, and plugin screens visually consistent without moving PHP component definitions into JavaScript.
Requirements
- PHP 8.3 or newer
- Laravel 12
- Inertia Laravel 3 for Resource page delivery
- React 19 or Vue 3 for the supplied frontend renderers
- Node.js and pnpm for monorepo development
Individual packages may require fewer dependencies. For example, the schema and support packages do not require an Inertia application.
Package map
Clean core Composer packages
| Package | Responsibility |
|---|---|
inlayphp/core |
Plugin lifecycle, extension manifests, frontend assets, registries, and render hooks |
inlayphp/support |
Safe URLs, serializable conditions, and shared low-level contracts |
inlayphp/schemas |
Layout components such as sections, grids, tabs, wizards, groups, and callouts |
inlayphp/validation |
Central Laravel validation classes shared across forms, requests, imports, APIs, actions, and resources |
inlayphp/actions |
Reusable actions, bulk actions, confirmation dialogs, modal metadata, and endpoints |
inlayphp/forms |
Form schemas, editable fields, reactive conditions, data, actions, and validation metadata |
inlayphp/tables |
Columns, filters, searching, sorting, pagination, selection, row actions, and bulk actions |
inlayphp/infolists |
Read-only record presentation and nested entry schemas |
inlayphp/notifications |
Session- or database-backed, transport-safe notifications for Inertia and Laravel |
inlayphp/panels |
Complete panel runtime: registration, authentication, dashboards, routes, navigation, themes, Resources, and plugins |
inlayphp/theme |
Shared base/default theme presets and semantic light/dark token contracts |
inlayphp/design |
Public design-system façade, CSS variable generation, and application theme generator |
inlayphp/widgets |
PHP-first dashboard stats, charts, tables, providers, and panel integration |
inlayphp/resources |
Model-centric CRUD orchestration across tables, forms, infolists, pages, policies, and persistence |
inlayphp/authorization |
Owned ability registry and Laravel Gate/Policy decision bridge |
These packages are installed by inlayphp/inlay. They provide the panel and component framework without choosing a database permission system, media library, or import workflow.
Optional official Composer packages
| Package | Responsibility |
|---|---|
inlayphp/imports |
Column mapping, preview validation, fault-isolated processing, results, and failure downloads |
inlayphp/authorization-spatie |
Spatie permission synchronization, super-admin, cache, and team scoping adapter |
inlayphp/permission-manager |
Dcat-inspired roles, permissions, user assignment, and ability-sync audit panel plugin |
inlayphp/media |
Secure storage-neutral media catalog, uploads, folders, albums, visibility, trash, and transformations |
inlayphp/media-manager |
Authorized panel media browser, album filter, and picker plugin with a versioned Inertia contract |
inlayphp/media-spatie |
Optional zero-copy bridge between the Inlay catalog and Spatie Media Library |
inlayphp/tables-xlsx |
Optional PhpSpreadsheet XLSX export driver with typed cells, filters, selection, row limits, and formula-injection protection |
inlayphp/two-factor-authentication |
Optional encrypted TOTP enrollment, recovery codes, and panel login challenge step |
The CMS family is intentionally excluded from this initial public monorepo bootstrap. It will be released separately after its own package and contract review, without changing the core panel, form, table, resource, or plugin APIs.
Frontend packages
Framework-neutral runtime packages:
@inlayphp/core@inlayphp/actions@inlayphp/theme@inlayphp/design@inlayphp/ui@inlayphp/notifications
React packages:
@inlayphp/ui-react@inlayphp/actions-react@inlayphp/forms-react@inlayphp/tables-react@inlayphp/infolists-react@inlayphp/notifications-react@inlayphp/imports-react@inlayphp/panels-react@inlayphp/permission-manager-react@inlayphp/media-manager-react@inlayphp/widgets-react@inlayphp/two-factor-authentication-react
Vue packages:
@inlayphp/ui-vue@inlayphp/actions-vue@inlayphp/forms-vue@inlayphp/tables-vue@inlayphp/infolists-vue@inlayphp/notifications-vue@inlayphp/imports-vue@inlayphp/panels-vue@inlayphp/media-manager-vue@inlayphp/widgets-vue@inlayphp/two-factor-authentication-vue
The monorepo keeps PHP builders and their React/Vue adapters near each other so releases can keep both sides of a contract compatible.
Installation
Install the complete administration framework with two commands:
composer require inlayphp/inlay:"^0.3"
php artisan inlay:install --panels
The installer creates and registers AdminPanelProvider, enables the default
theme, authentication and account settings, generates a working User Resource,
bundles the Media Manager, publishes its migrations, redirects guests to the
panel login, scaffolds the official React pages, and configures Tailwind to scan
the installed @inlayphp/* npm packages. It detects npm, pnpm, Yarn, or Bun and
installs the required renderer packages. Run the printed migration, user, and
frontend build commands, then open /admin.
php artisan migrate php artisan inlay:make-user npm run build php artisan inlay:doctor --production
The installer writes this Tailwind CSS 4 source rule automatically. Keep it in the application stylesheet when deploying from a standalone repository:
@source '../../node_modules/@inlayphp/*/src/**/*.{ts,tsx,vue}';
inlay:doctor checks panel registration, the official renderer dependency,
Tailwind source discovery, generated User and Media files, and compiled CSS.
Run it without --production before building, or with --production after the
Vite build to prove the deployed stylesheet contains Inlay utilities.
inlay:make-user uses hidden password and confirmation prompts. Its --name,
--email, and --password options are available for controlled automation, but
putting passwords directly in shell history is not recommended.
Use --panel=reports for another panel id, --without-media or
--without-users for a smaller preset, --no-npm when CI manages frontend
installation separately, and --force to replace generated application files.
React is the turnkey renderer today. Vue remains available through its official
adapters and will receive the same installer preset next.
The installer is repeatable. Running it again preserves application-owned
providers, Resources, pages, and validation files while restoring missing
supporting files. Use --force only when you intentionally want to replace the
generated application code.
For a tenant-aware starter panel, provide the Eloquent tenant model while
installing. The generated provider imports the model and calls tenant() for
you; tenant membership is still decided by your model's TenantAccess and
HasTenants implementations:
php artisan inlay:install \
--panel=workspace \
--tenant-model='App\\Models\\Team' \
--tenant-parameter=team \
--tenant-route-key=slug
This generates routes such as /{team}/workspace and keeps the tenant
configuration in application-owned PHP, where it can be reviewed and extended.
Omit --tenant-route-key to use the model's normal Laravel route key.
The root package is the recommended all-in-one installation and includes the storage-neutral media catalog and Media Manager. Roles, database permissions, Spatie adapters, imports, two-factor authentication, and CMS features remain separate plugins so a default panel stays understandable.
Install only the features an application uses. A Form and Table application typically needs:
composer require inlayphp/forms inlayphp/tables inlayphp/actions inlayphp/validation
For React:
pnpm add @inlayphp/forms-react @inlayphp/tables-react @inlayphp/actions-react
For Vue:
pnpm add @inlayphp/forms-vue @inlayphp/tables-vue @inlayphp/actions-vue
Add model-centric CRUD orchestration with:
composer require inlayphp/resources pnpm add @inlayphp/resources @inlayphp/resources-react
Vue resource pages use @inlayphp/resources-vue. Both adapters render owner-scoped Relation Managers from the same PHP-defined Forms and Tables.
Compose Resources into a protected panel with:
composer require inlayphp/panels inlayphp/resources pnpm add @inlayphp/panels-react
Add Dcat-style access management and the media library as independent panel plugins:
composer require inlayphp/permission-manager inlayphp/media-manager pnpm add @inlayphp/permission-manager-react @inlayphp/media-manager-react
Their required backend foundations are installed transitively: Permission Manager installs the Spatie authorization adapter, and Media Manager installs the storage-neutral media catalog. Register them through the same panel plugin API used by community extensions:
use Inlay\MediaManager\MediaManagerPlugin; use Inlay\PermissionManager\PermissionManagerPlugin; return $panel->plugins([ PermissionManagerPlugin::make(), MediaManagerPlugin::make(), ]);
Use @inlayphp/media-manager-vue and @inlayphp/permission-manager-vue for a Vue panel. Both official plugin renderers expose the same page registries and contracts as their React counterparts. Install inlayphp/imports with its React or Vue renderer only when an application needs import workflows.
All public Composer packages are available from Packagist and the official React/Vue renderers are available from npm. The monorepo uses local workspaces only for development and release verification.
See Installation and deployment for clean Laravel, standalone Forms/Tables, custom renderer, CI, and Laravel Cloud workflows.
Quick start: a PHP-first Resource
Generate the resource, its list/create/edit pages, and a centralized validation class:
php artisan make:inlay-resource User
Standalone pages outside a panel have their own generators:
php artisan make:inlay-form-page Billing/CreateInvoice --model=Invoice php artisan make:inlay-table-page Reports/ListInvoices --model=Invoice
Each scaffolds the page class, derives its Inertia component name and query-string prefix from the class name, and prints the Route::inlayForm() or Route::inlayTable() line to register it. Existing files are never overwritten without --force.
The central resource can configure its table and form once:
<?php namespace App\Inlay\Resources; use App\Models\User; use App\Validation\UserRules; use Illuminate\Database\Eloquent\Model; use Inlay\Actions\Action; use Inlay\Forms\Fields\Select; use Inlay\Forms\Fields\TextInput; use Inlay\Forms\Fields\Toggle; use Inlay\Forms\Form; use Inlay\Resources\Resource; use Inlay\Resources\ResourceOperation; use Inlay\Tables\Columns\BadgeColumn; use Inlay\Tables\Columns\TextColumn; use Inlay\Tables\Filters\SelectFilter; use Inlay\Tables\Table; final class UserResource extends Resource { protected static string $model = User::class; protected static ?string $navigationIcon = 'users'; protected static bool $usesLaravelPolicy = true; public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('name')->searchable()->sortable(), TextColumn::make('email')->searchable(), BadgeColumn::make('status')->colors([ 'active' => 'success', 'suspended' => 'danger', ]), ]) ->filters([ SelectFilter::make('status')->options([ 'active' => 'Active', 'suspended' => 'Suspended', ]), ]) ->actions([ Action::make('delete') ->url('/users/{id}') ->method('delete') ->color('danger') ->requiresConfirmation(), ]); } public static function form(Form $form): Form { return $form ->precognitive() ->schema([ TextInput::make('name')->required()->maxLength(255), TextInput::make('email')->email()->required(), Select::make('role')->options([ 'admin' => 'Administrator', 'member' => 'Member', ]), Toggle::make('active')->default(true), ]); } public static function validation(): string { return UserRules::class; } public static function getPages(): array { return [ 'index' => ListUsers::route('/'), 'create' => CreateUser::route('/create'), 'edit' => EditUser::route('/{record}/edit'), ]; } }
Each page selects an Inertia component while remaining connected to the Resource:
use Inlay\Resources\Pages\ListRecords; final class ListUsers extends ListRecords { protected static string $resource = UserResource::class; protected static string $component = 'users/index'; }
Register the full CRUD route set without writing a controller:
use App\Inlay\Resources\UserResource; use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests; use Inlay\Resources\Facades\InlayResources; InlayResources::routes([UserResource::class], [ 'middleware' => ['auth'], 'mutationMiddleware' => [HandlePrecognitiveRequests::class], ]);
For this Resource, Inlay registers:
| Method | URI | Purpose |
|---|---|---|
GET |
/users |
List page and Table props |
GET |
/users/create |
Create page and Form props |
GET |
/users/{record}/edit |
Edit page, populated Form, and safe record props |
POST |
/users |
Authorized, validated, transactional creation |
PATCH |
/users/{record} |
Scoped, authorized, validated update |
DELETE |
/users/{record} |
Scoped, authorized, transactional deletion |
The application frontend only renders the supplied contracts.
Reusable resource shells can also consume the PHP page contract directly:
ResourcePage renders server-authored breadcrumbs, actions, header/footer
widget dashboards, Forms, Tables, Infolists, and RelationManagers in either
React or Vue. Widget themes and custom renderers remain replaceable through the
same WidgetDashboard options used by panel dashboards.
React:
import { Form } from '@inlayphp/forms-react' import { Table } from '@inlayphp/tables-react' export default function UsersIndex({ form, table, errors }) { return ( <> <Form resource={form} errors={errors} /> <Table resource={table} /> </> ) }
Vue:
<script setup lang="ts"> import { Form } from '@inlayphp/forms-vue' import { Table } from '@inlayphp/tables-vue' defineProps<{ form: FormResource table: TableResource errors: Record<string, string> }>() </script> <template> <Form :resource="form" :errors="errors" /> <Table :resource="table" /> </template>
See the full Resources documentation for routing options, lifecycle hooks, scoped queries, security guarantees, and advanced persistence.
Forms
Forms support nested layouts, initial data, defaults, validation metadata, conditional visibility, conditional requirements, live change/blur metadata, and Inertia submission.
use Inlay\Forms\Fields\Select; use Inlay\Forms\Fields\TextInput; use Inlay\Forms\Fields\Toggle; use Inlay\Forms\Form; use Inlay\Schemas\Components\Grid; use Inlay\Schemas\Components\Section; $form = Form::make('create-user') ->action(route('users.store')) ->method('post') ->validation(UserRules::class, 'create') ->precognitive(mode: 'blur', debounce: 350) ->schema([ Section::make('account')->label('Account')->schema([ Grid::make(2)->schema([ TextInput::make('name')->required()->autofocus(), TextInput::make('email')->email()->required(), Select::make('account_type') ->options([ 'personal' => 'Personal', 'company' => 'Company', ]) ->default('personal') ->live(), TextInput::make('company_name') ->visibleWhen('account_type', 'company') ->requiredWhen('account_type', 'company'), Toggle::make('active')->default(true), ]), ]), ]); return inertia('users/create', ['form' => $form]);
Available initial field types include text, textarea, select, checkbox, checkbox list, radio, toggle, toggle buttons, hidden, color, date/time, upload, slider, tags, key/value, code, Markdown, rich text, repeater, and builder fields.
Tables
Tables can execute allow-listed Eloquent searching, sorting, filtering, and pagination from request input. Only columns and filters explicitly marked as supporting a query operation can change the query.
$table = Table::make('users') ->searchPlaceholder('Search users…') ->columns([ TextColumn::make('name')->searchable()->sortable(), TextColumn::make('email')->searchable(), BadgeColumn::make('status'), BooleanColumn::make('active'), ]) ->filters([ SelectFilter::make('role')->options(Role::labels()), SelectFilter::make('status')->options(Status::labels()), ]) ->actions([ Action::make('edit')->url('/users/{id}/edit'), Action::make('delete') ->url('/users/{id}') ->method('delete') ->requiresConfirmation(), ]) ->query(User::query(), request()->query(), perPage: 15); return inertia('users/index', ['table' => $table]);
Named, server-authored views reuse the same allow-listed query contract in React and Vue:
use Inlay\Tables\Views\TableView; $table->views([ TableView::make('active') ->label('Active users') ->filters(['status' => 'active']) ->sort('name') ->default(), TableView::make('invited')->filters(['status' => 'invited']), ]);
The selected view is transported as {table}_view; explicit search, filter,
sort, grouping, and page-size parameters override its defaults. Standalone
TablePages also expose Save, Edit, and Delete controls when a personal-view
store is enabled. The default store is session-scoped; applications that need
cross-device persistence can bind TableViewStore to the database driver and
publish the optional inlay_table_views migration.
The initial catalog includes text, badge, boolean, icon, image, color, select, toggle, text-input, and checkbox columns; select, boolean, ternary, text, date, and numeric filters; row/header/bulk actions; selection; loading and empty states; and responsive pagination.
Centralized validation
Validation rules belong in one reusable Laravel class instead of being copied between a field schema, Form Request, import, and Resource controller.
use Illuminate\Validation\Rule; use Inlay\Validation\ValidationContext; use Inlay\Validation\Validation; final class UserRules extends Validation { public function rules(ValidationContext $context): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => [ 'required', 'email', Rule::unique('users', 'email')->ignore($context->record()), ], 'role' => ['required', Rule::in(['admin', 'member'])], ]; } public function prepare(array $data, ValidationContext $context): array { $data['email'] = strtolower(trim((string) ($data['email'] ?? ''))); return $data; } }
Generate application-owned validation classes with php artisan make:inlay-validation User, then execute the same validation from forms, imports, actions, or APIs through Inlay\Validation\ValidationRunner.
ValidationContext provides:
- Operation, such as
create,edit, or an application-specific operation. - Source, such as Form, Import, API, Action, or bulk processing.
- Prepared input data.
- Current record and authenticated user.
- Consumer-specific options.
Profiles also support custom messages, attribute labels, after-validation callbacks, dependency injection, and stop-on-first-failure behavior.
Resource persistence lifecycle
Resource mutations follow this order:
resolve scoped record (when required)
→ authorize operation
→ prepare and validate input
→ begin database transaction
→ before hook
→ mutate validated data
→ create/update/delete model
→ after hook
→ commit
→ redirect with success message
Applications can customize small lifecycle steps without replacing the controller:
/** * @param array<string, mixed> $data * @return array<string, mixed> */ protected static function mutateDataBeforeCreate(array $data): array { return [...$data, 'password' => Str::random(32)]; } protected static function afterCreate(Model $record, array $data): void { UserCreated::dispatch($record); }
Resources are fail-closed until canAccess() is implemented. Record lookup always uses the Resource query, so tenant or visibility scopes cannot be bypassed by passing a model identifier directly.
Infolists, imports, and panels
Infolists render read-only record details with the same shared layouts:
$infolist = Infolist::make('user') ->schema([ Section::make('Account')->columns(2)->schema([ TextEntry::make('name')->copyable(), TextEntry::make('email')->url('mailto:{state}'), IconEntry::make('active')->boolean(), ]), ]) ->data($user->toArray());
Imports provide column mapping, previews, centralized row validation, authorization, casting, isolated failures, persistence callbacks, and matching five-step React/Vue wizards. Queue transport and storage remain application-owned.
Panels define branding, paths, navigation groups, middleware, theme, plugins, and the application shell from PHP. The Admin package adds login/logout, a protected dashboard, automatic Resource CRUD routes, and Resource navigation. React and Vue renderers provide responsive side/top navigation, active states, badges, user menus, breadcrumbs, SPA-link adapters, and slots.
Theming and layout customization
Inlay supports three customization levels:
- Theme tokens for product-wide color, radius, and control sizing.
- Root classes and typed per-slot class maps for page-specific layout changes.
- Renderer registries for replacing built-in components or adding community components.
Choose the quiet shadcn-style foundation or the polished Inlay admin preset entirely from PHP:
use Inlay\Theme\Theme; $panel->theme( Theme::default() ->accent('#7c3aed') ->radius('0.875rem') ->font('Inter, ui-sans-serif, system-ui') ->tokens(['sidebar-width' => '18rem']) ->darkTokens(['accent' => '#a78bfa']) );
Theme::base() supplies a neutral zinc foundation. Both presets use semantic tokens—background, surface, foreground, muted, border, hover, status colors and status surfaces, overlays, radius, sizing, font, and shadow—so an application theme automatically reaches panels, forms, tables, actions, infolists, imports, media, permission pages, and widgets. New applications should import Design from inlayphp/design; the lower-level Theme and @inlayphp/theme APIs remain compatible for existing apps. Panel also forwards application-defined semantic tokens as scoped CSS variables, so community packages can add a token without forking the core theme.
Generate a PHP theme class and matching CSS variables instead of hand-writing the integration:
php artisan make:inlay-theme Brand
This creates app/Inlay/Themes/BrandTheme.php and resources/css/inlay/brand.css. The stylesheet includes light variables plus OS-preference and [data-theme="dark"] overrides. See inlayphp/design for the complete generator and frontend API.
const theme = { accent: '#2563eb', radius: '0.75rem', controlHeight: '2.5rem', 'table-row-hover': '#eff6ff', } <Form className="customer-form" resource={form} theme={theme} /> <Table className="customer-table" classNames={{ filtersPanel: 'bg-(--inlay-surface-muted)', applyButton: 'shadow-sm', }} resource={table} theme={theme} />
Stable semantic attributes make CSS overrides independent of internal Tailwind utility classes:
.customer-form [data-field='email'] { grid-column: 1 / -1; } .customer-table [data-slot='toolbar'] { align-items: end; } .customer-table [data-filter='status'] { min-width: 12rem; }
Plugin and renderer contracts reject duplicate ownership so two extensions cannot silently replace the same component. Plugin registration is atomic: shared registries roll back if registration fails.
Public wire contracts
Every top-level PHP object serializes a named, versioned contract:
{
"contract": "inlay.forms.v1",
"type": "form",
"name": "create-user",
"schema": []
}
Current top-level contracts include:
inlay.forms.v1inlay.tables.v1inlay.actions.v1inlay.infolists.v1inlay.imports.v1inlay.panels.v1inlay.resources.v1inlay.media-manager.v1inlay.themes.v1inlay.widget-dashboard.v1
Adding optional keys is backward-compatible. Changing the meaning or shape of an existing key requires a new contract version.
Arbitrary PHP closures never cross the Inertia boundary. Dynamic behavior must be resolved on the server or represented as explicit, allow-listed condition/action metadata.
Security model
The packages enforce several boundaries by default:
- Resource authorization is fail-closed.
- Resource records are resolved through the Resource’s scoped Eloquent query.
- Authorization occurs before mutation validation and persistence.
- Only validated input reaches Resource persistence.
- Eloquent fillable/guarded rules remain active.
- Resource writes and lifecycle hooks are transactional.
- Tables only apply declared searchable, sortable, and filterable query operations.
- URLs are normalized by a shared safe-URL policy that rejects executable and protocol-relative values.
- Conditions and component types use explicit allow lists.
- Page paths reject traversal, malformed encoding, and record-placeholder mismatches.
- Fresh builders are required to prevent mutable state leaking between long-running requests.
- Extension registries reject ownership collisions and roll back failed plugin registration.
- Panel routes declare abilities and enforce Laravel Gate decisions before returning or mutating data.
- Media uploads are private by default, use randomized object keys, and enforce configured size, MIME, and extension allow-lists.
- Media delivery uses short-lived signed URLs, authorization, private cache headers, and
nosniff; untrusted formats download instead of rendering inline. - Media deletion is recoverable until an explicit permanent-delete operation, and folder moves prevent cycles and logical orphans.
Application policies, domain-specific validation, database constraints, CSRF protection, authentication middleware, malware scanning, storage lifecycle rules, and rate limiting remain application responsibilities.
Community extensions
Community packages should be able to provide:
- Custom Form fields and renderers.
- Table columns, filters, and actions.
- Infolist entries.
- Action modal content.
- Panel plugins and navigation.
- Theme presets and semantic CSS.
- Shared PHP and frontend packages that register the same renderer key.
Custom components should publish a stable PHP payload, declare a renderer category, register a unique renderer key, and provide matching React and/or Vue implementations. The Core package supplies version compatibility checks and ownership-safe registries.
The community schema-view template is a cloneable, continuously tested example containing a Composer component plus React and Vue adapters. It demonstrates stable renderer naming, nested schema rendering, deferred data, registry ownership, package exports, and compatibility checks.
Repository layout
packages/
core/ Plugin and extension kernel
support/ Shared safe contracts
schemas/ Layout primitives
validation/ Central Laravel validation
actions/ Action contracts and adapters
form/ Forms PHP + React + Vue
table/ Tables PHP + React + Vue
infolist/ Infolists PHP + React + Vue
import/ Imports PHP + React + Vue
panel/ Panels PHP + React + Vue
theme/ Shared PHP and frontend theme presets (compatibility layer)
design/ Public design façade, CSS generator, and frontend recipes
notifications/ Session/database notification contracts + React/Vue renderers
widgets/ PHP dashboard widgets + React/Vue renderers
resources/ Resource CRUD orchestration
admin/ Authenticated panel composition
authorization/ Laravel Gate bridge and ability registry
authorization-spatie/ Roles, teams, super-admin, and permission sync adapter
permission-manager/ Dcat-style access manager + React pages
media/ Storage-agnostic secure media catalog
media-manager/ Panel media browser/picker + React/Vue pages
media-spatie/ Zero-copy Spatie Media Library adapter
two-factor-authentication/
Optional TOTP/recovery-code manager and panel login step
examples/
react/ Buildable React package example
vue/ Buildable Vue package example
community-schema-view/
Cloneable Composer + React + Vue community component template
playground/
laravel-react/ Laravel 12 + Inertia v3 + React 19 integration app with Vue standalone and panel showcase routes
docs/ Architecture, component matrix, and focused examples
tests/ Pest contract and PHP behavior tests
Run the playground
cd playground/laravel-react
composer install
pnpm install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate --seed
pnpm run build
php artisan serve --port=8013
Open http://127.0.0.1:8013/admin and sign in with test@example.com / password.
The playground demonstrates:
- A Resource-defined Form and Table.
- Authenticated login/logout, a dashboard, and a responsive admin shell.
- PHP-generated Resource navigation and protected CRUD routes.
- Server-side search, filters, sorting, and pagination.
- Conditional fields and Precognition metadata.
- Central validation shared with imports and Resource mutations.
- Transactional create, update, and delete routes.
- Accessible action confirmation.
- Theme tokens, class overrides, and collapsible code examples.
- Roles, permissions, user-role assignment, and an audit comparing registered, missing, synced, and stale abilities.
- A working media browser with seeded assets, folders, search, MIME filtering, uploads, metadata editing, trash/restore, and a reusable picker.
Monorepo development
Install dependencies from the repository root:
composer install pnpm install
Run PHP syntax and Pest tests:
composer lint
composer test
Run the complete release-oriented gate sequentially so declaration generation cannot race the export audit:
pnpm verify
The individual frontend commands remain available when iterating on one layer:
pnpm test:frontend pnpm typecheck pnpm build
Verify the Laravel playground:
cd playground/laravel-react composer lint:check composer types:check pnpm run lint:check pnpm run format:check pnpm run types:check php artisan test pnpm run build
The main test suites use Pest for PHP, Vitest with Testing Library for React/Vue, PHPStan/Larastan for Laravel static analysis, TypeScript and vue-tsc for adapter contracts, and production builds to verify package declarations and exports.
GitHub Actions runs the same package gates plus a separate PHP 8.4 / Node 22
integration job for playground/laravel-react. That job installs the
playground's committed Composer and pnpm lockfiles, runs its full Pest suite,
type checks, lint/format checks, and production build, so demo routes,
migrations, and renderer wiring cannot silently drift away from the packages
they demonstrate.
Documentation
- Release checklist and split-package process
- Architecture and package boundaries
- Component matrix
- Focused Laravel, React, and Vue examples
- Resources and CRUD lifecycle
- Forms
- Tables
- Actions
- Notifications
- Validation
- Imports
- Infolists
- Panels
- Core extensions and plugins
- Authorization
- Spatie authorization adapter
- Permission manager
- Media catalog
- Media manager
- Spatie media adapter
- Two-factor authentication
Roadmap
The next major milestones are:
- Deepen resource page and widget composition. Parent-scoped nested resource URLs, hosted action-form sub-transports, selection-aware bulk action modals, per-record bulk outcome reports, an allow-listed per-page chooser, removable filter indicators, PHP-declared filter form layout, per-column search and sort callbacks, safe header and per-record cell attributes, column actions, scoped and custom summarizers, arbitrary schema filters, closure-backed column and table presentation, fluent Relation Groups, and keyboard-accessible React/Vue tabs are available. Resource and Relation Manager soft deletes include scoped query, filter, row/bulk action, lifecycle, React/Vue, test DSL, and playground support. Validated pivot-aware create/attach/edit forms, secure searchable attach/detach, and HasMany/MorphMany associate/dissociate UI are available.
- Dashboard widget caching is available through the opt-in
CacheableWidgetsprovider contract. Resource header actions now use the resource action boundary with page authorization and render through the shared React/Vue Actions runtime, including confirmation, action forms, lifecycle responses, and custom transport hooks. Remaining page-level action UX and resource composition hardening are next, and panel widget providers can be discovered from an explicit application namespace. The standalone notification contract, session/database delivery, action integration, and React/Vue renderers are available. - An optional two-factor authentication plugin with encrypted TOTP/recovery
state, challenge and authenticated settings routes, and React/Vue challenge
and security-settings renderers. Existing Fortify models can reuse their
encrypted columns through Inlay's dependency-free storage adapter, and QR
rendering remains an application-edge contract. An opt-in Fortify challenge
bridge can keep Fortify as the authentication owner while using the shared
Inertia Form page. Panels exposes the ordered post-credential
LoginSteppipeline required by the native plugin. - Async relationship option loading and direct-to-storage upload adapters need broader application fixtures and release hardening across both renderers.
- Responsive layouts, relationship-group automation, and advanced bulk selection. Server-authored named views, owner-scoped personal save/edit/delete persistence, query-wide filtered CSV exports, selection-aware bulk CSV downloads, queued export payloads, and the optional first-party PhpSpreadsheet XLSX driver now reuse the same authorized React/Vue contract. Grouping, summaries, and column management have their first production-tested React/Vue slice.
- Rich text/code editor integrations are available; deeper schema reactivity and long-tail editor extension documentation remain release work.
- Vue playground parity is available for standalone Form/Table routes, the panel dashboard, a real UserResource CRUD comparison (list, create, and edit), the package-owned media-manager page, permission-manager access pages, and two-factor settings. Package-owned Vue plugin pages resolve through the same panel shell as local pages. Multi-panel discovery is available through
PanelRegistry::directoryFor()and the React/VuePanelSwitcher; remaining work is the long-tail Vue plugin-page sweep, tenant-aware starter kits, and reusable application starter kits. - The media manager now exposes a bounded filesystem storage browser with a stable PHP/React/Vue contract; community S3/API browsers can register through
MediaStorageRegistry. Media albums/collections, manager filtering, image focal-point metadata editing, bounded usage/reference inspection, and opt-in queued transformations are available. - Coordinated package versions, changelogs, upgrade guides, the first signed release tag, and community extension documentation. The split and npm publishing automation plus common registry metadata are now in place; the first public release still needs organization credentials and an explicitly approved version line.
The project does not aim to move Laravel business logic into React or Vue. Frontend adapters should stay replaceable; PHP contracts remain the durable public API.
License
Inlay is intended to be released under the MIT License. See individual package metadata for the current license declaration.