HTTP middleware
Look up the App route stack, registered aliases, parameters, request effects, refusal codes, and host-only boundaries.
HTTP middleware
Keep the generated outer route stack and authorize each operation against its actual target. The minimal example shows the order; the alias table defines arguments and refusal responses. Organization context supplies coordinates, while the permission guard decides whether the actor may perform the action.
Signature
An App never imports App\Http\Middleware. The SDK publishes the stable names
that cross the package boundary in packages/app-sdk/packages/laravel/src/Http/Middleware.php:
final class Middleware
{
public const AGENT_DELEGATION = 'agent.delegation';
public const APP_INSTALLED = 'app.installed';
public const AUTHENTICATED_LOCALE = 'nexia.locale';
public const LEGAL_ENTITY_CONTEXT = 'nexia.context.legal_entity';
public const WORKSPACE_CONTEXT = 'nexia.context.workspace';
}Parameterized authorization and structural guards remain string signatures registered by the host. Use only the signatures in this page; the existence of a Core middleware class does not make that class an App dependency.
Minimal example
This is the complete outer stack emitted from
stubs/package-app/empty-routes.stub, with a concrete App key. It identifies the
tenant, scopes the session, authenticates either a human or delegated Agent,
narrows delegated authority, checks App availability, then hydrates view and
organization context.
use Illuminate\Support\Facades\Route;
use Nexia\Http\Middleware;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Middleware\ScopeSessions;
Route::middleware([
'web',
InitializeTenancyByDomainOrSubdomain::class,
PreventAccessFromUnwantedDomains::class,
ScopeSessions::class,
'auth:sanctum,agent',
'agent.delegation',
'app.installed:workshop',
Middleware::LEGAL_ENTITY_CONTEXT,
Middleware::WORKSPACE_CONTEXT,
])->group(function (): void {
// Register Workshop routes here.
});Authorize each concrete operation inside the group. Tenant-owned generated routes use can.tenant_wide; canonical Legal Entity routes authorize in the controller using explicit request targets or the stored record owner. Use can.scope when the URL carries the target. The outer stack establishes identity and context only.
Parameters
Generated outer stack
Order is part of the contract. Authentication cannot safely run before tenant identification, and a delegated request must be narrowed before a capability guard or controller executes.
| Entry | Parameters | Required | Runtime effect |
|---|---|---|---|
web | none | yes | Starts the browser/session stack. Core also appends live support-session enforcement and replaces the default CSRF middleware with its bearer-aware subclass. |
InitializeTenancyByDomainOrSubdomain::class | none | yes | Resolves the tenant from the request host and enters its database, cache, filesystem, and URL context. |
PreventAccessFromUnwantedDomains::class | none | yes | Refuses a domain that is not valid for the resolved tenant. |
ScopeSessions::class | none | yes | Prevents a tenant session from being reused under a different tenant identity. |
auth:sanctum,agent | guard list | yes | Accepts a Sanctum-authenticated human session or a signed Agent delegation token. Authentication alone grants nothing. |
Middleware::AGENT_DELEGATION | none | yes for Agent-callable routes | For delegated requests, intersects the token allowlist, signed Legal Entity/Operating Unit target, and live tool policy. It is a no-op for a human request. |
Middleware::APP_INSTALLED.':{app_key}' | app_key | yes | Requires the App and its transitive prerequisites to be operational in the tenant. |
Middleware::LEGAL_ENTITY_CONTEXT | fixed route name legalEntity | yes in the generated stack | Uses an explicit route or delegation target; otherwise restores an active session/default preference. It never grants authority. |
Middleware::WORKSPACE_CONTEXT | none | yes in the generated stack | Restores or clears an accessible Workspace preference. Workspace is view state, not a permission scope. |
Registered Nexia aliases
bootstrap/app.php registers fourteen custom aliases. Ten are used by App
routes; four belong to platform-only entry points.
| Alias signature | Parameters and defaults | Intended owner | Behavior |
|---|---|---|---|
agent.delegation | none | App and Core API groups | Narrows delegated Agent authority; human Sanctum requests pass unchanged. |
app.installed:{app_key} | required App key | App routes | Returns 404 unless the complete App prerequisite closure is operational. |
nexia.locale | none | Localized App routes | Resolves the authenticated user's effective locale for the request. Put it after authentication; it chooses presentation language and grants no authority. |
nexia.context.legal_entity | fixed legalEntity route parameter | App and Core routes | Writes the resolved internal id to request attribute legal_entity_id; selection and membership remain non-authority context. |
nexia.context.workspace | no middleware parameter | App and Core browser routes | Maintains session workspace_id only when the user can access that Workspace. |
can.tenant_wide:{permission} | required permission slug | App and Core operations | Evaluates the permission against an empty tenant-wide data target. A narrower Legal Entity or Operating Unit Grant cannot satisfy it. |
can.scope:{permission},{scope_type},{route_parameter}[,{subject_population}] | permission, dimension key, route parameter, optional SDK population value such as self | App and Core operations | Resolves the explicit public resource target, evaluates the Access Grant, and records Legal Entity/Operating Unit request attributes when applicable. |
operating_unit.affiliated[:{legalEntityParameter},{operatingUnitParameter}] | defaults legalEntity, operatingUnit | Nested App and Core routes | Proves that the two resolved route resources are affiliated. This structural check creates no Grant. |
nexia.context.operating_unit | fixed operatingUnit route parameter | App and Core routes with an explicit Operating Unit | Writes request attribute operating_unit_id; a delegated request must match its signed target. App routes currently use this host-registered string because the SDK has no Middleware constant for it. |
human.only | none | Human self-service App and Core routes | Rejects a delegated Agent marker while allowing the authenticated human path to continue. |
service-token | X-Service-Token header; no middleware parameter | Core /internal/* only | Authenticates the Agent gateway with the configured service secret. It is not an App route contract. |
agent_data:{action} | default read; * reads the action from the request | Core generic Agent-data endpoints only | Resolves a runtime Resource key, then enforces delegation allowlist and live permission. Apps contribute Resources but do not attach this alias. |
signature.enabled | none | Core Signature entry points only | Refuses the platform Signature surface when the tenant feature is disabled. It is not an App route contract. |
tenant.require_2fa | none | Tenant Shell routes only | Allows the document Shell to load for enrollment; JSON requests that still require enrollment receive 423. |
Options: choosing guards and order
Choose a capability guard from the operation's real data owner, not from the screen that happens to call it.
| Situation | Add after the outer stack | Why |
|---|---|---|
| Tenant-owned catalog or setting | can.tenant_wide:quality.inspection_method.read | There is no narrower resource coordinate; only a tenant-wide Grant is applicable. |
| Legal Entity target in the URL | can.scope:quality.inspection_result.read,core.legal_entity,legalEntity | The URL supplies the exact Legal Entity target used for the Access Grant decision. |
| Operating Unit-owned record | operating_unit.affiliated followed by can.scope:{permission},core.operating_unit,operatingUnit | Affiliation proves route structure; the scoped guard separately proves authority and also requires the Legal Entity coordinate. |
| Self-service data that an Agent must never call | human.only before the capability guard | Human-only is caller-kind narrowing, not a substitute for the permission. |
| Permission limited to one population | Fourth can.scope parameter, for example self | The decision must allow that exact population in addition to the action and data scope. |
Keep app.installed outside individual CRUD routes so every App endpoint fails
closed consistently. Put can.* at the smallest concrete operation. A Policy
still checks the loaded record, a list predicate excludes out-of-scope rows in
SQL, and the domain service enforces transition rules. Middleware does not
replace any of those layers.
Output or return
A passing middleware returns the next response. Context middleware may add request-local data for downstream contracts; none of these values is evidence of authority by itself.
| Effect | Consumer | Security meaning |
|---|---|---|
| Active tenant and tenant database connection | Models, cache, filesystem, URL generation | Isolation boundary established before App code runs. |
| Authenticated human or delegated Agent | Controllers and authorization services | Actor identity only. |
| Validated browser password session | Core API root routes that attach the session check | A password-changed session is rejected; stateless delegated requests pass this host check unchanged. |
legal_entity_id request attribute | Authorization context and downstream services | Explicit or selected context; not a Grant. |
operating_unit_id request attribute | Operating Unit-aware authorization and services | Explicit route/delegation context; not affiliation or authority. |
Session workspace_id | Shell and browser view restoration | Cosmetic/view context only. |
Authorization decision from can.* | Current operation | Coarse capability decision; record and state checks still run later. |
The generated App stack follows this sequence (authorization may run in a route guard or the controller):
tenant identification
-> tenant-scoped session
-> human or Agent authentication
-> delegated-authority narrowing
-> App operational check
-> request context hydration
-> route capability guard
-> Policy and list predicate
-> domain transition
The Core routes/api.php root group explicitly adds authorization-denial auditing, throttle:api, and the password-session check. Package routes load through their own provider: an /api URL does not inherit this root group. Attach any required rate limit explicitly. The
shared api limiter allows 300 requests
per minute for normal traffic, keyed by tenant plus user id or IP. Delegated
Agent traffic uses a separate 240-per-minute bucket keyed by tenant plus Agent
id, so it does not consume the user's browser bucket. Endpoint-specific limits
still apply on top. The API root also runs Laravel's password-hash session check
against the stateful web guard; stateless delegated traffic passes through.
The web and api groups re-check a redeemed support session on every request.
Those automatic host concerns are not aliases an App copies into its package
route file.
Errors
| Observable result | Cause | Resolution |
|---|---|---|
401 | Authentication failed, a browser session was invalidated by a password change, or a platform service-token was absent/invalid | Reauthenticate with the supported human/Agent credential; never use the internal service token from an App. |
403 from agent.delegation | Permission slug, signed organization target, or tool policy falls outside the delegation | Mint a correctly narrowed token and keep the route's can.* declaration accurate. |
403 from can.tenant_wide or can.scope | No applicable Access Grant, target dimension, or requested population | Fix the Grant or the route's real data coordinate; do not fall back to a Shell selection. |
403 from human.only | A delegated Agent called a human self-service endpoint | Use an Agent-capable operation with an explicit permission, or keep the endpoint human-only. |
404 from app.installed | App absent, disabled, uninitialized, unknown, or blocked by a prerequisite | Install/repair the App closure; do not bypass the guard. |
404 from operating_unit.affiliated | Route Legal Entity and Operating Unit are not affiliated | Correct both stable public route identifiers. |
419 | A redeemed support session expired or was centrally revoked | End the impersonated session and start a new authorized support session. |
423 JSON response | Tenant policy requires 2FA enrollment for the current password session | Complete enrollment; SSO policy remains owned by the external IdP. |
429 | The shared API bucket or a tighter endpoint-specific limiter is exhausted | Honor the response rate-limit headers and back off; do not bypass the host limiter. |
503 from service-token | Platform service-token configuration is empty | Configure both sides of the internal gateway; this is an operator issue, not an App fallback. |
Real usage
packages/quality/routes/routes.php retains the generated outer group with tenant initialization, authentication, App installation, Legal Entity context, and Workspace context. Add an operation-specific permission guard when adding a Resource route.
packages/people/routes/routes.php adds nexia.context.operating_unit and
operating_unit.affiliated where the URL carries both organization
coordinates. packages/payroll/routes/routes.php combines human.only with a
can.scope guard whose fourth parameter is self for personal payroll lines.
Inspect the registered chain without changing state:
task artisan -- route:list --path=quality
task artisan -- route:list --path=legal-entities --jsonFocused source-backed behavior is covered by
tests/Feature/AppRuntime/TenantAppInstallationRuntimeTest.php,
tests/Feature/Authorization/CanTenantWideMiddlewareTest.php,
tests/Feature/Access/RouteOperatingUnitContextTest.php,
tests/Feature/Agent/AgentDelegationGuardTest.php, and
tests/Feature/WorkspaceContextTest.php.
Related
- SDK contracts — the PHP boundary that publishes middleware names without exposing Core classes
- Permissions and roles — how capability, Grant scope, and subject population form the decision behind
can.* - Add a protected API — apply the guards, Policy, list predicate, and denial tests to one App operation
- Tenant and context — distinguish tenant, actor, organization, and Workspace context before choosing a guard
- Agent Resource Data Runtime — see how generic Agent writes return through the same authorized App routes