Sessions and sign-in
The frontend server keeps the session in a sealed, HttpOnly cookie and holds the access and refresh tokens server-side. The renderer itself serves no session, sign-in, OAuth or e-mail render route: those are the DMS's own, declared in its dms.server.ts. Without a module that registers auth hooks, pages carry no user and session, and a refused session is not renewed.
For the screens a user sees, start with the illustrated built-in authentication guide: it shows the sign-in form and Security page. This page covers the server hooks behind those screens; registering a hook does not create a sign-in UI.
Auth hooks
The server runs session logic outside any route: it renews a session the backend refused on a page visit, puts the user and session page props together, and sends a signed-out visit to sign in. The module that owns sign-in provides that logic:
server.registerAuth({
signInPath: "/auth",
signInExempt: ["/auth/**", "/onboarding"],
refreshSession: renewSession, // (event, session) => the new session, or undefined
publicSession: (session) => ({
user: session.user,
session: { accountId: session.accountId, activeTenantId: session.activeTenantId },
}),
});
refreshSessionis called once when the backend answers401to a signed-in page visit. The visit is then retried with the session it wrote throughevent.session.write.publicSessiongives the page props of a signed-in session. The server addssession.updatedAt, the stamp of its last write, andevent.session.view()returns the same props to a route.signInExemptpatterns are/x,/x/*(one segment below) or/x/**(the page and everything below). Left out, they are<signInPath>/**and/onboarding, so a customsignInPathis exempt on its own.
The highest-priority module that registers auth hooks provides all of them; another one is reported and ignored.
Interface language
The interface language follows the session (useDmsUserSession().user.language), not the props of the page on screen. The first render takes it from the session the page arrived with, and it changes when the session's language does, after useDmsUserSession().fetch() or a visit that brings a newer session.
- A page Inertia restores from history, or serves from its prefetch cache, carries the session it was fetched with. A page that carries an older
session.updatedAtstamp for the same account updates neither the session nor the language. - A session without a language, such as a signed-out one, keeps the language on screen.
$i18n.setLocale()still switches it at any time. - The language the application offers is remembered in the
dms_localecookie (one year,SameSite=Lax). A page without a signed-in user's language, such as a public sign-in page, first renders in that one: a visitor's choice survives a reload, while a signed-in user's own language comes first.
Opening a session from a module flow
/auth/login, /auth/signup and /auth/verify-2fa are not the only ways a visitor becomes authenticated. A module can own a flow that ends with an authenticated user, such as a self-service registration completing after payment or an invitation being redeemed, and needs the session opened at its end. POST /auth/establish is the generic form of those routes.
The browser names a backend endpoint and the payload to send it:
await $fetch("/auth/establish", {
method: "POST",
body: {
endpoint: "/api/saas/register/finalize",
payload: { /* whatever that backend route expects */ },
},
});
The frontend server calls that endpoint itself, server to server, exactly as it calls the backend's login route, and writes the session from the token pair the backend answers with. The browser never sends or receives a token: it gets back the same { user, account } body the login route returns, and the two-factor and tenant-assignment outcomes are handled the same way.
Because the route turns a backend endpoint into a login, it only calls the endpoints declared for it. A backend module declares its own when it registers its frontend:
await AddFrontendModule({
name: "@antelopejs/dms-saas-frontend-vue",
sourcePath: path.join(__dirname, "../frontend-vue"),
renderer: { name: "vue", version: "3" },
authEstablishEndpoints: ["/api/saas/register/finalize"],
});
The DMS gathers them into the manifest, and dev and build write them into the workspace. DMS_AUTH_ESTABLISH_ENDPOINTS adds endpoints no module declares, comma-separated; the allow-list is the union of both.
Every entry is matched verbatim against absolute /api/… paths: no prefixes, no query strings, no traversal. An undeclared endpoint is answered 403 and never called. The route is POST-only and same-origin, like every other auth action.