Backend and session integration
Use this guide in your application repository after installing Atrium. Atrium provides the frontend shell and UI contracts. Your application supplies real identity, permissions, API endpoints, data normalization, and remote-service adapters.
Replace the starter’s demonstration session
The starter has a fixed demonstration user, capabilities: ['*'], and a login page whose link only navigates home. Replace these before exposing real records. The showcase’s login, recovery, verification, context selector, and local fixture changes illustrate presentation only. Its demonstration verification code 123456 is not an authentication mechanism.
Implement an application-owned session.js against your backend. A useful contract is:
| Application function | Responsibility |
|---|---|
loadSession(). | Obtain the current user, display initials, UI capabilities, and CSRF token from the backend before starting the authenticated application. Handle an unauthenticated response according to your login flow. |
expireSession(). | Clear application session state and direct the user to sign in after session expiry. Do not mistake a forbidden operation for an expired session. |
logout(). | Invalidate the backend session, clear local identity and capabilities, and handle failures through your application UI. |
These names describe application functions, not Atrium exports. The backend’s authentication protocol and response shape determine their implementation. Use anonymous state with an empty capability list if your application starts its public login route without an authenticated identity. Protect private routes with explicit capabilities or an application-owned startup guard. A route without a capability is not automatically private.
In your existing main.js, load the session and create your services before calling createAdmin. Replace the demonstration session and add context and onLogout to its existing configuration. The relevant wiring is:
import { createResourceService } from './services/resources.js';import { loadSession, expireSession, logout } from './session.js';
const session = await loadSession();const resourceService = createResourceService({ /** * Read the current token supplied by the application's session adapter. * * @returns {string} CSRF token for this request. */ csrfToken: () => session.csrfToken, onUnauthorized: expireSession,});Use session: { user: session.user, capabilities: session.capabilities }, context: { resourceService }, and onLogout: logout in createAdmin, preserving its other starter settings. If the session changes after startup, update app.Alpine.store('session').user and .capabilities explicitly. Replacing your local session variable does not replace the shared store.
The current shared shell calls onLogout and immediately navigates to /login. It does not await a returned promise. Your logout adapter must handle its asynchronous work and failures. Customize the header action or use an application controller if navigation must wait for server confirmation. Never treat navigation to /login as proof that the backend session has ended.
Adapt a resource endpoint
The following complete service example assumes your backend supports GET /api/v1/resources with the listed query parameters and returns { data: [...], total: number }. These are example API choices, not endpoints supplied by Atrium. Adapt them to your actual backend. Create services/resources.js in your application:
import { createHttpClient } from '@agon/atrium';
/** * Adapt the application's resource API to the shared table contract. * * @param {object} options - Application session hooks. * @param {Function} options.csrfToken - Read the current CSRF token. * @param {Function} options.onUnauthorized - Handle an expired session. * @returns {{list: Function}} Resource listing adapter. */export function createResourceService(options) { const request = createHttpClient({ base: '/api/v1/', csrfToken: options.csrfToken, onUnauthorized: options.onUnauthorized, });
return { /** * Load one page while preserving cancellation from the table. * * @param {object} query - Normalized search, sort, paging, status, and signal. * @returns {Promise<{items: Array<object>, total: number}>} Matching records and total count. * @throws {Error} When the request or response adaptation fails. */ async list(query) { const search = new URLSearchParams({ q: query.search, page: String(query.page), limit: String(query.pageSize), sort: query.sort, direction: query.direction, });
if (query.status) { search.set('status', query.status); }
const result = await request(`resources?${search}`, { signal: query.signal, });
return { items: result.data, total: result.total, }; }, };}Return stable row IDs and the total matching record count before paging. Normalize backend timestamps to ISO 8601, and preserve null for missing measurements. Zero means a measured zero. Keep endpoint names, tenant selection, pagination differences, and response normalization in the application adapter.
Connect the service to a lazy page
Create pages/resources.html:
<section x-data="resourceList"> <h1 tabindex="-1" x-text="$t('resources')"></h1> <include src="{{ uiRoot }}/data-table.html"></include></section>Create pages/resources.js:
import { dataTable } from '@agon/atrium';
/** * Register the resource page with application-owned services. * * @param {object} context - Shared runtime and configured application services. * @param {object} context.Alpine - Atrium's Alpine runtime. * @param {object} context.resourceService - Resource listing adapter. * @returns {void} Registers a provider before the page mounts. */export function register(context) { context.Alpine.data( 'resourceList', /** * Create independent table state for this route instance. * * @returns {object} Table provider with request and cleanup lifecycle. */ () => dataTable({ path: '/resources', columns: [ { id: 'name', label: 'name', sortable: true, }, ], /** * Delegate normalized queries to the application's API adapter. * * @param {object} query - Filters, paging, sort, and cancellation signal. * @returns {Promise<{items: Array<object>, total: number}>} Matching records. */ load: (query) => context.resourceService.list(query), }), );}Add this route before the wildcard in routes.js and supply the resources and name translation keys in your messages:
{ path: '/resources', label: 'resources', icon: 'box', group: 'workspace', capability: 'resources:read', template: 'pages/resources.html', module: 'pages/resources.js',},The authenticated backend response must supply resources:read for an allowed user. The route capability controls frontend visibility and page access. The API must independently authorize every request. The table owns loading, errors, empty results, pagination, and request cancellation. The page module registers the provider without starting requests itself. See data tables for the full contract.
Transport and failure handling
createHttpClient defaults to same-origin credentials, sends JSON request bodies through its json option, reads a CSRF token for each request, and forwards the supplied AbortSignal. It returns parsed JSON or null for HTTP 204. HttpError exposes status and fields for backend validation errors. Keep form errors in the page controller and display a useful retry state for operational failures.
The current helper parses an error response as JSON before calling onUnauthorized for HTTP 401. Configure API failures to return JSON. An HTML login redirect or proxy error produces an explicit JSON-response error instead of a successful session-expiry callback. HTTP 403 represents forbidden access and does not call onUnauthorized.
For a separate API origin, provide an application adapter with the credentials and CORS behavior required by your backend. A same-origin /api/ URL in Vite development reaches the frontend dev server unless you configure a development proxy or another API origin. Production API routing is a separate server configuration. Atrium does not supply retries, token refresh, authentication storage, or a tenant-switching protocol.
Keep request cancellation, timer teardown, and socket cleanup in the owning provider’s destroy() method. Use x-text for backend strings. Trusted build-time templates must never come from API responses.
Domain-specific applications
For i-MSCP, application modules can represent domains, mailboxes, databases, accounts, and service operations. Adapt those endpoints to the same UI contracts. The Atrium package has no knowledge of these entities. The same boundary applies to Django, hosting platforms, and other backends.
Maps, video, SSH gateways, and RFB desktops have their own component integration guides. Your application supplies media, geometry, authorization, and connection adapters. Showcase fixtures are examples rather than production services.
Verify the integration
- Sign in through the real backend and confirm the shell receives the expected identity and capabilities.
- Check a permitted route, a denied route, direct API authorization failures, and a session that expires while the page is open.
- Load records, change filters, navigate away during a pending request, and verify loading, empty, and error states.
- Confirm logout invalidates the server session and clears application state, including when the request fails.
- Verify CSRF handling for state-changing requests and tenant isolation where your application has multiple tenants.
Continue with deployment and application acceptance.
Author
Laurent Declercq l.declercq@agon-innovation.ch
License
Unless otherwise stated all source code is licensed under LGPL 2.1 and has the following copyright:
© 2026, Agon Partners Innovation AG, All rights reserved.The design material and the “Agon Ātrium” trademark is the property of their authors. Reuse of them without prior consent of their respective authors is strictly prohibited.
Version
Version: 20260921