Middleware
Custom middleware lives in backend/app/Http/Middleware/. Wiring is done in backend/bootstrap/app.php (Laravel 11+ style — no kernel class).
App-level (every API request)
The API group in bootstrap/app.php prepends:
| Order | Middleware | Purpose |
|---|---|---|
| 1 | RecordAuditContext | Resolves the App\Support\AuditContext singleton, captures actor + IP + UA + request id, makes them visible to the Auditable trait. |
Laravel's defaults still apply (SubstituteBindings, throttle on auth routes, JSON validation error handling).
Route-aliased
These are referenced in routes/api.php by alias.
tenant.resolve
App\Http\Middleware\ResolveTenantMembership — runs on every /tenant/{tenant}/... group. It:
- Resolves the URL
{tenant}(slug or numeric id) into aTenantmodel. - Should verify the authenticated user is a member of that tenant (
$user->tenants()->where('id', $tenant->id)->exists()). The current implementation skips this membership re-check — seeAUDIT.md§1 for the fix. - Sets the active tenant for the request and binds it into the container so policies can read it via
app(Tenant::class).
require.permission:<key>
App\Http\Middleware\RequirePermission — gates the route with one of the keys in App\Auth\PermissionRegistry. Returns 403 if the authenticated user lacks the permission for the active tenant. Multiple keys can be ANDed by chaining the middleware.
require.premium:<feature_slug>
App\Http\Middleware\RequirePremium — gates a route on the tenant's plan having the feature unlocked. Reads from tenants.premium_features JSON column. (Audit note: the middleware falls back to the user's current_tenant_id when the route lacks an explicit {tenant} — make this explicit to avoid leakage across tenants.)
auth:sanctum
Standard Laravel auth guard. Used on the owner portal, the caregiver-accept route, and /auth/me.
signed
Standard Laravel signed-URL middleware. Used on /owner/me/pets/{pet}/documents/{document}/download so the URL itself is the authorization (short-lived, generated by PetDocument::generateSignedUrl).
throttle:<max>,<minutes>
Standard rate-limit middleware. Wired today on:
/auth/forgot-password—throttle:6,1/auth/reset-password—throttle:10,1
(Audit recommends extending this to /auth/login, /auth/verify-reset-token, and /p/{slug}.)
Helpers
App\Support\AuditContext
Per-request singleton with:
actorUserId()/actorSystemAdminId()tenantId()(default tenant for cross-tenant safe paths)ip(),userAgent(),requestId()
The Auditable trait reads from this when writing audit_logs rows. Background jobs (e.g. notification dispatch) bind their own AuditContext so events get captured even when no HTTP request is in flight.
Container singletons in AppServiceProvider
AuditContext::class— request-scoped singleton.NotificationDriverManager::class— singleton so two reminders in one request share the resolved driver.
Removed: SQLite math UDF registration
A previous version of AppServiceProvider registered SQLite UDFs (SIN/COS/ASIN/SQRT/POWER/RADIANS) to support the haversine distance expression in ProviderDirectoryController. The geo filter has been refactored to a two-phase bounding-box pre-filter (SQL) + PHP-side haversine approach, which is portable across MySQL / PostgreSQL / SQLite without UDFs. The provider boot method no longer registers anything beyond the singletons above.