Frontend
Server-rendered BXM views, small Alpine.js components, and a Vite-compiled SCSS/JS pipeline.
On this page
- How it fits together
- Alpine.js architecture
- SCSS structure
- Vite configuration
- Server-rendered view components
- Application shell
- Authentication partials
- UI partials
- Alpine components and stores
- Application shell and authentication
- Admin and profile forms
- UI components and global APIs
- Stores, utilities, and magic properties
- CSRF on mutating requests
- Avatars & branding logo
Frontend
How it fits together
The frontend is a hybrid server-rendered + Alpine.js application - no SPA, no client-side router:
Admin.bxm, AuthSplit.bxm, and friends in app/layouts/ render the HTML frame.
Views in app/views/ render with rc/prc data already resolved by the handler.
Small x-data components handle forms, modals, drawers, and toggles - no build step needed per-component.
SCSS + JS from resources/assets/ compile into public/includes/, served at the ASSET_URL prefix.
Alpine.js architecture
App.js (entry)
├── Registers all Alpine stores + components
├── Imports Bootstrap JS + Phosphor icons + Tippy.js
│
├── Stores ($store.*)
│ ├── theme.js → dark/light mode, syncs data-bs-theme + localStorage
│ └── sidebar.js → collapse/open, mobile overlay, localStorage persistence
│
└── Components (x-data)
├── auth/ → AuthForm, RegisterForm, ForgotPasswordForm, PasswordResetForm
├── security/ → AuditLogForm, PermissionsForm, RolesForm, UserDetailForm, UsersForm
├── profile/ → PasskeyOnboarding, PreferencesForm, ProfileForm
├── settings/ → SettingsForm, SettingsRegistryForm
└── ui/ → Drawer, GlobalProgress, GlobalToast, Logo, MessageBox, PasswordMeter, PasswordStrength, Switch
Each component is a standalone module returning an Alpine x-data object:
export default () => ( {
visible: true,
init() {
setTimeout( () => this.visible = false, 5000 );
}
} );
<div x-data="messageBox" x-show="visible" x-transition>
<!-- alert content -->
</div>
SCSS structure
app.scss
├── _variables.scss Bootstrap variable overrides
├── bootstrap Full Bootstrap 5.3 import
├── _base.scss CSS custom properties (light/dark theme)
├── components/ 9 component partials
├── layouts/ Admin + Auth layout partials
└── views/ Page-specific styles
Vite configuration
vite.config.mjs uses coldbox-vite-plugin's coldbox() plugin:
- Entry points:
resources/assets/scss/app.scssandresources/assets/js/App.js refresh: appRefreshPaths— auto full-reload on handler/view changespublicDirectory: "public/includes"— where built assets land- SCSS preprocessor with
silenceDeprecationsflags for newer Dart Sass (import, global-builtin, color-functions, if-function)
npm run dev # Vite dev server with HMR
npm run build # Production build → public/includes/
npm run lint # ESLint check on resources/assets/js
npm run lint:fix # ESLint auto-fix
npm run lint:scss # Stylelint on resources/assets/scss
In production, compiled asset URLs are prefixed with the ASSET_URL environment variable (.env.example defaults it to /includes) - see Configuration.
Server-rendered view components
These BXM partials live under app/views/_components/ and are rendered with ColdBox's view() helper. They are intentionally presentation-focused: pass values through the args struct and keep business logic in handlers or services.
Application shell
| Partial | Purpose and inputs |
|---|---|
_components/app/includes | Document metadata, theme/sidebar FOUC prevention, passkey script, and Vite CSS/JS. Optional title. Include once in <head>. |
_components/app/sidebar | Admin navigation, permission-aware Users/Roles/Permissions/Audit Log links, settings submenu, and sidebar footer. Reads prc.authUser; include from Admin.bxm. |
_components/app/sidebar-brand | Application logo/name link used by the sidebar. |
_components/app/sidebar-footer | Authenticated user summary and profile/sign-out actions used by the sidebar. |
_components/app/topbar | Sidebar toggle, theme toggle, breadcrumbs, user menu, and sign-out action. Reads prc.authUser and prc.title. |
_components/app/topbar-breadcrumbs | Dashboard breadcrumb rendered inside the topbar. Extend when adding deeper navigation. |
_components/app/topbar-notifications | Topbar notification slot/component for application notifications. |
_components/app/footer | Copyright and footer links. Optional classes. Reads prc.settings.cbCopyrightNotice. |
Authentication partials
| Partial | Purpose and inputs |
|---|---|
_components/auth/footer | Footer used by authentication layouts. |
_components/auth/passwordInput | Reusable password field with visibility toggle and password-strength affordances. |
UI partials
| Partial | Purpose and inputs |
|---|---|
_components/ui/modal | Generic Alpine dialog that renders an optional nested view. Required id should be unique; supports title, openExpression, closeExpression, contentView, and contentArgs. |
_components/ui/drawer | Right-side focus-trapped dialog with backdrop/Escape closing and optional contentView/contentArgs; also initializes drawer(). |
_components/ui/confirm | Confirmation dialog with static or Alpine-bound message, confirm/cancel expressions, labels, icon, button class, and disabled expression. |
_components/ui/messagebox | Dismissible info/success/warning/error alert. Supports static message/title or dynamic messageExpression/typeExpression/dismissAction, plus autoDismiss and classes. |
_components/ui/globalProgress | Global accessible progress bar. Include once per layout; controlled by $progress.start()`, `$progress.set(), and $progress.stop(). |
_components/ui/globalToast | Global toast stack. Include once per layout; accepts duration, position, and maxVisible, and receives notifications from $toast(). |
_components/ui/avatar | Renders a user's avatar image when hasAvatar is true, falling back to initials otherwise. Read-only display used by the sidebar, topbar, Users listing, and Users detail page — see Avatars & branding logo. |
_components/ui/logo | Reusable application logo/branding partial. |
_components/ui/passwordMeter | Password policy meter used beside password fields. |
_components/ui/progressbar | Inline progress bar partial for a local numeric value. |
_components/ui/switch | Accessible switch control partial for boolean settings. |
Alpine components and stores
resources/assets/js/App.js registers the following names globally with Alpine. Use them as x-data="name" or x-data="name(...)" in BXM views. Form components make remote requests to the matching handler routes and expect the CSRF token supplied by their view, sent through fetchWithCsrf() (see CSRF on mutating requests).
Application shell and authentication
| Alpine name | Source | Responsibility |
|---|---|---|
adminBody | components/app/AdminBody.js | Admin page shell behavior and global layout events. |
sidebarBrand | components/app/SidebarBrand.js | Sidebar brand interactions. |
footer | components/app/Footer.js | Footer state and current-year behavior. |
authForm | components/auth/AuthForm.js | Login submission, validation, remember-me, and errors. |
registerForm | components/auth/RegisterForm.js | Registration validation, email availability, and submission. |
forgotPasswordForm | components/auth/ForgotPasswordForm.js | Forgot-password request state and feedback. |
passwordResetForm | components/auth/PasswordResetForm.js | Password reset token submission and validation. |
Admin and profile forms
| Alpine name | Source | Responsibility |
|---|---|---|
usersForm | components/security/UsersForm.js | User listing, search, pagination, invitation, status, and admin actions. |
userDetailForm | components/security/UserDetailForm.js | User profile, role, permission, preference, token, and verification actions. |
rolesForm | components/security/RolesForm.js | Role CRUD and assigning/removing users and permissions. |
permissionsForm | components/security/PermissionsForm.js | Permission listing and CRUD operations. |
auditLogForm | components/security/AuditLogForm.js | Audit filtering, pagination, detail drawer, CSV export, purge, and clear actions. |
settingsForm | components/settings/SettingsForm.js | Core application settings editing and cache-related feedback. |
logoUploader | components/settings/LogoUploader.js | Branding logo upload/remove for the "App Logo Path" field, alongside its existing manual URL input and live preview — see Avatars & branding logo. |
settingsRegistryForm | components/settings/SettingsRegistryForm.js | Registry search, pagination, create/update, enable/disable, and delete actions. |
profileForm | components/profile/ProfileForm.js | Profile fields, password policy, API token management, the email-change request/cancel sub-form, and avatar upload/remove. |
preferencesForm | components/profile/PreferencesForm.js | Persisting user preferences. |
passkeyOnboarding | components/profile/PasskeyOnboarding.js | Passkey registration and required-passkey onboarding. |
UI components and global APIs
| Alpine name | Source | Responsibility |
|---|---|---|
messageBox | components/ui/MessageBox.js | Alert visibility and optional timed dismissal. |
passwordMeter | components/ui/PasswordMeter.js | Password requirement and strength display. |
passwordStrength | components/ui/PasswordStrength.js | Password strength calculation and labels. |
switchComponent | components/ui/Switch.js | Toggle state and change handling. |
drawer | components/ui/Drawer.js | Drawer lifecycle and focus behavior. |
globalProgress | components/ui/GlobalProgress.js | Progress events and current progress value. |
globalToast | components/ui/GlobalToast.js | Toast queue, dismissal, type mapping, and stack limits. |
The source also contains Header.js, Sidebar.js, TopBarNotifications.js, and Logo.js. Their exports are available for local imports, but they are not currently registered by App.js; register them with Alpine.data() before using them as global x-data components.
Stores, utilities, and magic properties
| API | Source | Usage |
|---|---|---|
$store.theme | stores/theme.js | Light/dark mode, data-bs-theme, and localStorage persistence. |
$store.sidebar | stores/sidebar.js | Desktop collapse, mobile open/close, and localStorage persistence. |
$formatDate`, `$formatDateTime, $relativeDate | utils/dateFormat.js | Consistent date display with fallbacks. |
$countLabel | utils/countLabel.js | Singular/plural count labels. |
$sortClass`, `$sortIcon | utils/sort.js | Sortable table headers and indicators. |
$passwordMeetsPolicy | utils/passwordPolicy.js | Checks the configured password requirements. |
$isEmail | App.js | Lightweight email-format check. |
$toast` / `$progress | components/ui/GlobalToast.js, GlobalProgress.js | Global notification and progress APIs. |
$focus` / `$copy | App.js | Focus a descendant after Alpine updates; copy text through the browser clipboard API. |
createRemoteListing() | utils/listing.js | Shared remote listing state, loading, pagination, and error handling. |
fetchWithCsrf(), refreshCsrfToken() | utils/csrf.js | Sends a mutating request with the component's CSRF token, recovering once from a stale one. |
AlpinePlugins.js installs Collapse, Focus, Mask, and Persist. passkeys.js provides the browser-side WebAuthn integration. Keep new reusable browser APIs documented here and add their registration/import to App.js when they are global.
CSRF on mutating requests
Every component action that sends a non-GET request goes through fetchWithCsrf() (utils/csrf.js) instead of calling fetch() directly. This is the one encapsulated place mutating requests are built, so the token-recovery behavior - and anything added to it later (request/response hooks, global headers, telemetry) - only has to change here rather than in every component that happens to mutate state.
Why it has to recover at all. A component's csrfToken is embedded once, when its view renders. The server can invalidate it while the page is still open, in two ways cbcsrf's own docs call out: csrfField() (the mixin behind every hidden csrf input) force-rotates the session's token on its first use per request, so any page that renders it - Settings, the passkey-required page, the auth pages - silently invalidates the token sitting in every other open tab; and a token expires a fixed time after it was created, not after the page loaded, so a page rendered late in a token's life can be served one with only seconds left. Either way, a component's embedded token can go stale before the user finishes typing.
The contract:
export async function fetchWithCsrf( component, url, method, buildRequest ) { /* ... */ }
export async function refreshCsrfToken( component ) { /* ... */ }
componentis the Alpine component instance (passthis). It must expose a mutablecsrfTokenproperty -fetchWithCsrf()reads it to build the request and, on a stale-token retry, overwrites it with the session's current token viarefreshCsrfToken().buildRequest( csrfToken )returns the method-specificRequestInitfields (headers,body,credentials, etc.) for the given token. It is called again on retry, so it must build the body fresh each time rather than closing over a value computed once - this is what lets the same helper coverURLSearchParams,JSON.stringify(), andFormDatabodies alike.- On a 403,
fetchWithCsrf()callsrefreshCsrfToken()and, if it obtained a genuinely new token, replays the request once withbuildRequest()called again. A second 403 (e.g. a real authorization failure, or a session that has expired entirely) is returned as-is - callers still need their normal error handling for that case.
const response = await fetchWithCsrf( this, "/permissions", "POST", ( csrf ) => ( {
headers : { "Content-Type": "application/x-www-form-urlencoded" },
body : new URLSearchParams( { permission: this.form.permission, csrf } ),
} ) );
const response = await fetchWithCsrf( this, form.action, "POST", ( csrf ) => {
const formData = new FormData( form );
formData.set( "csrf", csrf );
return { body: formData, credentials: "same-origin", headers: { Accept: "application/json" } };
} );
Only GET/HEAD reads skip fetchWithCsrf() and call fetch() directly - they carry no CSRF token and cannot 403 for one. A handful of cbSecurity module endpoints (the WebAuthn passkey ceremony routes) are also called with plain fetch(): they authenticate through the WebAuthn ceremony itself, not this app's CSRF token, so they are out of scope for this helper. Every other mutation in resources/assets/js/components/ goes through fetchWithCsrf(); keep new form components consistent with that when they add a request that changes server state.
Avatars & branding logo
User avatars and the application branding logo are stored on the private cbfs assets disk (see Configuration) and streamed out by Assets.bx (see Handlers & Routing) rather than served as static files.

