Configuration
The DMS module reads one config object — the config block you give @antelopejs/dms in your antelope.config.ts (Project setup). The reference below covers every key and its default. All keys are optional; a bare config: {} runs with the defaults.
"@antelopejs/dms": {
source: { /* … */ },
config: {
apiBaseUrl: "http://localhost:5010",
clientBaseUrl: "http://localhost:3001",
homepage: "/form/form-simple",
meta: { title: "Acme Admin", description: "Internal operations dashboard" },
auth: {
jwtSecret: "…", // the DMS's session-token signing secret — set a strong value in production
mustValidateEmail: true,
},
realtime: { driver: "auto" },
htmlRender: { renderEndpoint: "http://localhost:3001/api/html/render" },
},
},
Top-level options
| Key | Type | Default | Purpose |
|---|---|---|---|
apiBaseUrl | string | http://127.0.0.1:5010 | Where the backend is reachable. The frontend's authenticated fetch is bound to it, and modules read it through GetApiBaseUrl() to build absolute links to backend routes. |
clientBaseUrl | string | http://localhost:3001 in development, none in production | The dashboard's http(s) origin, without a path or parameters. Used in generated links (for example the signup link in invite emails), added to the api module's allowed CORS origins, and read by modules through GetClientBaseUrl(). Required in production: the DMS refuses to start without it, or with a value that is not an origin. |
homepage | string | / | The route the dashboard opens on. Read by the frontend via useHomepage(). A user whose role cannot access it is redirected to the first page they can access. |
meta | object | — | { title?, description? } — the dashboard's document title and description, served through /api/system-state and applied by the SEO plugin. |
auth | object | see below | The authentication tuning block. |
htmlRender | object | see below | The HTML/email rendering block. |
realtime | object | { driver: "auto" } | The realtime transport block. |
frontend | object | see below | The layer-distribution block: who may fetch the frontend layers and their private options. |
store | object | see below | The module store block: where its catalog comes from. |
docsPreview | boolean | false | Opts into the hidden public /dms-preview page. Framework development mode (GetRuntimeInfo().dev) or DMS_DOCS_PREVIEW=1 also enables it, even when this option is false. Evaluated at module construction, not dynamically; NODE_ENV does not select framework development mode. See Block preview page. |
Public URLs in a module
A module that builds an absolute link asks the DMS for the origin instead of reading request headers, which a reverse proxy may not forward (X-Forwarded-Host):
import { GetApiBaseUrl } from "@antelopejs/interface-dms/api-base-url";
import { GetClientBaseUrl } from "@antelopejs/interface-dms/client-base-url";
const fileUrl = `${await GetApiBaseUrl()}/i18n/site/fr.json`; // a backend route
const pageUrl = `${await GetClientBaseUrl()}/modules/lang`; // a dashboard page
Each answers the configured value (apiBaseUrl, clientBaseUrl), or undefined when the configuration clears it. Under ajs dms dev, both follow the origins the dev server reports, as the frontend's URLs do.
auth — tokens, validation, sanitization
These settings drive the Built-in authentication flow. Lifetimes are in milliseconds.
| Key | Type | Default | Purpose |
|---|---|---|---|
jwtSecret | string | "" | The DMS's own signing secret for session tokens, mixed into each user's per-user token secret. It is independent of the @antelopejs/auth-jwt module's secret. Required in production: the DMS refuses to start when it is empty or "dev". |
accessTokenLifetime | number | 1 hour | Access-token validity. Short by design — the frontend refreshes transparently. |
refreshTokenLifetime | number | 30 days | Refresh-token validity. Tokens older than 15 days are rotated on refresh. |
emailValidationTokenLifetime | number | 1 day | How long an email-validation token stays usable. |
passwordRecoverTokenLifetime | number | 1 day | How long a password-reset code stays usable. |
mustValidateEmail | boolean | false | When true, users must validate their email before @AuthUser-guarded routes accept them. A signup through an invitation link counts as validated and never needs a separate round. |
userSensitiveKeys | string[] | [] | Extra User fields to strip from API responses, on top of the built-in set (tokens, 2FA secrets, …). Dotted paths supported. |
oauth | object | — | Login providers. See below. |
trustedProxies | number | 0 | Reverse proxies in front of the API that append to x-forwarded-for. Picks the client address recorded on sessions, looked up for a sign-in's country, and answered to modules by getClientIp. See below. |
clientIpHeader | string | — | Header the hosting platform overwrites with the client's address (X-Client-IP on AntelopeJS Cloud). When set, it replaces trustedProxies. See below. |
signInCountry | object | — | Where the "New sign-in" alert reads a sign-in's country. See below. |
Client address behind proxies
Every session records the address it was opened from — it appears under Profile › Sessions, and the sign-in country is looked up from it. auth.trustedProxies tells the API how many reverse proxies stand in front of it, each appending the address it received the request from to x-forwarded-for. The API walks back from its socket peer through that many entries, from the right, and records the one the outermost trusted proxy saw; entries further left were written by the client and are never used. With the default 0 it records the socket peer and ignores the header. A chain shorter than configured yields its leftmost entry. Modules resolve a request's address the same way through getClientIp (@antelopejs/interface-dms/client-ip), so this one setting covers them too.
Count every hop between the browser and the API. Sign-in goes through the frontend server, which calls the API's /api/auth/* routes and sets x-forwarded-for to the client it resolved itself (from its own DMS_TRUSTED_PROXY_HOPS), so it is one hop; add each proxy on its way to the API — a frontend server reaching the API directly needs 1, one reaching it through its public URL behind a CDN and an ingress needs 3. Too low records a proxy address; too high lets a client choose the recorded address.
Client address from the platform
auth.clientIpHeader names a header the hosting platform writes on every request with the client's address. When it is set, the API takes the client address from that header, for sessions, the sign-in throttle and getClientIp, and ignores trustedProxies. A request without that header, or with anything but one IP address in it, falls back to the socket peer, never to x-forwarded-for. The frontend server sets the same header on its backend requests when its DMS_CLIENT_IP_HEADER names it, so name the same header on both sides.
DMS_CLIENT_IP_HEADER. Do not set it on an API any other caller can reach: that caller would choose its own address.Country of a sign-in
A sign-in from a browser, system and device type the account never used raises a "New sign-in" notification. The country it came from counts too: the same browser and system signing in from another country is a new sign-in, and the notification names the country (never the address). auth.signInCountry says where the country comes from:
| Option | Type | Default | Description |
|---|---|---|---|
header | string | — | Header a trusted reverse proxy fills with the client's ISO 3166-1 country code, such as CF-IPCountry behind Cloudflare. Read only when set, and wins over the database. The proxy must overwrite it on every request, since any caller can send it. |
database | string | false | the bundled DB-IP database | A MaxMind-format country database (.mmdb) the client address (see above) is looked up in when the header gives nothing. false turns the lookup off. |
The package ships the DB-IP IP to Country Lite database (data/dbip-country-lite.mmdb), downloaded afresh each time a release is packed. It is licensed under the Creative Commons Attribution 4.0 International License: IP Geolocation by DB-IP. The attribution ships with it in data/NOTICE.md; a dashboard that shows sign-in countries to its users keeps that attribution in its own credits. When neither source knows the country — a private address, a lookup turned off — the device alone decides. Behind the frontend server, the header reaches the API when the frontend server relays it: @antelopejs/dms-frontend 0.4 relays the header its DMS_COUNTRY_HEADER variable names (CF-IPCountry by default) when DMS_TRUSTED_PROXY_HOPS is above 0. Name the same header on both sides.
auth.oauth — login providers
Precoded providers (github, google) turn into a "Continue with …" button on the login and signup screens. A provider stays disabled — and invisible — until the instance supplies both credentials, so this block is entirely opt-in.
auth: {
jwtSecret: "…",
oauth: {
providers: {
github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET },
},
allowAccountCreation: true,
},
},
| Key | Type | Default | Purpose |
|---|---|---|---|
providers | object | {} | { github?, google? }, each { clientId, clientSecret, scopes? }. Credentials belong in environment variables, never in a committed config. |
allowAccountCreation | boolean | false | Whether an unknown provider identity may create an account without an invitation. Off by default: turning it on makes every provider account a sign-up entry. |
linkByVerifiedEmail | boolean | true | Whether a provider identity binds to the existing account holding the same e-mail. Turn off to require linking from an authenticated session. |
callbackBaseUrl | string | clientBaseUrl | Public origin the provider redirects back to. Set it when the browser-facing origin differs from the configured one. |
Register <callbackBaseUrl>/auth/oauth/<provider>/callback as the authorization callback URL in the provider's application settings.
The browser-facing OAuth routes are rate-limited to 10 flows per minute per client, enforced by the frontend server (each route carries its own 10-hit budget, so a full start + callback flow spends one of each). What identifies a client: the frontend server resolves it from its own DMS_TRUSTED_PROXY_HOPS environment variable, the number of proxies in front of it — by default 0, the socket address only, since any caller can write x-forwarded-for; behind a shared proxy clients then share one budget until you set it. Relevant mostly with allowAccountCreation on, where the flow would otherwise be an unthrottled account factory.
Account linking rules. A provider account already bound to a user always resolves to that user. Otherwise, an account holding the same e-mail is only adopted when the provider marks that e-mail verified and the local account is validated — control of the mailbox is the single prerequisite, the same bar password recovery sets. Anything else is refused rather than linked silently. Two-factor authentication is unaffected: an OAuth login on a 2FA-enabled account still runs the challenge before a session is issued.
Signing in with a provider from an invitation link consumes the invitation, provided the provider e-mail matches the invited one — and creates the account even with allowAccountCreation off, since the invitation is itself the permission to join.
htmlRender — HTML & email rendering
The backend renders HTML templates (transactional emails, static HTML) by calling the frontend's render endpoint with a short-lived service token — see Backend services.
| Key | Type | Default | Purpose |
|---|---|---|---|
renderEndpoint | string | http://localhost:3001/api/html/render | The frontend route that renders a named template to HTML. |
serviceSecret | string | "dev" | Shared secret signing the service token. Required in production: the DMS refuses to start when it is empty or "dev". |
serviceTokenLifetime | number | 5 minutes | Validity of each render-service token, in milliseconds (at most 5 minutes). |
realtime — transport driver
The realtime layer (SSE topics, presence, notification streams — see Backend services) picks its broker with realtime.driver:
| Value | Behavior |
|---|---|
"auto" | (default) Uses Redis when a @antelopejs/redis instance is available, otherwise falls back to memory. |
"redis" | Always use Redis pub/sub — required for multi-instance deployments. |
"memory" | In-process only. |
driver: "auto" or "redis") as soon as you deploy more than one backend instance, and route /api/realtime/* with affinity (Deployment).frontend — the frontend server
A production frontend is built and started from the project itself: ajs dms build and ajs dms start evaluate antelope.config.ts and call each module's frontend function (the dmsFrontend entry of its package.json), which the backend calls too. The backend only serves two routes to the frontend tool:
| Route | Use |
|---|---|
GET /dms/frontend | The manifest ajs dms dev follows: the modules, their layer paths and options. |
POST /dms/frontend/ready | A frontend server reports the modules it serves and their package versions; the backend warns when one differs from what it runs. |
Renderer selection
Pass renderer and rendererVersion together on the manifest route. Versions match by major version.
- Unsupported selections return
404. - Incomplete or invalid selections return
400. - Omitting both parameters selects Vue 3 for compatibility.
The manifest's top-level version is the frontend protocol version, not a renderer or package version. Adapters fail closed when they receive a version they do not implement.
Bootstrap protection
The manifest carries things no browser and no stranger should see: each module's privateOptions (oauth.relaySecret, htmlRender.serviceSecret) and the paths of its sources.
bootstrapSecret is the credential ajs dms presents to reach these routes, and setting it is what closes them: an instance that configures one answers 401 error.frontend.bootstrap_required to every caller that does not present it.
| Key | Type | Default | Purpose |
|---|---|---|---|
bootstrapSecret | string | — | Shared secret between this instance and its frontend server. Configuring it gates both routes. Set a strong value in production. |
requireBootstrap | "warn" | "enforce" | "warn" | Consulted only when no bootstrapSecret is configured. "warn" serves an unauthenticated caller a manifest stripped of every private option, and logs once. "enforce" answers 401 — which, with no credential to present, closes the routes to everyone. |
config: {
frontend: {
bootstrapSecret: process.env.DMS_BOOTSTRAP_SECRET,
},
},
Set the same value as DMS_BOOTSTRAP_SECRET on the frontend server. It is deliberately not derived from auth.jwtSecret: it lives on another host, so it must not be a function of the secret that signs session tokens, and rotating it must not log every user out.
requireBootstrap cannot re-open a gated instance: the credential's presence decides on its own. To leave the routes open, leave bootstrapSecret unset.Development credentials
In development nothing needs configuring. The instance generates an ephemeral per-boot credential and publishes it to the project's .antelope/dms-dev.json (owner-readable only, stamped with its pid). ajs dms dev and ajs dms prepare find it by walking up from the working directory.
The file outlives the instance that wrote it: deleting it on shutdown could race a replacement backend's freshly published credential. A leftover copy is ignored once its pid is gone.
store — module store
The Modules page installs, updates, pauses and removes the official AntelopeJS modules on a running platform (Built-in dashboard). What it installs goes into antelope.config.ts, like a module added with ajs project modules add, so every environment loads it.
Owners change modules from the store in development only. In production, where a new module also needs a frontend rebuild, the store lists modules read-only and the platform loads what antelope.config.ts declares.
| Key | Type | Default | Purpose |
|---|---|---|---|
catalogUrl | string | — | A remote catalog, shaped like the bundled data/module-store/catalog.json, read every hour. The bundled catalog is used when unset or when the URL cannot be read. |
The store writes one entry per module, under the package name:
modules: {
"@antelopejs/dms-lang": {
source: {
type: "package",
package: "@antelopejs/dms-lang",
version: "^0.1.1",
},
optional: true,
},
},
It edits only that entry, through the core: the rest of the file — comments, expressions, other modules — stays as it is, and the running project does not restart for the write, since the store has already loaded, stopped or reloaded the module itself. A paused module keeps its entry with disabled: true; uninstalling removes the entry, and the module's data stays where it was.
- Which entries. The store manages the entries that install a catalog module from npm, whatever their key, so a module first added by hand can be updated or paused from the Modules page. An entry that loads a module from a local folder or git is left alone: the store recognises the working copy by the package name in its
package.json, shows the module as active with that version, and offers no action on it (installing it from npm next to it is refused). - Versions. The store writes a caret range on the latest release. Releases within the range load at the next start on their own; the page offers an update when the latest release falls outside it, and the update moves the range. A release whose
@antelopejs/interface-dmsrange the installed DMS does not satisfy is refused, as the runtime would. - Never blocking a start.
optional: truehas the core load the module once the others have started, and run on without it when it fails to download, construct or start: the page shows it as not running, with an Apply action. - The frontend loader. A release whose frontend layer declares a range of
@antelopejs/dms-frontend(engines["@antelopejs/dms-frontend"]in itsfrontend-vue/package.json, read from the published archive) that excludes the running dms-frontend is refused, asajs dms devwould refuse it. The tile names the range, thepnpm add -D @antelopejs/dms-frontend@<version>that would satisfy it, and the installed layers that version would refuse; the store never moves dms-frontend itself. The running version is the one the frontend server reports when it becomes ready, or else the one installed in the project. - Interfaces. A release that depends on an interface no running module implements (an
@antelopejs/interface-rediswith no Redis module, say) asks, at install, which npm package implements it: the modules the interfaces registry lists for it, asajs project modules installoffers, or any other npm package, never a git or local source, with the JSON config of its entry. That module is installed first, under its own entry, and the install is undone as a whole when either fails. A module other running modules need for such an interface cannot be paused or removed until they are.
The store needs @antelopejs/core 1.15.0 or later, which implements the module declaration functions of @antelopejs/interface-core/modules (ListModuleDeclarations, SaveModuleDeclaration, RemoveModuleDeclaration, from interface-core 0.2.1), retires the generation LoadModule replaces and refuses a module whose interfaces nothing implements. It also needs a configuration whose default export is an object, plain or through defineConfig. Otherwise it stays read-only and says why. @antelopejs/dms-frontend 0.6.1 or later reports its version when it becomes ready; with an older one, the store reads the version installed in the project.