Adminix Documentation Help

Security

Adminix is an admin panel package. Treat every request to Adminix routes as privileged application traffic.

Authorization

By default Adminix requires an authenticated user that implements AdminixUserInterface. The middleware checks getAdminCriteria() from that interface.

use AlexKudrya\Adminix\AdminixUser; use AlexKudrya\Adminix\AdminixUserInterface; class User extends Authenticatable implements AdminixUserInterface { use AdminixUser; }

AdminixUser applies a boolean cast to the default is_admin attribute unless the application already defines its own cast. This keeps strict access checks consistent across SQLite, MySQL, and PostgreSQL hydration. Override getAdminCriteria() for a different server-owned access policy. Boolean criteria accept only real booleans or database-compatible integer/string 0 and 1; all other criterion types use strict comparison. When a panel selects an explicit guard, Adminix exposes the verified guard user through Auth::user() and $request->user() only while that request is handled, then restores the previous default guard for long-lived workers.

Do not enable no_auth_access in production. It is intended only for local demos and isolated development environments.

Logout is a state-changing action and is exposed only as a POST route with CSRF protection. Use the package sidebar form or submit a POST request to adminix_login_logout; do not link to logout with a GET URL. Successful login regenerates the Laravel session ID. Logout invalidates the session and regenerates the CSRF token.

Layered validation

Treat every browser payload as untrusted. HTML attributes and JavaScript validation improve the admin experience, but they are not security boundaries. They can prevent common mistakes, show faster feedback, and keep the form pleasant to use; the backend must still reject invalid, missing, oversized, malformed, unauthorized, or tampered data.

For each browser-facing input surface, keep the layers explicit:

  • render useful HTML constraints such as required, accept, min, max, step, and maxlength when the field supports them;

  • add JavaScript preflight when it improves interaction, for example upload previews, async selects, inline editors, imports, or modal forms;

  • enforce Laravel/backend validation from server-side Adminix module configuration before any write, handler call, queued job, upload storage, import execution, or external request.

This applies to resource fields, modal fields, list filters, inline edits, action fields, imports, media operations, tenant/page params, signed context, file formats, mime/extension allow-lists, and maximum file sizes. Do not trust browser-provided file names, mime types, extensions, hidden metadata, datasource names, writable field lists, relation parent IDs, or route/query params. Adminix should return controlled validation, authorization, or not-found responses instead of accepting unsafe input or leaking uncaught server errors.

Server-side resource configuration

Create and save routes use the server-side module configuration for:

  • dataSource;

  • primary key;

  • writable fields;

  • readonly fields;

  • fields hidden from the current visibility context;

  • static hidden field values;

  • signed param:* hidden field values rendered by package views.

Legacy Blade forms still render hidden metadata for compatibility with the current frontend, but controllers do not trust hidden adminix_data_source, _primary_key, or arbitrary writable field lists from the request. param:* hidden values are accepted only from signed form context generated during server-side rendering; unsigned or tampered request values are ignored and then handled by normal validation. Endpoints whose module contract depends on route params, tenant context, relations, or contextual visibility require that current signed form context for fetch, create, update, relation, and async-select operations. Omitting the token cannot turn a contextual module into an unscoped request; old open forms must be refreshed after an upgrade to receive the current context.