- Display goes through the
_components/ui/avatarpartial: it renders<img src="/avatars/:userId/:size">whenhasAvataris true, and falls back to an initials<span>otherwise. It is wired into the sidebar, topbar, and Users listing table (server-projectedhasAvatarfield), and inline in the Users detail page (x-show/x-cloaktoggling onuser.hasAvatar, since that page's avatar sits inside an Alpine-driven summary card rather than a static partial). - Upload/remove for the current user's own avatar lives on the Profile page, owned by
profileForm(ProfileForm.js): a hidden file input reads the selected image as a base64 data URI (readFileAsDataUrl()) and posts it toPOST /profile/avatar;DELETE /profile/avatarremoves it. Both bump aversioncounter used as a cache-busting query param on the streamed URL, since the file path itself does not change between uploads. - The branding logo gets the same upload/remove treatment on the Settings page, via the
logoUploadercomponent (LogoUploader.js) againstPOST/DELETE /settings/logo. It replaces thecbAppLogosetting's text input value with the streamed path (/branding/logo/lg) on upload, and restores the configured default on removal — the manual URL text input and live<img>preview keep working exactly as before for anyone who wants to pointcbAppLogoat an external URL instead. - Both upload endpoints accept the same shapes: images are decoded server-side with
BaseSecureHandler.decodeDataUri(), then resized/cropped intosm/lgJPEG (avatar) or PNG (logo) variants byImageService(app/models/system/ImageService.bx).