idei / usim
UI Services Implementation Model Framework
Requires
- php: ^8.2
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/filesystem: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- laravel/sanctum: ^3.0|^4.0
- nikic/php-parser: ^5.7
- symfony/finder: ^6.0|^7.0|^8.0
- symfony/var-dumper: ^6.0|^7.0|^8.0
Requires (Dev)
None
Suggests
- laravel/octane: Required to handle state reset in long-running processes.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-20 13:10:12 UTC
README
A Server-Driven UI framework for Laravel. Define your entire user interface in PHP — screens, menus, forms, tables, modals — and let the framework render, diff, and update everything automatically on the client.
Table of Contents
- Requirements
- What Is New Since 0.5.0
- Installation
- Core Concepts
- Available Components
- Screens in Depth
- Event System
- Built-in UI Helpers
- Database Translations
- Modals & Dialogs
- Data Tables
- File Uploads
- Authentication Scaffolding
- Testing Screens
- Core Contracts & Mockable Architecture
- Configuration
- Headless Mode
- API Endpoints
- Artisan Commands
- Octane / RoadRunner Support
- Directory Structure
- Release & Upgrade Guide
- License
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | 10.x / 11.x / 12.x / 13.x |
| laravel/sanctum | ^3.0 / ^4.0 |
| spatie/laravel-permission | ^6.0 |
| symfony/finder | ^6.0 / ^7.0 |
| nikic/php-parser | ^5.7 |
| symfony/var-dumper | ^6.0 / ^7.0 |
| illuminate/contracts | ^10.0 / ^11.0 / ^12.0 / ^13.0 |
What Is New Since 0.6.0 (v0.7.0)
- Container appearance API:
->card()and->plain()fluent helpers onContainerto switch between card and flat visual variants. - Container tabs API:
->tabs(),->tabItem(),->activeTab(),->onTabChange(),->onTabClose()and->tabColors()allow server-driven tab strips with theme-aware defaults, disabled tabs, closable tabs, and per-tab color overrides. - Carousel and calendar components consume CSS theme tokens for consistent light/dark styling.
- Bug fix:
Screennow always persists state afterpostLoadUI(), fixing stale cache on?reset=truereloads. - Bug fix: Checkbox
checkedstate now syncs correctly from incremental server responses.
For the full release details, see CHANGELOG.md.
Installation
composer require idei/usim
Laravel's package auto-discovery will register UsimServiceProvider automatically.
Quick Start
Run the install command to scaffold a complete working application with authentication, profile, menus, configuration, and routes:
php artisan usim:install
Then follow the printed instructions:
php artisan migrate ./start.sh [-r]
Starting the Application
The ./start.sh script uses RoadRunner instead of Laravel's artisan serve command. This is necessary because artisan serve is single-threaded and does not properly support the framework's concurrent execution requirements.
Usage
Note:
Visit http://localhost:8000 — you have a working USIM app.
Use
--forceto overwrite existing files.
Core Concepts
Screens
A Screen is a PHP class that defines a full page. Each screen extends Screen and builds its UI inside buildBaseUI():
Screen Operational Model
In USIM, treat a Screen as a stateful backend UI service, not as a passive template:
- A Screen owns UI structure, interaction rules, and persisted state.
buildBaseUI()defines the initial component tree.getRoutePath()derives the canonical URL from namespace/class naming.- Event actions resolve to
on<ActionName>(array $params)handlers on the same class. - Request cycle is: restore state -> run handler -> compute diff -> send only delta.
- Authorization is part of the Screen contract (
authorize,checkAccess). - Menu metadata also belongs to the Screen contract (
getMenuLabel,getMenuIcon,getRoutePath).
This model keeps backend as the source of truth and avoids business-logic duplication across client and server.
Important clarification: USIM does not register one Laravel route per Screen class. Instead, it uses a catch-all web route plus a generic /api/ui/{screen} loader and resolves URL <-> Screen class through naming convention.
<?php namespace App\UI\Screens; use Idei\Usim\UI; use Idei\Usim\Enums\LayoutType; use Idei\Usim\Screen; use Idei\Usim\Components\Container; class HelloScreen extends Screen { protected function buildBaseUI(Container $container, ...$params): void { $container ->layout(LayoutType::VERTICAL) ->padding(20); $container->add( UI::label('title') ->text('Welcome to the UsersManager') ->style('h1') ); $container->add( UI::button('hello_btn') ->label('Hello USIM!') ->primary() ->action('hello_button_clicked') ); } public function onHelloButtonClicked(array $params): void { $this->toast('Data refreshed!', 'success'); } }
After creating the file, register it:
php artisan usim:discover
Then visit /hello-screen in your browser.
UI — The Component Factory
UI is a static factory that creates component builders. Every builder uses a fluent API:
// Labels UI::label('greeting')->text('Hello World')->style('h2')->center(); UI::label('legal_copy')->html('legal.terms-snippet'); // Buttons UI::button('save')->label('Save')->style('primary')->action('save_form'); UI::button('floating_help') ->label('Help') ->style('secondary') ->position('BOTTOM_RIGHT') ->offsets(24, 24) ->action('open_help'); // Inputs UI::input('email')->label('Email')->type('email')->required(true)->placeholder('you@example.com'); // Containers (layouts) $row = UI::container('toolbar') ->layout(LayoutType::HORIZONTAL) ->gap('10px'); $row->add(UI::button('btn_a')->label('A')); $row->add(UI::button('btn_b')->label('B')); $container->add($row); $tabs = UI::container('workspace_tabs') ->tabs([ ['id' => 'overview', 'label' => 'Overview', 'color' => '#dbeafe', 'active_color' => '#2563eb'], ['id' => 'reports', 'label' => 'Reports', 'disabled' => true, 'disabled_color' => '#e2e8f0'], ['id' => 'settings', 'label' => 'Settings', 'closable' => true], ], 'overview') ->onTabChange('workspace_tab_changed') ->onTabClose('workspace_tab_closed') ->tabColors([ 'active_tab_color' => 'var(--ui-surface)', 'active_tab_text_color' => 'var(--ui-text-strong)', ]); $tabs->add(UI::label('overview_copy')->text('Overview content'), tab: 'overview'); $tabs->add(UI::label('reports_copy')->text('Reports content'), tab: 'Reports'); $container->add($tabs);
Event Handlers
When a button fires an action (e.g. ->action('save_form')), the framework calls a handler method on the same screen class. The convention is on + PascalCase action name:
| Action string | Handler method |
|---|---|
save_form |
onSaveForm(array $params) |
delete_item |
onDeleteItem(array $params) |
navigate_home |
onNavigateHome(array $params) |
The $params array contains all current component values (inputs, selects, checkboxes, etc.) from the client.
State Management
Screen state is server-side. The framework automatically:
- Builds the UI tree on first load
- Stores the serialized state
- On events, restores state → runs your handler → diffs old vs new → sends only the delta to the client
Properties prefixed with store_ are persisted across requests and mirrored on the client as part of the USIM storage payload.
Persistence is now plain by default:
store_*values are serialized as regular JSON values so the client can inspect and use them directly.- Add the
_cryptsuffix only for sensitive values that must be protected before being sent to client storage. - Browser clients persist storage in
sessionStorage(scoped per browser tab) and sendX-UI-Client-IdalongsideX-USIM-Storage, guaranteeing state and authentication isolation across tabs. - This makes client-side decisions possible for non-sensitive state such as
store_theme, and also enables finer-grained storage synchronization because the client can apply only the changed keys instead of replacing one fully encrypted blob each time.
class MyScreen extends Screen { protected string $store_username = ''; // persisted in plain text protected int $store_page = 1; // persisted in plain text protected string $store_theme = 'light'; // readable by the client protected string $store_token_crypt = ''; // persisted encrypted protected string $tempValue = ''; // NOT persisted }
Use _crypt only when the value should not be readable from client storage.
Available Components
| Factory Method | Builder Class | Description |
|---|---|---|
UI::label() |
Label |
Text labels, headings, paragraphs |
UI::button() |
Button |
Action buttons with styles |
UI::input() |
Input |
Text, email, password, hidden inputs |
UI::select() |
Select |
Dropdown selects |
UI::checkbox() |
Checkbox |
Checkboxes and toggles |
UI::form() |
Form |
Form grouping |
UI::table() |
Table |
Data tables with pagination |
UI::card() |
Card |
Cards with title, description, actions |
UI::container() |
Container |
Layout container (vertical/horizontal/grid) with card() / plain() appearance and optional server-driven tabs |
UI::menuDropdown() |
MenuDropdown |
Navigation dropdown menus |
UI::uploader() |
Uploader |
File upload with preview and crop |
UI::calendar() |
Calendar |
Calendar/date picker |
UI::carousel() |
Carousel |
Media carousel for image/audio/video with manual/auto modes |
UI::split() |
Split |
Resizable/collapsible split panel layout |
UI::timer() |
Timer |
Autonomous timer for polling, delays, and scheduled events |
Builders share a fluent API across UIComponent and Container with common methods like:
->visible(bool $visible) ->width(string $width) ->padding(mixed $padding) ->margin(mixed $margin) // ... and many more styling options
Screens in Depth
Creating a Screen
- Create a class in
app/UI/Screens/(or a subdirectory):
// app/UI/Screens/Products/List.php namespace App\UI\Screens\Products; use Idei\Usim\Screen; use Idei\Usim\Components\Container; use Idei\Usim\UI; class List extends Screen { protected function buildBaseUI(Container $container, ...$params): void { $container->add( UI::label('title')->text('Products')->style('h1') ); // Add your table, filters, etc. } }
- Run discovery:
php artisan usim:discover
- Visit
/products/list— the URL is automatically derived from the namespace path.
Screen Discovery
USIM uses Symfony Finder to scan the configured screens directory and generates a manifest cache:
php artisan usim:discover
# Output: Found 12 screens. USIM manifest generated successfully!
The manifest maps URL slugs to screen classes using a CRC32-based offset system for deterministic component IDs.
Authorization
Override authorize() to control access:
// Public screen (default) public static function authorize(): bool { return true; } // Authenticated users only public static function authorize(): bool { return self::requireAuth(); } // Guests only (e.g. login screen) public static function authorize(): bool { return !self::requireAuth(); } // Role-based public static function authorize(): bool { return self::requireRole('admin'); } // Permission-based public static function authorize(): bool { return self::requirePermission('manage-users'); }
When authorization fails, the framework automatically redirects to login (for guests) or shows a 403 (for insufficient permissions).
Menu Integration
Screens integrate with the navigation menu via static methods:
public static function getMenuLabel(): string { return 'My Screen'; } public static function getMenuIcon(): ?string { return '📊'; }
Then in your Menu screen, use $menu->screen(MyScreen::class) for automatic linking with permission checks:
$menu->screen(UsersManager::class); // auto label + icon $menu->screen(Products\List::class, 'All Products', '📦'); // custom label + icon
Lifecycle Hooks
| Method | When |
|---|---|
buildBaseUI($container) |
Called on first load to build the initial UI tree |
postLoadUI() |
Called after state is restored — update components with live data |
onResetScreen() |
Called when ?reset=true is passed in the URL |
protected function postLoadUI(): void { // Update components with current data after state restoration $user = Auth::user(); $this->input_name->value($user->name); $this->input_email->value($user->email); }
Component auto-injection: If you declare a typed property with the same name as a component ID, USIM automatically injects the builder instance. For example,
protected Input $input_name;will be populated with the input created asUI::input('input_name').
Event System
Handling Button Actions
// In buildBaseUI: $container->add( UI::button('btn_save') ->label('Save') ->action('save_item') // → calls onSaveItem() ); // Handler: public function onSaveItem(array $params): void { $name = $params['input_name'] ?? ''; $email = $params['input_email'] ?? ''; // Save to database... $this->toast('Item saved!', 'success'); }
Cross-Service Events
Emit events that ALL active screen services receive using UsimEvent:
use Idei\Usim\Events\UsimEvent; // Emit from anywhere: event(new UsimEvent('user_logged_in', ['user' => $user]));
Any screen with a matching handler will react:
// In Menu screen or any other screen: public function onUserLoggedIn(array $params): void { $user = $params['user']; $this->updateMenuForUser($user); }
Built-in UI Helpers
Available inside any Screen handler:
// Show a toast notification $this->toast('Operation successful', 'success'); // types: success, error, info, warning // Navigate to another URL $this->redirect('/products'); $this->redirect(); // reload current screen // Close the currently open modal $this->closeModal(); // Update modal fields $this->updateModal([ 'field_name' => ['error' => 'This field is required'] ]); // Show an error page $this->abort(404, 'Not found'); // Switch theme on the client $this->changeTheme('dark'); // Switch language on the client and set application locale $this->changeLanguage('es');
Labels can now render raw HTML or an existing Blade view:
UI::label('welcome_copy') ->html('emails.verify-email', ['user' => $user]);
Most components also support anchor-based positioning helpers for floating or pinned UI:
UI::container('floating_panel') ->position('TOP_RIGHT') ->positionMode('fixed') ->offsets(16, 16);
Database Translations
USIM now supports package-level database translations with key-based identifiers and language fallback.
Published migration stubs create these tables:
usim_languagesusim_text_keysusim_text_values
The TranslationService manages CRUD for languages, keys, and values (including optional media fields).
Global utility helper:
t('app.welcome', ['name' => 'Emilio']);
Resolution order:
- current locale in DB
- fallback locale in DB (
enby default) - key literal
Translation values support placeholders (:name, :count, etc.) and optional media metadata (media_url, media_meta).
Auto-key behavior for human-readable text (t('Some text')):
- key length limit is configurable via
usim.i18n.auto_key_max_length(env:USIM_I18N_AUTO_KEY_MAX_LENGTH, default20) - when truncation is needed, USIM tries to continue to the next separator so the current word is not cut mid-word
- escaped and real line breaks are normalized before key generation and fallback text storage
I18n suggestion logging:
- when a key is auto-generated from human-readable text, USIM emits an i18n warning suggesting to replace the literal text with the generated key
- log context includes generated key, source text, group, file, line, and best-effort character position
- configure channel with
usim.i18n.log_channel(env:USIM_I18N_LOG_CHANNEL, defaulti18n) - enable/disable with
usim.i18n.log_autokey_suggestions(env:USIM_I18N_LOG_AUTOKEY_SUGGESTIONS, defaulttrue)
Recommended key naming for package and scaffolded code:
usim.component.*for reusable package component defaults.usim.dialog.*andusim.time_unit.*for framework-level dialog and timer labels.usim.common.*for shared scaffold labels/placeholders.usim.auth.*,usim.admin.*,usim.menu.*for generated screen/component UI text.usim.service.*for scaffolded service response and validation messages.
When adding or changing scaffold text, prefer t('...') with one of the namespaces above and include the key in UsimTranslationSeeder.
Modals & Dialogs
Quick Confirmation Dialogs
Use ConfirmDialog for standard dialogs:
use Idei\Usim\Modals\ConfirmDialog; use Idei\Usim\Enums\DialogType; public function onDeleteItem(array $params): void { ConfirmDialog::open( caller: $this, type: DialogType::CONFIRM, title: 'Delete Item', message: 'Are you sure you want to delete this item?', confirmAction: 'confirm_delete', cancelAction: 'cancel_delete', ); } public function onConfirmDelete(array $params): void { // Perform the delete... $this->closeModal(); $this->toast('Item deleted', 'success'); }
Dialog types: INFO, CONFIRM, WARNING, ERROR, SUCCESS, CHOICE, TIMEOUT.
Custom Modals
Build custom modal content using any component and set ->parent('modal'):
$modal = UI::container('my_modal') ->parent('modal') ->padding('20px'); $modal->add(UI::input('field_a')->label('Name')); $modal->add( UI::button('btn_submit') ->label('Submit') ->action('submit_modal') );
Data Tables
For paginated server-side data tables, extend AbstractTableModel:
use Idei\Usim\DataTable\AbstractTableModel; class ProductsTable extends AbstractTableModel { public function getColumns(): array { return [ ['name' => 'id', 'type' => 'int'], ['name' => 'name', 'type' => 'string'], ['name' => 'price', 'type' => 'float'], ]; } public function getFormattedPageData(int $currentPage, int $perPage): array { return Product::paginate($perPage, ['*'], 'page', $currentPage) ->items(); } }
Then use it in your screen:
$table = UI::table('products_table'); $dataModel = new ProductsTable($table); // configure columns, pagination, etc. $container->add($table);
File Uploads
Use the Uploader for file uploads with temporary storage, preview, and image cropping:
use Idei\Usim\Enums\AspectRatio; $uploader = UI::uploader('avatar') ->label('Profile Photo') ->allowedTypes(['image/*']) ->maxFiles(1) ->maxSize(2) // MB ->aspect(AspectRatio::_1_1) // crop ratio ->size(1); // display size $container->add($uploader);
In your event handler, confirm the upload to move it from temporary to permanent storage:
public function onSaveProfile(array $params): void { if ($filename = $this->uploader_avatar->confirm($params, 'images', $oldFilename)) { $user->avatar = $filename; $user->save(); } }
Temporary uploads are automatically cleaned up hourly via a scheduled job.
Image placeholders
An Image without a src receives a generated placeholder by default. The default
generator creates an SVG locally using the component caption and configured colors.
Its dimensions use pixel-sized width/height when provided, or the configured
placeholder dimensions; AspectRatio fills in a missing dimension:
use Idei\Usim\Enums\AspectRatio; use Idei\Usim\ValueObjects\Size; $image = UI::image('post_image') ->width(Size::px(640)) ->placeholderAspect(AspectRatio::_16_9) ->placeholderId($post->id) ->placeholderColors('#e2e8f0', '#475569') ->caption($post->title);
For images saved by USIM's upload flow, storage() resolves a relative filename
with UploadService::fileUrl(). Absolute URLs and root-relative paths remain
unchanged:
$image = UI::image("post_img_{$post->id}") ->src($post->image ?? '') ->storage('uploads/images') ->placeholderAspect(AspectRatio::_16_9) ->placeholderId($post->id);
placeholderId() provides a stable identity for seeded image services. To use
Picsum, set USIM_IMAGE_PLACEHOLDER_GENERATOR to
Idei\Usim\Services\ImagePlaceholders\PicsumImagePlaceholderGenerator; the URL
uses the configured seed prefix and the same identity to return a stable image.
To integrate another image API (including one requiring an API key), implement
ImagePlaceholderGeneratorInterface and configure its fully-qualified class in
USIM_IMAGE_PLACEHOLDER_GENERATOR, or bind that interface in the application.
The available .env settings are USIM_IMAGE_PLACEHOLDER_ENABLED,
USIM_IMAGE_PLACEHOLDER_WIDTH, USIM_IMAGE_PLACEHOLDER_HEIGHT,
USIM_IMAGE_PLACEHOLDER_BACKGROUND, USIM_IMAGE_PLACEHOLDER_TEXT_COLOR, and
USIM_IMAGE_PLACEHOLDER_SEED_PREFIX. An explicit src always takes precedence;
an action-based image shows its placeholder while the request is in progress.
fallback() remains the separate behavior for a source that fails to load.
Authentication Scaffolding
php artisan usim:install provides a complete authentication and admin system:
| Screen | Path | Description |
|---|---|---|
Login |
/auth/login |
Email/password login with Sanctum tokens |
ForgotPassword |
/auth/forgot-password |
Send password reset link via email |
ResetPassword |
/auth/reset-password |
Reset password form |
EmailVerified |
/auth/email-verified |
Email verification handler |
Profile |
/auth/profile |
User profile (name, photo, password change) |
Admin\UsersManager |
/admin/dashboard |
User management table with CRUD and role assignment (admin only) |
Supporting files:
- AuthController — API endpoints for register, login, logout, verify email, reset password
- UsimUser trait — Custom notification methods for password reset and email verification
- UserService — Full user management: find, get, create, update (with role sync, email validation, notifications)
- Default root/admin/user provisioning is now handled during
php artisan usim:installthrough config-driven database upsert. - EventServiceProvider — App-level event/listener registration scaffold
- Email view stubs — Styled Blade views for password reset and email verification emails
- Terms view — Blade view for terms and conditions display
- Migrations —
temporary_uploads,profile_imagecolumn, andterms_accepted_atcolumn on users table
Default Users (via .env)
After install, configure your .env:
ADMIN_FIRST_NAME=Admin ADMIN_LAST_NAME=User ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=your-secure-password USER_FIRST_NAME=Regular USER_LAST_NAME=User USER_EMAIL=user@example.com USER_PASSWORD=your-secure-password
The installer now provisions the default access data during php artisan usim:install; no separate package seeder is required.
Testing Screens
This package ships a self-contained testing guide under docs/:
docs/SCREEN_TESTING_GUIDE.md— Human-oriented guide with patterns, helpers, and examples.docs/tests_prompt.md— Copy/paste prompt template to ask any agent/chat to generate new screen tests.
Recommended workflow:
- Read the guide to follow the project conventions (
uiScenario, component-level assertions, response contracts). - Use
docs/tests_prompt.mdas a base when delegating test generation to an AI agent. - Validate locally with
php artisan test(or file-level execution first).
Core approach used across this project:
$ui = uiScenario($this, SomeScreen::class, ['reset' => true]); $ui->component('btn_submit')->expect('action')->toBe('submit_form'); $response = $ui->click('btn_submit', ['field' => 'value']); $response->assertOk(); expect($response->json('toast.type'))->toBe('success'); $ui->assertNoIssues();
Core Contracts & Mockable Architecture
USIM is decoupled from the host application using Dependency Inversion (DIP). The core package never depends on host models like App\Models\User or concrete database libraries directly. Instead, it relies on strict interfaces in Idei\Usim\Contracts:
| Interface | Namespace | Purpose |
|---|---|---|
AuthorizableActorInterface |
Idei\Usim\Contracts |
Entities that can be checked for permissions (getAuthIdentifier()) |
UsimUserInterface |
Idei\Usim\Contracts |
User profile representation (getEmail(), getName()) |
PairableActorInterface |
Idei\Usim\Contracts |
Kiosk and physical device actor abstraction (isPaired(), getPairingIdentifier()) |
ScreenAuthorizerInterface |
Idei\Usim\Contracts |
Evaluates permissions/roles for screens without tight coupling to Spatie or DB |
UnitsServiceInterface |
Idei\Usim\Contracts |
Contract for organizational units synchronization |
UnitContextResolverInterface |
Idei\Usim\Contracts |
Resolves active operational unit slugs |
UnitSyncResult |
Idei\Usim\Contracts |
Readonly, strongly-typed result DTO for unit synchronizations |
In-Memory Testing with Mocks
Because screens authorize via ScreenAuthorizerInterface, you can test authorization in unit tests without database migrations, seeds, or Spatie permissions:
use App\Models\User; use App\UI\Screens\Admin\UsersManager; use Idei\Usim\Contracts\ScreenAuthorizerInterface; use Mockery; it('authorizes admin via mock authorizer', function () { $user = new User(['id' => 1, 'email' => 'admin@example.com']); $authorizer = Mockery::mock(ScreenAuthorizerInterface::class); $authorizer->shouldReceive('can')->with($user, 'users.manage', Mockery::any())->once()->andReturn(true); app()->instance(ScreenAuthorizerInterface::class, $authorizer); $screen = new UsersManager(); expect($screen->authorize($user))->toBeTrue(); });
Configuration
Publish the config file (done automatically by usim:install):
php artisan vendor:publish --tag=usim-config
This creates config/usim.php:
return [ 'front_store_key' => env('FRONT_STORE_KEY', 'my-app'), 'screens_namespace' => 'App\\UI\\Screens', 'screens_path' => app_path('UI/Screens'), 'api_url' => env('API_BASE_URL', env('APP_URL')), 'upload_disk' => env('UPLOAD_DISK', 'local'), 'users' => [ 'roles' => [ // admin/user role metadata + seed_user defaults ], ], 'i18n' => [ 'default_locale' => env('USIM_DEFAULT_LOCALE', env('APP_LOCALE', 'en')), 'fallback_locale' => env('USIM_FALLBACK_LOCALE', 'en'), ], ];
| Key | Description | Default |
|---|---|---|
front_store_key |
Unique application identifier used to scope persisted UI storage keys | my-app (override via FRONT_STORE_KEY) |
screens_namespace |
PSR-4 namespace where screens live | App\UI\Screens |
screens_path |
Filesystem path to scan for screens | app/UI/Screens |
api_url |
Base URL for internal HTTP calls | APP_URL |
upload_disk |
Laravel filesystem disk for uploaded files | local (override via UPLOAD_DISK) |
users.roles |
Role metadata and default seeded users | admin / user roles scaffold |
i18n.default_locale |
Preferred locale for DB translation lookup | APP_LOCALE or en |
i18n.fallback_locale |
Fallback locale for DB translations | en |
Headless Mode
USIM can run in headless mode, serving API endpoints only without requiring a web renderer.
Configuration
Set the environment variable:
USIM_HEADLESS_MODE=true
Behavior
- When
USIM_HEADLESS_MODE=true: Requests to the web catch-all route return406 Not Acceptablewith JSON error - All clients must consume
/api/ui/{screen}and/api/ui-eventendpoints directly - API endpoints function identically in both modes
- Ideal for backend-driven applications, AI agents, mobile apps, or multi-client architectures
Example (Node.js/JavaScript Client)
// Load a screen const screenResponse = await fetch('/api/ui/login'); const screen = await screenResponse.json(); console.log(screen); // { // "10": { "type": "container", "parent": "root", ... }, // "11": { "type": "input", "parent": 10, ... }, // "agent_context": { // "purpose": "User login with email/password", // "inputs": ["email", "password"], // "outputs": ["redirect", "toast", "abort"] // } // } // Extract agent context (if present) if (screen.agent_context) { console.log('Screen purpose:', screen.agent_context.purpose); }
Example (PHP Client)
// Use Laravel HTTP client or GuzzleHttp to consume API directly $response = Http::get(config('usim.api_url') . '/api/ui/admin/dashboard'); $screen = $response->json(); if (isset($screen['agent_context'])) { // Agent access to metadata $context = $screen['agent_context']; }
API Endpoints
USIM registers these routes automatically:
| Method | URI | Description |
|---|---|---|
GET |
/api/ui/{screen} |
Load a screen (returns JSON UI tree) |
POST |
/api/ui-event |
Handle an event (returns JSON diff) |
POST |
/api/upload/temporary |
Upload a file to temporary storage |
DELETE |
/api/upload/temporary/{id} |
Remove a temporary upload |
GET |
/files/{path} |
Serve uploaded files |
The client-side JavaScript (ui-renderer.js) handles these calls automatically.
Artisan Commands
| Command | Description |
|---|---|
php artisan usim:discover |
Scan screens directory and generate manifest cache |
php artisan usim:install |
Scaffold a complete USIM application (screens, auth, admin, tests) |
php artisan usim:install --force |
Overwrite existing files |
Octane / RoadRunner Support
USIM is compatible with long-running processes. The service provider automatically resets the component ID generator on each request when laravel/octane is detected:
// Automatic — no configuration needed $events->listen(\Laravel\Octane\Events\RequestReceived::class, function () { UIIdGenerator::reset(); });
The UIChangesCollector is registered as a scoped singleton, ensuring clean state per request.
Directory Structure
After usim:install, your application will have:
app/
├── Http/Controllers/Api/
│ └── AuthController.php # Auth API endpoints
├── Models/
│ └── User.php # With UsimUser, HasRoles, HasApiTokens traits
├── Services/
│ ├── Auth/
│ │ ├── AuthSessionService.php
│ │ ├── LoginService.php
│ │ ├── PasswordService.php
│ │ └── RegisterService.php
│ └── User/
│ └── UserService.php # Full CRUD, role management, email notifications
└── UI/
├── Components/
│ ├── DataTable/
│ │ └── UserTableModel.php # Paginated user table
│ └── Modals/
│ ├── EditUserDialog.php
│ └── RegisterDialog.php
└── Screens/
├── Home.php # Landing page
├── Menu.php # Navigation menu (links UsersManager for admins)
├── Admin/
│ └── UsersManager.php # User management (admin only)
└── Auth/
├── Login.php
├── ForgotPassword.php
├── ResetPassword.php
├── EmailVerified.php
└── Profile.php
config/
└── usim.php # USIM configuration (includes users.roles)
database/
├── migrations/
│ ├── *_create_temporary_uploads_table.php
│ ├── *_add_profile_image_to_users_table.php
│ └── *_add_terms_accepted_at_to_users_table.php
└── seeders/
└── (consumer-defined seeders only; USIM no longer ships package seeders)
providers/
└── EventServiceProvider.php
resources/views/emails/
├── reset-password.blade.php
└── verify-email.blade.php
resources/views/
└── terms.blade.php
routes/
├── api-auth.php # Auth API routes
└── web.php # + catch-all route for screens
The package itself lives in vendor/idei/usim/ (or packages/idei/usim/ during development).
License
MIT