[data-reveal]{opacity:1!important;transform:none!important}
Configuration
Building

Configuration

Every option the DMS module accepts in antelope.config.ts — URLs, auth token lifetimes, HTML rendering, realtime driver, and dashboard presentation.

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.

This is a reference page. On a first read, skim the example below and move on to Pages & components — come back when you need a specific key.
antelope.config.ts
"@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

KeyTypeDefaultPurpose
apiBaseUrlstringhttp://127.0.0.1:5010Where 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.
clientBaseUrlstringhttp://localhost:3001 in development, none in productionThe 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.
homepagestring/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.
metaobject—{ title?, description? } — the dashboard's document title and description, served through /api/system-state and applied by the SEO plugin.
authobjectsee belowThe authentication tuning block.
htmlRenderobjectsee belowThe HTML/email rendering block.
realtimeobject{ driver: "auto" }The realtime transport block.
frontendobjectsee belowThe layer-distribution block: who may fetch the frontend layers and their private options.
storeobjectsee belowThe module store block: where its catalog comes from.
docsPreviewbooleanfalseOpts 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.

KeyTypeDefaultPurpose
jwtSecretstring""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".
accessTokenLifetimenumber1 hourAccess-token validity. Short by design — the frontend refreshes transparently.
refreshTokenLifetimenumber30 daysRefresh-token validity. Tokens older than 15 days are rotated on refresh.
emailValidationTokenLifetimenumber1 dayHow long an email-validation token stays usable.
passwordRecoverTokenLifetimenumber1 dayHow long a password-reset code stays usable.
mustValidateEmailbooleanfalseWhen 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.
userSensitiveKeysstring[][]Extra User fields to strip from API responses, on top of the built-in set (tokens, 2FA secrets, …). Dotted paths supported.
oauthobject—Login providers. See below.
trustedProxiesnumber0Reverse 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.
clientIpHeaderstring—Header the hosting platform overwrites with the client's address (X-Client-IP on AntelopeJS Cloud). When set, it replaces trustedProxies. See below.
signInCountryobject—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.

The header is trustworthy only because the platform guarantees who reaches the API: its gateway overwrites the header on every public request, and nothing else but the frontend server can open a connection to the API. AntelopeJS Cloud guarantees both (default-deny network policies, gateway and approved private callers only) and sets the option itself through 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:

OptionTypeDefaultDescription
headerstring—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.
databasestring | falsethe bundled DB-IP databaseA 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.

antelope.config.ts
auth: {
  jwtSecret: "…",
  oauth: {
    providers: {
      github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET },
    },
    allowAccountCreation: true,
  },
},
KeyTypeDefaultPurpose
providersobject{}{ github?, google? }, each { clientId, clientSecret, scopes? }. Credentials belong in environment variables, never in a committed config.
allowAccountCreationbooleanfalseWhether 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.
linkByVerifiedEmailbooleantrueWhether a provider identity binds to the existing account holding the same e-mail. Turn off to require linking from an authenticated session.
callbackBaseUrlstringclientBaseUrlPublic 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.

KeyTypeDefaultPurpose
renderEndpointstringhttp://localhost:3001/api/html/renderThe frontend route that renders a named template to HTML.
serviceSecretstring"dev"Shared secret signing the service token. Required in production: the DMS refuses to start when it is empty or "dev".
serviceTokenLifetimenumber5 minutesValidity 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:

ValueBehavior
"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.
The memory driver is single-instance: events, presence, and notification streams don't cross process boundaries. Run Redis (and 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:

RouteUse
GET /dms/frontendThe manifest ajs dms dev follows: the modules, their layer paths and options.
POST /dms/frontend/readyA 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.

KeyTypeDefaultPurpose
bootstrapSecretstring—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.

KeyTypeDefaultPurpose
catalogUrlstring—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-dms range the installed DMS does not satisfy is refused, as the runtime would.
  • Never blocking a start. optional: true has 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 its frontend-vue/package.json, read from the published archive) that excludes the running dms-frontend is refused, as ajs dms dev would refuse it. The tile names the range, the pnpm 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-redis with no Redis module, say) asks, at install, which npm package implements it: the modules the interfaces registry lists for it, as ajs project modules install offers, 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.