Save flows reject requests with no writable input and return a not-found error when an update affects no rows. Create flows reject requests with no writable input. Modal API requests with malformed params return controlled JSON errors. Requests with missing module metadata and non-scalar list filter values are ignored or rejected with controlled errors instead of producing uncaught server errors. Adminix endpoint resolution is server-side: page, module, list, field, action, datasource, primary key, relation context, and writable fields are resolved from package configuration or signed context before an endpoint writes or reads data. Signed contexts are bound to the current Adminix panel as well as the page/module contract, so a token rendered by one configured panel is rejected by another. Queued imports and bulk actions serialize the server-resolved panel key and restore that panel scope in the worker; browser payloads do not select it. On authenticated panels they also serialize only a server-derived guard name and scalar authentication identifier. At execution time the worker reloads the actor through that guard's current provider, requires both Laravel's Authenticatable contract and AdminixUserInterface, rechecks current admin criteria, and exposes the actor through Auth::user() and the synthetic request only for that job. Guard state and request user resolvers are cleared and restored in finally, including when execution throws, so sequential jobs cannot inherit another actor. Actor restoration happens before tenant providers, endpoint/module visibility, list scope, import module resolution, and queued bulk authorizeUsing() are evaluated. Missing, malformed, deleted, non-admin, or otherwise stale actor context fails closed without invoking the application handler. no_auth_access panels remain actorless by design and clear any previously resolved guard users before each queued callback; they must stay limited to local/demo use. Known stale context, invalid configuration, validation, and authorization rejections are controlled terminal results. Unexpected handler/container/runtime exceptions escape the queued job unchanged so Laravel can apply attempts and backoff; panel, tenant, request, and actor scopes are still restored in finally. Every new queued job also carries a server-generated execution UUID. Handlers can read it through BulkActionRequest::executionId() or ImportRequest::executionId() and use an application-owned unique receipt to make redelivery idempotent. The identifier is only a stable key; it cannot make unrelated database transactions or external requests exactly-once. Best-effort queued lifecycle events contain only execution/panel/module/action identity, attempt, status, actor type/ID, and aggregate issue count. They exclude selected IDs, action fields, query and signed contexts, CSV rows/mappings/filenames/temporary paths, handler messages, and credentials. Event construction and listeners cannot alter a handler result or Laravel retry behavior. Restart all long-lived queue workers during the upgrade. Actorless jobs dispatched by older Adminix versions fail closed on authenticated panels and must be redispatched from fresh authenticated bulk requests or fresh import previews after deployment. Web and API endpoints return controlled redirects or JSON errors for invalid module/context/validation states.

Password fields use only server-side ResourceField::password() configuration for create/update availability. Stored password values are never returned into resource or modal form data, optional blank password updates are ignored, and confirmed() validates against Laravel's field_confirmation key. Adminix persists the submitted value as-is; consuming apps must own hashing, strength rules, and credential lifecycle policies.

Upload fields use only server-side ResourceField::upload() configuration for disk, directory, validation rules, remove behavior, and old-file cleanup. The request may contain a file payload or a remove checkbox, but it cannot choose a disk, directory, stored filename, or writable upload column. On update, scalar values submitted under an upload field name are rejected; only real uploaded files or an allowed remove checkbox can change stored upload paths. Readonly upload fields are ignored like other readonly fields. If validation or database update fails, Adminix cleans up newly stored files; previous files are deleted only after a successful replace/remove update when deleteReplaced is enabled. Resource create/update/upload audit events are emitted only after successful package write flows and contain server-derived module metadata, writable fields, subject, and upload descriptors. Recorder implementations must still persist audit rows with application-side tenant/user ownership rules.

Media browser URLs are server-side module configuration. MediaBrowserModule::fromDisk() is appropriate for public filesystem disks; private media should use application routes, temporary signed URLs, or storage helpers that enforce authorization before streaming or redirecting. The browser cannot choose media disk, directory, item list, or URL strategy. Image transform callbacks return application-owned thumbnail/preview URLs only; they must not trust browser-provided variants, storage paths, tenant IDs, or private object keys. Media preview and copy URL actions are read-only browser helpers. Media replace/remove actions only render server-declared forms with CSRF and method spoofing; consuming application routes must authorize the admin, tenant, owner, file key, upload validation, storage write, and deletion policy.

Field visibility helpers such as hiddenOnCreate(), hiddenOnUpdate(), hiddenOnModal(), hiddenOnDetail(), and hiddenOnIndex() are server-side module configuration. For create/update/modal/list writes, hidden-by-context fields are excluded from writable field resolution and validation whitelists. They are not authorization rules; keep tenant, owner, and permission checks in server-side criteria, guards, policies, or handlers.

