HTTP reference
Routing overview
Externa exposes a session-cookie HTTP surface for the Inertia SPA and same-origin JSON endpoints, plus a separate stateless Public CMS API under /api/v1 (Bearer API keys or anonymous public role).
Route files
Entry: routes/web.php (session/Inertia). API: routes/api.php → /api/v1/*. Domain modules: admin.php, settings.php, collections.php, ai.php. Console/schedule: routes/console.php. Health: /up via bootstrap/app.php.
Composition (routes/web.php)
/(home) — redirects todashboardif authenticated, otherwiselogin.- Authenticated shell (
auth+verified):GET /dashboard— also requirespermission:can-show-dashboard- Notification JSON helpers:
notifications,notifications/unread-count,notifications/read
requiresibling files:admin.php— users, groups, roles, permissions, activity logs, filessettings.php— account / profile / 2FA settings pagescollections.php— collections, fields, itemsai.php— AI webhook + authenticated AI UI/API
Fortify registers login/register/password/2FA routes separately (prefix empty, web middleware).
Middleware layers
Configured in bootstrap/app.php:
Routing registration
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
apiPrefix: 'api',
)
web— Inertia SPA, Fortify, admin/collections/AI session routes (cookies, CSRF).api— Public CMS API (/api/v1/...), no session;ResolveApiAccess+ collection permission guard. See Public CMS API.
Aliases
| Alias | Class | Use |
|---|---|---|
permission | EnsureUserHasPermission | permission:{PermissionEnum value} |
can.manage.files | EnsureCanManageFiles | File-manager route → file permission mapping |
Appended to web
| Middleware | Role |
|---|---|
HandleAppearance | Appearance cookie / theme |
HandleInertiaRequests | Shared Inertia props |
AddLinkHeadersForPreloadedAssets | Asset preload link headers |
Cookie / CSRF exceptions
- Cookies not encrypted:
appearance,sidebar_state,locale - CSRF except:
ai/webhooks/collection-import,api/graphql, and baregraphql(no/graphqlroute is registered — clients use/api/graphql)
Typical stacks by area
| Area | Middleware |
|---|---|
| Dashboard / notifications | auth, verified (+ permission on dashboard) |
| Admin resources | auth, verified, permission:… per action |
| Files | auth, verified, can.manage.files |
| Collections | auth, verified, permission:can-*-collections (bulk via FormRequest — see Collections API) |
| Project / appearance settings | auth, verified, permission:can-manage-project-settings |
| AI authenticated | auth, verified, permission:can-use-ai (+ extra on some routes) |
| AI webhook | throttle:30,1 + bearer / header token check (no session) |
| Public CMS API / GraphQL | throttle:api + ResolveApiAccess (no session/CSRF) |
Client surfaces
Clients are expected to be:
- The Inertia React frontend (same origin, session cookie, CSRF).
- Server-side jobs / tools using the same auth context.
- The collection import webhook with a shared secret (not a user session).
- External sites / apps via the Public CMS API (
/api/v1, anonymouspublicrole and/or Bearerek_…API keys).
Treat JSON from /files/*, /ai/*, and session /collections/* as first-party SPA APIs. Use /api/v1 for headless integrations.
Inertia shared props
HandleInertiaRequests::share() merges:
| Prop | Source / meaning |
|---|---|
name | config('app.name') |
auth.user | Current user model or null |
auth.permissions | Effective permission name list from EffectivePermissionResolver::permissionsFor() |
auth.roleNames | Effective role names (direct + via groups) |
auth.isSuperAdmin | Whether effective roles include super-admin |
sidebarOpen | From sidebar_state cookie (true when missing) |
collectionLocales | Project settings content_locales (seeded from config/collections.php) |
collectionLocaleMeta | { code, name, flag }[] for enabled content locales |
defaultContentLocale | Project settings default content locale |
notifications.unread_count | Count of unread database notifications for the user |
Frontend permission checks use useCan() against auth.permissions / auth.isSuperAdmin (see Effective permissions).
Wayfinder
Named Laravel routes and controller actions are exposed to TypeScript via Laravel Wayfinder (@laravel/vite-plugin-wayfinder). Prefer generated helpers under resources/js/routes / actions instead of hard-coding path strings in React.
Deep-link route ordering
Static path segments are registered before parameterized catch-alls so reserved names are not captured as IDs:
- Files:
/files/list,/files/upload, … beforeGET /files/{folder?} - AI:
/ai/status,/ai/conversations, … beforeGET /ai/{conversation}(UUID)
Admin UI deep-links (drawers / panels)
List screens sync selected drawers into the query string (Inertia replace + preserveState) so a URL can reopen the same surface:
| Surface | Query | Behavior |
|---|---|---|
| Collections index | ?edit={id} / ?new=1 | Opens create/edit collection drawer |
| Users / Groups | ?edit={id} / ?new=1 | Opens user or group drawer |
| Collection fields | ?field={id} / ?newField=1 | Opens edit field drawer or add-field type picker |
| Files | ?file={id} | Opens the file detail panel (fetches by id if not on the current page) |
Closing the drawer/panel removes the param without dropping other filters (search, trash, tags, …). Nested field-type multi-step beyond newField is out of scope for this sync.
Item full-page edit uses a separate return query to restore list filters — not a drawer deep-link.
Next
| Surface | Docs |
|---|---|
| Admin + files JSON | Admin API |
| Collections (session) | Collections API |
Public CMS API (/api/v1) | Public CMS API |
| GraphQL | GraphQL |
| AI | AI API |
| Project settings | Project settings |