Deployment
Production runs the same two processes as development — the backend and the acms frontend (Architecture) — but each is built once and launched from its build artifacts instead of the dev tooling. The steps below are the production-specific ones; the pages linked from each section own the details.
Build and run the backend
Build the project once, then launch from the artifacts:
ajs project build
ajs project start
ajs project start launches from the build output and skips module download and graph validation. ajs project dev (and ajs project run, its legacy alias) runs the project in dev mode even without -w — so don't use either as a production launcher.
Use production values in your antelope.config.ts: real secrets (see the checklist below), Redis for realtime, durable file storage, and your SMTP settings. Every key and default is in Configuration.
Build and run the frontend
The frontend is built against a running backend — acms build downloads the resolved Nuxt layers from it — then served from the build output:
export CMS_BOOTSTRAP_SECRET=… # same value as the backend's nuxt.bootstrapSecret
acms build -b https://api.example.com
acms start -b https://api.example.com -p 3001
Neither command auto-discovers the backend: pass -b or set CMS_BACKEND_URL. The URL also selects the workspace under ~/.acms/, so start must receive the same URL as build. Flags, workspace behavior, and the --offline cached-manifest mode are in Frontend CLI.
CMS_BOOTSTRAP_SECRET is what entitles the build host to the layers and their private options. Without it the build still succeeds, but the resulting frontend has no oauth.relaySecret or htmlRender.serviceSecret — provider login and server-side HTML rendering then fail at runtime. Both ends say so: acms build warns that no layer arrived with private options, and the backend logs the same conclusion. Set nuxt.requireBootstrap: "enforce" to make the build fail instead. start does not need the secret.
Pass it through the environment rather than --bootstrap-secret, and note that a credential read from a file keeps its trailing newline — acms rejects a value that cannot travel in an HTTP header rather than letting fetch fail obscurely.
Startup order
Start the backend first and wait until it is ready; the frontend asks it for the layers at startup (Architecture). The same ordering applies to the build step: the backend must be up and serving your final module set before you run acms build, or the built frontend won't match it.
Rebuild the frontend whenever the layer set changes — you add or remove a module that calls AddNuxtLayer, or you change a layer's code. Backend-only changes (pages, components, data) need no frontend rebuild: pages are data the frontend draws.
Production checklist
The defaults are development values. Set each of these before going live:
| Key | Default | Production requirement |
|---|---|---|
auth.jwtSecret (DMS config) | "" | A strong random value — it signs every session token. |
htmlRender.serviceSecret (DMS config) | "dev" | A strong random value — it signs the render-service tokens. |
nuxt.bootstrapSecret (DMS config) | — | A strong random value — it gates the layer manifest and source archive. Also set it as CMS_BOOTSTRAP_SECRET where you run acms build. |
secret (the @antelopejs/auth-jwt module's config) | — | A strong random value, independent of auth.jwtSecret. |
apiBaseUrl / clientBaseUrl | localhost | Your public backend and dashboard URLs — generated links (invite emails, signup) use them. |
htmlRender.renderEndpoint | localhost | The production frontend's /api/html/render URL, reachable from the backend. |
All of these live in the DMS config block except the auth-jwt secret, which belongs to that module's own entry — Configuration documents each key. Transactional email also needs an interface-email implementation (typically @antelopejs/nodemailer) wired in the config, or invites, validation, and password resets fail (Backend services).
Multiple instances
Running more than one backend instance adds two requirements:
- Redis for realtime. The in-memory driver does not cross process boundaries — events, presence, and notification streams stay on one instance. Load
@antelopejs/redisand keeprealtime.driverat"auto"or set"redis"(Configuration). - Locks around scheduled work. Cron jobs fire on every instance, so wrap fleet-wide work in
runWithLock— the lock is database-backed, so it needs no extra infrastructure (Backend services).
realtime.driver: "auto"), so most production stacks include it from the start.