Module visibility helpers such as hidden(), visibleUsing(), authorizeUsing(), can(), and canAny() are server-side render and endpoint gates. Hidden modules are removed before data providers run, before nested dashboard/panel children render, and before Adminix endpoint resolution returns a named module. Use Laravel Gates/policies through can() for role or policy-aware module availability. This controls whether Adminix exposes the module surface; it does not replace datasource criteria, tenant scope, write policies, action handler authorization, imports, media routes, or queued job checks.

Resource and detail relation managers resolve the parent record from server-side module props before building child UI. The relation list receives a server-owned foreign-key criterion; browser filters can narrow that query but cannot select another parent record. Relation create/edit modals use signed relation context for the parent resource and inject the child foreign key server-side. Child edit fetch/save adds the parent foreign-key criterion, so a child record from another parent returns a controlled not-found response instead of being updated. Every relation fetch/create/save re-resolves the parent through the current module and tenant criteria, so a signed relation token cannot keep writing after the parent leaves the active scope.

Persistent notification storage scopes reads, inserts, history, and mark-read updates by the server-resolved panel key. Custom providers must use NotificationQueryDto::panelKey() for the same boundary, and out-of-request producers must pass an explicit panel key when they target a non-default panel.

List sorting accepts only asc and desc from the request. List lenses accept only configured server-side lens names from lens-{moduleName}. Lens default filters are applied only when the matching request filter key is absent; browser-provided filter keys are never silently replaced by lens defaults. List row details are read-only and opt-in per module through ListModule::rowDetails(). Detail values are prepared from server-declared ListRowDetails fields against the already scoped list records; the browser cannot choose datasource, criteria, field list, or primary key for the detail panel. Password row-detail fields do not return stored values. List CSV export is opt-in per module, uses the server-side list datasource and fields, requires signed page-param context, and ignores browser-provided datasource or column lists. Hidden list fields are excluded from CSV unless the field is explicitly marked exportable. List exports and import failed-row downloads neutralize spreadsheet formula prefixes before serialization; this is an output safety layer and does not replace authorization or field selection. When a tenant context is active, Adminix embeds the provider-approved tenant key and criteria hash in signed module endpoint contexts. Endpoint verification recalculates the current tenant context server-side and rejects stale, missing, or mismatched tenant payloads. Provider resolution errors fail closed with a controlled response; Adminix does not turn an unavailable tenant provider into an empty, unscoped criterion set. Adminix also injects active provider criteria into Adminix-owned list/resource scopes, including rendered lists, resource fetches, CSV exports, bulk actions, reorder, soft-delete, clone, modal fetches, and resource update lookups. Create flows derive server-owned default values only from simple equality tenant criteria, such as tenant_id = 9; complex criteria constrain queries but are not written into create payloads. This prevents the browser from substituting tenant context for package-owned scopes, but custom handlers, imports, media routes, external writes, guards, and policies must still enforce application tenancy. Import preview is opt-in through ImportModule and resolves the target module server-side before reading a CSV upload. The preview endpoint accepts only a file payload plus page/module identifiers, verifies the extension, server-detected MIME type, and text content, applies the configured size limit, parses rows server-side, runs configured preview row validation server-side, and stores preview data in the session. Stored preview state is bound to the signed panel, tenant, and route-parameter scope and cannot be downloaded or queued from another valid Adminix scope. Queued imports additionally store a server-owned temporary CSV copy and render execute controls only when queued() and handler() are configured. New queued imports transfer that file from preview ownership to queue ownership and carry the server-resolved panel plus validated tenant criteria snapshot into a sessionless worker. Queue ownership lasts through unexpected retryable failures: the CSV and .queued marker remain available for the next Laravel attempt. Success and controlled rejection clean them immediately, while the job's terminal failed() callback cleans them after attempts are exhausted. Failed-row CSV downloads verify signed import context and export only rows/messages from the current server-side preview validation report. The execute endpoint verifies a signed import context, requires the existing preview session, dispatches a queued job, and the job reparses the CSV and revalidates every row from the current server-side module config before calling the handler. Import routes do not trust browser-submitted datasource, writable fields, mapping, validation rules, failed rows, temporary paths, or persistence behavior. Import progress triggers only start same-page polling; progress state must come from the configured server-side progress handler. Queued import result downloads and reports are server-side handler metadata; signed URLs, storage, retention, and authorization belong to the consuming app. Application import handlers own idempotent database writes, failed-row storage, and audit rules. List row reorder is opt-in per module through ListModule::reorderable() or treeReorderable(), requires a signed rendered-row context, and accepts only a permutation of the visible primary keys. Tree reorder also signs the rendered parent map; submitted adminix_reorder_parent_ids must cover the same visible ID set, pass cycle validation, and use the parent field/root sentinel configured server-side. Tree reorder controls are not rendered for incomplete tree scopes where a visible row points to a parent outside the current rendered row set. Adminix resolves datasource, base criteria, relation scope, primary key, reorder field, tree parent field, and persistence strategy from server-side module configuration. The built-in SQL/Eloquent writer uses a transaction and a temporary sentinel value outside the current datasource range before assigning the existing order values to the new order, so permanent order values do not grow after repeated reorders and immediate unique constraints are not hit during normal swaps. MongoDB Level 1 keeps the same signed row-set and server-scope boundary for an explicit MongoDB Eloquent model. Built-in reorder remains SQL-only; for MongoDB, external stores, custom rank algorithms, or storage-specific locking/versioning, use reorderUsing() and keep authorization, tenant criteria, concurrency control, and persistence server-side. See MongoDB Level 1 compatibility for the complete supported/custom/unsupported matrix. List soft-delete row actions are opt-in per module through restoreAction(), forceDeleteAction(), or softDeleteActions(). Adminix requires an Eloquent SoftDeletes datasource, verifies a signed page-param context, re-resolves relation context and current list scope, and acts only on records that are still trashed(). Do not trust browser-provided datasource, primary-key names, deleted state, tenant ids, or param:* values for restore or permanent deletion flows. List clone actions are opt-in per module through cloneAction(), bulkCloneAction(), or cloneActions(). Adminix verifies signed context, reapplies relation context and current list scope, and derives clone data from server-side rendered index fields plus equality criteria values. Do not let the browser choose clone fields, datasource, primary-key names, writable fields, hidden tenant ids, or param:* values. Saved table views are opt-in per module through ListModule::savedTableViews() and require a signed table view context. The browser may submit a proposed preset name and state snapshot, but Adminix derives the panel key from the current panel and owner type/ID from the authenticated request user, resolves page/module/provider from server configuration, and normalizes allowed search, filter, lens, sort, column visibility, column order, column width, and density keys before calling the provider. The standard database provider scopes every saved-view read and write by panel, owner, page, and module. Custom providers must do the same with TableViewQueryDto::ownerKey(), panelKey(), pageName(), and moduleName(). Do not make table view panel, ownership, tenant context, provider names, datasource, fields, filters, lenses, or sortable fields browser-authoritative. Per-admin preferences use PreferenceProviderInterface or the optional adminix_preferences table. Theme values are restricted to system, light, or dark. Table preference writes require signed table context and normalize default lens, column visibility, column order, column width, column pins, and density against the configured ListModule. Do not make preference ownership, page/module names, lens names, columns, pins, widths, datasource, tenant context, or owner IDs browser-authoritative. List bulk actions are opt-in per module and resolve the action by name from server-side ListModule::bulkActions(). Bulk requests may submit only selected scalar IDs and an action name. Adminix verifies signed page-param context, verifies signed relation context for relation child lists, applies the same datasource, primary key, base criteria, active lens, filters, search, sorting, and relation parent scope as the rendered list, and ignores IDs outside that scope. Handlers receive a scoped selected-record query; they must not rebuild writes from browser-provided datasource, writable fields, parent IDs, or hidden metadata. Bulk action audit metadata uses the same scope-filtered selected IDs; do not replace it with raw browser-selected IDs in listeners or recorders. Action handlers should read only declared adminix_action_fields values from BulkActionRequest::field()/fields(). Queued bulk actions are authorized before dispatch and reapply the signed context, saved query filters/search/lens/sorting, and current server-side list scope when the queued job runs. They also restore the server-resolved panel and tenant criteria snapshot; queued request/session state cannot select another panel or tenant. Queued audit entries record bulk.queued on dispatch and bulk.executed after the queued handler finishes. Typed action responses such as redirects, modals, downloads, and new-tab targets must be derived from trusted server-side state, not browser-owned datasource, IDs, field names, or tenant context. Callback actions are opt-in page modules configured through CallbackActionModule. Adminix verifies signed page/module context, resolves the module from server-side PHP configuration, normalizes only declared adminix_action_fields, runs configured authorization, and then calls the server-side handler or webhook configuration. Do not let the browser choose callback handler classes, webhook URLs, headers, payload structure, tenant IDs, datasources, writable fields, or param:* values. Webhook callbacks are a convenience for server-configured outbound HTTP calls; application services should still own secrets, signing, retries, idempotency, and provider-specific audit rules.

