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)

  1. / (home) — redirects to dashboard if authenticated, otherwise login.
  2. Authenticated shell (auth + verified):
    • GET /dashboard — also requires permission:can-show-dashboard
    • Notification JSON helpers: notifications, notifications/unread-count, notifications/read
  3. require sibling files:
    • admin.php — users, groups, roles, permissions, activity logs, files
    • settings.php — account / profile / 2FA settings pages
    • collections.php — collections, fields, items
    • ai.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

AliasClassUse
permissionEnsureUserHasPermissionpermission:{PermissionEnum value}
can.manage.filesEnsureCanManageFilesFile-manager route → file permission mapping

Appended to web

MiddlewareRole
HandleAppearanceAppearance cookie / theme
HandleInertiaRequestsShared Inertia props
AddLinkHeadersForPreloadedAssetsAsset preload link headers
  • Cookies not encrypted: appearance, sidebar_state, locale
  • CSRF except: ai/webhooks/collection-import, api/graphql, and bare graphql (no /graphql route is registered — clients use /api/graphql)

Typical stacks by area

AreaMiddleware
Dashboard / notificationsauth, verified (+ permission on dashboard)
Admin resourcesauth, verified, permission:… per action
Filesauth, verified, can.manage.files
Collectionsauth, verified, permission:can-*-collections (bulk via FormRequest — see Collections API)
Project / appearance settingsauth, verified, permission:can-manage-project-settings
AI authenticatedauth, verified, permission:can-use-ai (+ extra on some routes)
AI webhookthrottle:30,1 + bearer / header token check (no session)
Public CMS API / GraphQLthrottle:api + ResolveApiAccess (no session/CSRF)

Client surfaces

Clients are expected to be:

  1. The Inertia React frontend (same origin, session cookie, CSRF).
  2. Server-side jobs / tools using the same auth context.
  3. The collection import webhook with a shared secret (not a user session).
  4. External sites / apps via the Public CMS API (/api/v1, anonymous public role and/or Bearer ek_… 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:

PropSource / meaning
nameconfig('app.name')
auth.userCurrent user model or null
auth.permissionsEffective permission name list from EffectivePermissionResolver::permissionsFor()
auth.roleNamesEffective role names (direct + via groups)
auth.isSuperAdminWhether effective roles include super-admin
sidebarOpenFrom sidebar_state cookie (true when missing)
collectionLocalesProject settings content_locales (seeded from config/collections.php)
collectionLocaleMeta{ code, name, flag }[] for enabled content locales
defaultContentLocaleProject settings default content locale
notifications.unread_countCount 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.

Static path segments are registered before parameterized catch-alls so reserved names are not captured as IDs:

  • Files: /files/list, /files/upload, … before GET /files/{folder?}
  • AI: /ai/status, /ai/conversations, … before GET /ai/{conversation} (UUID)

List screens sync selected drawers into the query string (Inertia replace + preserveState) so a URL can reopen the same surface:

SurfaceQueryBehavior
Collections index?edit={id} / ?new=1Opens create/edit collection drawer
Users / Groups?edit={id} / ?new=1Opens user or group drawer
Collection fields?field={id} / ?newField=1Opens 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

SurfaceDocs
Admin + files JSONAdmin API
Collections (session)Collections API
Public CMS API (/api/v1)Public CMS API
GraphQLGraphQL
AIAI API
Project settingsProject settings
Previous
AI configuration