This file provides guidance to AI agents (Claude, GPT, Copilot, etc.) when working with code in this repository.
Monorail is a Server-Driven UI (SDUI) framework for Laravel + Inertia.js + React. The server sends complete UI schemas (JSON) describing what to render; the React client renders it deterministically. This replaces traditional client-side state management and API design with a single source of truth in PHP.
- Package:
monorailphp/monorail(Composer) /@monorail/monorail(NPM) - Namespace:
Monorail - Stack: PHP 8.2+ · Laravel 11–13 · Inertia v3 · React 19 · Tailwind CSS v4 · Pest 4
- Type: Laravel service provider package with dual frontend (React) and backend (PHP) components
# Run all tests (from package root)
composer test
./vendor/bin/pest
# Run specific test file
./vendor/bin/pest tests/Feature/ResourceTest.php
# Run tests matching a pattern
./vendor/bin/pest --filter=testResourceIndex
# Run with compact output
./vendor/bin/pest --compact# Install PHP dependencies
composer install
# Install Node dependencies
npm installMonorail serializes all UI to JSON. The server generates schemas; React renders them deterministically without client-side decision-making.
Request → ResourceController → Resource::table()/form() → Inertia Serialization
↓
JSON Schema
↓
React Renderer
↓
Stateless UI
Each Panel scopes:
- Routes — auto-registered under
/admin(or custom path) - Resources — Eloquent models with CRUD pages, tables, forms, authorization
- Pages — dynamic content (CRUD + discoverable custom pages)
- Middleware — auth, error handling, view rendering
src/
├── Commands/ # Artisan generators: make-panel, make-resource
├── Dashboard/ # Widget classes: StatWidget, ChartWidget, TableWidget, etc.
├── Facades/ # Monorail:: facade for runtime access
├── Forms/
│ ├── Components/ # Field types: TextInput, Select, BelongsTo, DatePicker, etc.
│ └── Form.php # Form schema builder
├── Http/
│ ├── Controllers/ # ResourceController, GlobalSearchController, PanelController
│ └── Middleware/ # RenderMonorailErrorPages, authorization middleware
├── Pages/
│ ├── Blocks/ # Block types: WidgetBlock, GridBlock, HtmlBlock
│ ├── CreateRecordPage.php
│ ├── EditRecordPage.php
│ ├── ListRecordsPage.php
│ ├── ViewRecordPage.php
│ ├── DashboardPage.php
│ └── ResourcePage.php # Base for custom page discovery
├── Panel/
│ ├── Panel.php # Fluent config: path(), brand(), middleware(), etc.
│ ├── PanelManager.php # Registry (singleton)
│ └── PanelProvider.php # Abstract base for app panel definitions
├── Resources/
│ ├── RelationManagers/ # Manage related records on edit/view pages
│ └── Resource.php # Abstract base: table(), form(), widgets(), policies
├── Support/
│ ├── Contracts/ # Interfaces: HasColor, HasIcon, HasLabel
│ ├── Enums/ # Density, Font, Color, Grid layout constants
│ ├── Color.php # Color token utilities
│ └── Traits/ # Reusable concerns
└── Tables/
├── Actions/ # Row/bulk actions: EditAction, DeleteAction, etc.
├── Columns/ # Column types: TextColumn, BadgeColumn, ImageColumn, etc.
├── Filters/ # Filters: SelectFilter, DateRangeFilter, TrashedFilter
└── Table.php # Table schema builder
resources/js/
├── pages/
│ ├── page.tsx # List, Create, Edit, View pages (route-driven)
│ ├── dashboard.tsx # Dashboard with widget grid
│ └── error.tsx # Error page
├── components/
│ ├── panel-shell.tsx # Responsive sidebar + header layout
│ ├── data-table.tsx # Paginated, sortable, filterable table
│ ├── record-form.tsx # Form renderer with validation
│ ├── widget-renderer.tsx # Widget schema renderer
│ ├── block-renderer.tsx # Page block renderer
│ └── ui/ # shadcn/ui: button, input, select, etc.
├── lib/
│ ├── types.ts # TypeScript definitions for all schemas
│ ├── grid.ts # Grid layout utilities
│ └── utils.ts # Formatting, color mapping, etc.
└── monorail.tsx # Entry point for Vite
tests/
├── Feature/ # Integration tests: resources, forms, auth, policies
├── Fixtures/ # Stub models, policies, relation managers
└── TestCase.php # Base test setup (Orchestra\Testbench)
Panel— fluent configuration object for routes, resources, middleware, themePanelManager— singleton registry of all panels; auto-discovered or manually registeredPanelProvider— abstract base; host app extends this to define panels
- Abstract class; one per Eloquent model (e.g.,
UserResource extends Resource) - Declares:
$model— Eloquent classtable()— columns, filters, search, paginationform()— fields, layout, validationwidgets()— dashboard widgetsrelationManagers()— related-record managers- Custom pages via
discoverPages()in resource subdirectory
- Fully policy-gated (checks
viewAny,view,create,update,delete)
Form— fluent builderField— abstract base; subclasses:TextInput,Select,BelongsTo,DatePicker,FileUpload,KeyValue, etc.- Fields serialize to JSON with validation rules
SectionandTabsfor layout
Table— fluent builder; columns, filters, search, pagination, actionsColumn— abstract base; subclasses:TextColumn,BadgeColumn,ImageColumn,BooleanColumn,IconColumn- Sorting, filtering, pagination via query params
ResourceController— handles CRUD; calls Resource methods to build schemas- Routes auto-registered by
PanelManagerinroutes/monorail.php - No custom route definitions required
All UI is generated server-side and serialized to JSON:
Table::toArray()→ column schema, filters, actionsForm::toArray()→ field schema, layout, validation rulesWidget::toArray()→ data, metadata, rendering hints- Inertia passes JSON to React
- React deterministically renders without client-side logic
Every Resource enforces policies:
viewAny()→ show in nav, access list pageview()→ access view pagecreate()→ show create button/pageupdate()→ show edit button/pagedelete()→ show delete action
If user lacks viewAny, resource is hidden from nav automatically.
// 1. Create the PHP class
php artisan monorail:make-resource Post
// 2. Define model and table
public static string $model = Post::class;
public static function table(Table $table): Table {
return $table->columns([
TextColumn::make('id')->sortable(),
TextColumn::make('title')->sortable(),
TextColumn::make('created_at')->dateTime(),
]);
}
// 3. Define form (enables create/edit)
public static function form(Form $form): Form {
return $form->fields([
TextInput::make('title')->required(),
Textarea::make('body'),
]);
}
// 4. Auto-discovered in PanelProvider::discoverResources()- Subclass
Fieldinsrc/Forms/Components/FieldName.php - Implement
toArray(): arrayto serialize schema - Create React component in
resources/js/components/form-field-*.tsx - Add test in
tests/Feature/Forms/
- Subclass
Columninsrc/Tables/Columns/ColumnName.php - Implement formatting logic
- Create React component in
resources/js/components/table-cell-*.tsx - Add test in
tests/Feature/Tables/
- Create in
src/Dashboard/MyWidget.php - Implement
toArray()for serialized data - Declare in
Resource::widgets()with options:->columnSpan(1-6)— grid width->only(['list', 'edit'])— show on specific pages
- Create React component in
resources/js/components/widget-*.tsx
# All tests
./vendor/bin/pest
# Specific test
./vendor/bin/pest tests/Feature/ResourceTest.php
# Matching pattern
./vendor/bin/pest --filter=testName- Serialization First — All UI is generated in PHP, serialized to JSON, rendered by React
- No Client-Side Logic — React never makes decisions; it renders what the server sends
- Fluent Configuration — Builders use method chaining (
Panel::path()->brand()->middleware()) - Policy-Gated — All data access respects Laravel policies and gates
- Discoverable Components — Resources, pages, relation managers auto-discovered from directories
- Type-Safe Throughout — PHP type hints + TypeScript types + serialized validation rules
- Stateless Frontend — React components are pure renderers; no state management needed
- No JavaScript in resource definitions — All business logic lives in PHP
- Validation rules are serialized — React shows inline errors; no re-validation
- Authorization is server-side — Policies control visibility and operations
- Tables/forms are immutable from client — Forms POST back; tables show data only
- Dashboard layout is responsive —
Panel::dashboardColumns(4)affects widget grid - Configuration is publishable — Host app publishes
config/monorail.phpfor customization
- Use
TestCasebase (extendsOrchestra\Testbench\TestCase) - Fixtures in
tests/Fixtures/— stub models, policies, relation managers - In-memory SQLite database for all tests (configured in
phpunit.xml) - Every change requires a corresponding test or test update
Example test structure:
test('UserResource lists expected columns', function () {
$columns = UserResource::table(Table::make())->getColumns();
expect($columns)->toHaveCount(3);
expect($columns[0])->toBeInstanceOf(TextColumn::class);
});Monorail registers with the host app's build system:
/* In host app's app.css */
@import 'tailwindcss';
@source '../../vendor/monorail/monorail/resources/js';// In host app's vite.config.ts
laravel({
input: [
'resources/css/app.css',
'resources/js/app.tsx',
'vendor/monorail/monorail/resources/js/monorail.tsx',
],
}),
resolve: {
alias: {
'@monorail': path.resolve(__dirname, 'vendor/monorail/monorail/resources/js'),
},
},In the host app:
npm run dev # Watch frontend
npm run build # Production build