Maintenance banners are read-only notices. Use them to explain read-only mode, deployment freezes, or degraded service, but enforce real write blocking in application policies, routes, jobs, commands, or datasource criteria. Treat format('html') banner content as trusted PHP configuration only.

Health status modules are also read-only. Configure check handlers, service names, URLs, credentials, and tenant scope in PHP or application services only. Do not let browser input choose which dependency to probe or which handler class to execute. Search input escapes SQL LIKE wildcards and is capped before query application. Global search is opt-in per configured resource; keep tenant/owner criteria server-side and expose only non-sensitive direct columns as searchable title/description fields. Invalid date and date-range filter values are kept in the rendered filter state but ignored by the database query instead of producing a server error. Row action confirmation text is JSON-encoded before it is placed in inline JavaScript. Class-string data sources must be Eloquent models; non-model classes are rejected with a controlled package error.

Run Adminix Doctor during setup and before release to catch common configuration and module safety issues early:

php artisan adminix:doctor --strict

Doctor reports risky no_auth_access, invalid admin user model configuration, duplicate page identifiers, malformed criteria, invalid data sources, global search misconfiguration, unsafe editable/reorder list shape, tenant-like datasources without server-side scope, field visibility conflicts, and unsupported row-action criteria before the affected admin route is used. When a SQL/Eloquent datasource has a tenant_id column, Doctor expects either module criteria() for that column or a valid tenant context provider/switcher that can inject provider-owned criteria. This is a configuration warning, not a replacement for application policies, guards, import handlers, media routes, queued jobs, and custom action authorization.

Criteria validation uses the same operator contract in runtime queries, resource update lookup scopes, and Doctor diagnostics. Supported query operators are =, ==, !=, <>, >, <, >=, <=, like, and not like; row action visibility criteria remain limited to == and != because they compare already-rendered row values.

Validation

Put validation rules on module fields or module-level validation(). Update flows ignore the current row for string unique rules and Rule::unique() objects using the server-derived primary key. Use field helpers such as required(), nullable(), min(), max(), step(), upload(), and explicit Laravel validation rules together: rendered hints and JavaScript checks should match the backend rules, but backend validation is the final authority.

Public exposure checklist

  • keep no_auth_access set to false;

  • implement AdminixUserInterface on the configured user model;

  • configure strict getAdminCriteria() rules;

  • configure only intended fields as editable or writable;

  • keep sensitive model fields out of Adminix modules unless they are intentionally managed;

  • disallow indexing Adminix URLs in robots.txt.

Last modified: 26 July 2026