aegisnode
v0.1.1
Published
A view-first Node.js framework for modular web apps and JSON APIs with CLI scaffolding, runtime injection, auth, uploads, i18n, mail, and WebSocket support.
Maintainers
Readme
AegisNode
AegisNode is a modular, view-first Node.js framework for building web apps, JSON APIs, and hybrid projects. It gives you a structured project layout, runtime injection, CLI scaffolding, and production-ready defaults so you can start building features instead of first wiring routing, config loading, auth, uploads, and other framework glue.
AegisNode is designed for developers who want more structure than raw Express, but do not want a framework that hides the Node.js runtime behind too many abstractions. It keeps the request/response model familiar while organizing the codebase around clear app boundaries and reusable layers such as views, services, models, validators, subscribers, and app-local utilities.
It works well for projects that mix server-rendered pages and JSON endpoints, for teams that want a consistent project shape from the start, and for codebases that need built-in support for common backend concerns like auth, uploads, i18n, mail, maintenance mode, and environment-driven configuration.
Read this README in this order:
- Quick Start: get a project created, installed, and running.
- Core Concepts And App Structure: understand where code lives and how layers fit together.
- Common Tasks And Feature Guides: jump to the feature you need once the basics are clear.
- Full Settings Reference: use this when you already know which config block you are looking for.
- Runtime Patterns And Advanced Topics: read this last for middleware, validators, subscribers, and strict-layer behavior.
If you prefer a sidebar-based handbook, open docs/index.html in a browser.
Environment files are loaded automatically before settings.js or settings.ts is imported:
.env.env.local.env.<NODE_ENV>.env.<NODE_ENV>.local
Shell or hosting-panel environment variables win over values from .env files.
Quick Start
Use this section to get a project running first. Skip to later sections only when you need a specific feature or configuration block.
Create A Project
npm install -g aegisnode
mkdir blog && cd blog
aegisnode startproject blogCreate and enter the project directory first. startproject scaffolds into the current empty directory; it does not create a nested folder for you.
Use plain startproject for a JavaScript project. Add --typescript once if you want the whole scaffold to use .ts.
For a TypeScript project, use aegisnode startproject blog --typescript inside the target folder instead.
startproject creates app.js, loader.cjs, .env, settings.js, and routes.js in the current directory without creating any default app.
Use startproject --typescript to generate app.ts, settings.ts, routes.ts, app *.ts files, and tsconfig.json instead.
It also creates public/ and templates/ so the default staticDir and templates.dir targets already exist.
Create any additional folders such as logs/ yourself when needed.
Install Dependencies And Run The Server
npm install
aegisnode runserverIf you choose to stay outside the project folder, the project-level commands also accept --project <path>.
Create Your First App
aegisnode createapp users
aegisnode generate view profile --app users
aegisnode generate route profile --app userscreateapp updates settings.apps and the root routes.js or routes.ts mapping for you.
It also generates default app tests under apps/<app>/tests.
Startup Mode Rules
- Development (
env === 'development'): start withaegisnode runserver. - Non-development (
env !== 'development'): start withnode loader.cjs. node app.jsandnode loader.cjsare blocked in development mode.aegisnode runserveris blocked outside development mode.
Project Maintenance Commands
Use these after the project already exists:
aegisnode doctor
aegisnode doctor --app users
aegisnode fix --app users
aegisnode generateloader
aegisnode updatedepsWhat they do:
doctor: checks project structure, startup entry files, app declarations, and security/auth basics.doctor --app: checks one app for missing scaffold files, tests, and registrations.fix --app: recreates missing app scaffold files without overwriting existing files.generateloader: restoresloader.cjsandapp.jswhen startup files are missing.updatedeps: rewrites package dependency ranges to the current npmlatestversions and reinstalls.
Run project tests with:
npm testJavaScript vs TypeScript Projects
The project type is chosen once at startproject time:
aegisnode startproject blogscaffolds a JavaScript project in the current directoryaegisnode startproject blog --typescriptscaffolds a TypeScript project in the current directory
After that, the rest of the CLI follows the project automatically:
createappgeneratesviews.js/services.js/routes.jsin JavaScript projects, orviews.ts/services.ts/routes.tsin TypeScript projectsgeneratecreates artifacts with the same extension as the project, for exampleprofile.view.jsorprofile.view.tsfix,doctor, andgenerateloaderalso check and repair the matching project file type automatically
createapp, fix, generate, runserver, generateloader, doctor, and updatedeps are project-level commands.
Run them from the project root; do not cd into apps/<app>.
Core Concepts And App Structure
Use this section to understand the project shape before you start adding more features. The goal here is to make it clear where code belongs and what each layer is responsible for.
Generated Project Shape
startproject creates the runtime entry files and base config for you in the current directory:
- JavaScript projects:
app.js,loader.cjs,.env,settings.js,routes.js - TypeScript projects:
app.ts,loader.cjs,.env,settings.ts,routes.ts,tsconfig.json
createapp then adds feature modules under apps/<app>/:
views.jsorviews.tsmodels.jsormodels.tsservices.jsorservices.tsvalidators.jsorvalidators.tsroutes.jsorroutes.tssubscribers.jsorsubscribers.tsutils.jsorutils.tstests/
Other scaffold rules to know:
createappauto-detects the project root when you run it inside the project or from a parent folder containing exactly one AegisNode project.- New apps are registered in
settings.appsand mounted in the rootroutes.jsorroutes.ts. - Only apps declared in
settings.appsare allowed to load or mount. --mountaccepts only safe path segments (a-z,A-Z,0-9,_,-,:).- New app routes are generated in an API-ready CRUD shape by default:
GET /<mount>POST /<mount>GET /<mount>/:idPUT /<mount>/:idDELETE /<mount>/:id
- Default app tests generated by
createappare:- JavaScript projects:
apps/<app>/tests/models.test.js,validators.test.js,services.test.js,routes.test.js - TypeScript projects:
apps/<app>/tests/models.test.ts,validators.test.ts,services.test.ts,routes.test.ts
- JavaScript projects:
Generated Settings Shape
startproject generates a minimal settings.js or settings.ts, and runtime defaults fill the rest.
Access environment values directly with process.env in the settings file:
export default {
port: process.env.PORT ? Number(process.env.PORT) : 3000,
security: {
appSecret: process.env.APP_SECRET || '<generated-at-scaffold-time>',
},
};Injected app layers also receive env, so views, services, models, validators, controllers, subscribers, and loaders can use env.MY_NAME without importing process.env.
Generated shape:
export default {
appName: 'blog',
env: process.env.NODE_ENV || 'development',
host: process.env.HOST || '0.0.0.0',
port: process.env.PORT ? Number(process.env.PORT) : 3000,
trustProxy: false,
staticDir: 'public',
templates: {
enabled: true,
engine: 'ejs',
dir: 'templates',
base: 'base',
},
security: {
appSecret: process.env.APP_SECRET || '<generated-at-scaffold-time>',
},
logging: {
level: process.env.LOG_LEVEL || 'info',
},
database: {
enabled: false,
dialect: 'pg',
config: {},
options: {},
},
cache: {
enabled: true,
driver: 'memory',
options: {},
},
apps: [
// AEGIS_APPS_START
// AEGIS_APPS_END
],
};Notes:
- Keep
AEGIS_APPS_START/ENDmarkers;createappupdates this list automatically. startprojectwrites a local.envwith a generatedAPP_SECRETand also embeds the same generated secret insettings.jsorsettings.tsas a fallback.- The scaffold already includes
staticDir: 'public'and a defaulttemplatesblock, and it creates those directories for you. - Add optional blocks only when you need them:
https,i18n,helpers,websocket,uploads,mail,auth,api,swagger,loaders,environments,architecture,security.headers/ddos/csrf. - Any section you omit uses framework defaults from
src/runtime/config.js.
App File Usage Examples
Each generated app usually contains:
apps/<app>/views.jsapps/<app>/models.jsapps/<app>/services.jsapps/<app>/utils.jsapps/<app>/subscribers.jsapps/<app>/routes.js
If the project was created with --typescript, the same generated files use .ts instead of .js.
Usage by file:
views.js: HTTP handlers (req,res,next). Default signature can be context-first:handler({ service, validator, services, validators, ... }, req, res, next). Keepviews.jsthin: prefer only the view class and its imports. Avoid defining extra local helper/utility functions in the view file. Move reusable pure logic toutils.jsand app workflows toservices.js.models.js: data access layer only (SQL/NoSQL operations).services.js: business logic layer; orchestrates models and uses injected runtime objects when needed.utils.js: app-local pure utility functions. Use this for small reusable helpers that belong only to the app. Do not put DB access, request validation, or business workflows here.utils.jsis a plain module, not an injected runtime layer. If a utility needsjlive,helpers,i18n, or another injected runtime object, inject that object into a view/service/model first and pass it into the utility function as an argument.subscribers.js: event listeners (for exampleapp.booted,ws.connection, custom events).routes.js: route mapping only (route.get(...),route.post(...),route.use(...)) to view handlers.
Short rule for utils.js vs services.js:
- Use
utils.jsfor pure app-local helpers such as string formatting, slug generation, payload shaping, or small mappers. - Use
services.jsfor application behavior: anything that coordinates models, injected runtime objects, or feature rules.
Route modules are mapping-only (register(route)).
Framework context is injected into handlers as first argument (when handler uses 4 args): { service, validator, services, models, validators, auth, mail, helpers, i18n, events, ... }.
req.aegis is also available.
service/validator are app-scoped conveniences. For root/non-app routes, use services.get('<app>.<name>') / validators.get('<app>.<name>'), or create an app-scoped accessor with services.forApp('<app>').
What “app-scoped” means:
- In app routes (for example inside
apps/users/routes.js),{ service }resolves to that app service. - In root/global routes (
routes.js), there is no single app context, so use{ services }and fetch withservices.forApp('<app>').get('<name>')orservices.get('<app>.<name>').
Injected runtime dependencies:
AegisNode injects resolved runtime objects instead of asking app layers to import framework internals. config is the resolved runtime config from settings.js plus defaults and runtime overrides.
Available by layer:
- Views/handlers (
views.jsor any context-first route/controller action):appName,app,config,env,i18n,mail,logger,events,cache,io,auth,helpers,jlive,upload,services,models,validators,service,model,validator,database,dbClient - Services (
constructor({ ... })):appName,config,env,i18n,mail,logger,events,cache,io,auth,helpers,jlive,models,validators,services - Models (
constructor({ ... })):appName,config,env,i18n,mail,logger,events,cache,io,helpers,jlive,dbClient,database - Validators (
constructor({ ... })):appName,config,env,i18n,mail,logger,events,cache,io,auth,helpers,jlive,dbClient,database - Subscribers (
export default function ({ ... })):appName,rootDir,config,env,i18n,mail,logger,events,cache,io,auth,helpers,jlive,upload,services,models,validators,database,dbClient,app,server,templates,protocol,container,declaredAppNames - Controllers (
constructor({ ... })):appName,rootDir,config,env,i18n,mail,logger,events,cache,io,auth,helpers,jlive,upload,services,models,validators,database,dbClient,container,app - Loaders (
loadersentry function):rootDir,config,env,i18n,mail,logger,events,cache,io,auth,helpers,jlive,upload,services,models,validators,database,dbClient,app,server,templates,protocol,container,declaredAppNames,options - Request bridge (
req.aegis):config,env,i18n,locale,localeSource,t,setLocale,logger,events,cache,io,auth,mail,helpers,jlive,upload,services,models,validators,database,dbClient,appName,app - Template locals:
helpers,jlive,t,locale,i18n,money,number,dateTime,timeElapsed,timeDifference,breakStr
Key meanings:
| Key | Description |
| --- | --- |
| config | Resolved runtime config from settings.js, framework defaults, environment overrides, and runtime overrides. |
| env | Frozen environment snapshot (process.env plus runtime additions such as APP_SECRET). |
| i18n | Translator bridge. During a request it follows the active request locale; outside a request it falls back to defaultLocale unless you pass { locale }. |
| mail | Mail manager. Use mail.send({ to, subject, text/html }) or mail.sendMail(...). |
| logger | Runtime logger instance. |
| events | Event bus used by subscribers and app code. |
| cache | Cache backend instance (memory by default). |
| io | Socket.IO server instance when websocket support is enabled. |
| auth | Auth manager for JWT/OAuth2 flows. |
| helpers | Runtime helper functions such as money, number, dateTime, and timeElapsed. |
| jlive | jlive bridge instance. |
| upload | Upload manager used by route.upload. |
| services | Layer accessor used to fetch services by app/name. |
| models | Layer accessor used to fetch models by app/name. |
| validators | Layer accessor used to fetch validators by app/name. |
| service | App-scoped convenience service for the current app only. |
| model | App-scoped convenience model for the current app only. |
| validator | App-scoped convenience validator for the current app only. |
| database | Database runtime wrapper. |
| dbClient | Low-level database/query client. |
| appName | Current app name. |
| app | Current app metadata/context. |
| rootDir | Absolute project root. |
| server | HTTP/HTTPS server instance. |
| templates | Resolved template-engine configuration. |
| protocol | Server protocol (http or https). |
| container | Internal DI container. |
| declaredAppNames | Set of apps declared in config/routes. |
| options | Loader-specific options object from a { path, options } loader entry. |
| locale | Active request locale. Available on req.aegis and template locals. |
| localeSource | Where the current locale came from (query, cookie, header, manual, or disabled). |
| t | Convenience translator shortcut for the current request/template scope. |
| setLocale | Request helper used to change and optionally persist the active locale. |
// routes.js (root/global)
export default {
register(route) {
route.get('/dashboard', async ({ services }, req, res, next) => {
try {
const usersService = services.forApp('users').get('users');
const ordersService = services.forApp('orders').get('orders');
res.json({
users: await usersService.list(),
orders: await ordersService.list(),
});
} catch (error) {
next(error);
}
});
},
};Example views.js:
class UsersView {
static async index({ service }, req, res, next) {
try {
const data = await service.listUsers();
res.json({ data });
} catch (error) {
next(error);
}
}
static async create({ service, validator }, req, res, next) {
try {
const payload = validator.create(req.body || {});
const created = await service.createUser(payload);
res.status(201).json({ data: created });
} catch (error) {
next(error);
}
}
static async tools({ service, helpers, jlive }, req, res, next) {
try {
const stats = await service.stats();
res.json({
stats,
total: helpers.money(1299.5, { currency: 'USD' }),
elapsed: helpers.timeElapsed(Date.now() - 60_000),
token: jlive.generate(16),
});
} catch (error) {
next(error);
}
}
}
export default UsersView;Example models.js:
class UsersModel {
constructor({ dbClient }) {
this.dbClient = dbClient;
}
async list() {
return [{ id: '1', name: 'Alice' }];
}
async create(payload) {
return { id: '2', ...payload };
}
}
export default { users: UsersModel };Example services.js:
class UsersService {
constructor({ models, env }) {
this.usersModel = models.get('users');
this.env = env;
}
async listUsers() {
return this.usersModel.list();
}
async createUser(payload) {
return this.usersModel.create(payload);
}
}
export default { users: UsersService };Example subscribers.js:
export default function registerUsersSubscribers({ events, logger }) {
events.subscribe('app.booted', ({ appName }) => {
logger.info('[users] booted: %s', appName);
});
}Injected env is also available in:
- view handler context:
static index({ env }, req, res) { ... } - model constructors:
constructor({ dbClient, env }) { ... } - subscribers:
export default function ({ events, env }) { ... } - request runtime bridge:
req.aegis.env
Example routes.js:
import UsersView from './views.js';
export default {
appName: 'users',
register(route) {
route.get('/home', UsersView.home);
route.get('/', UsersView.index);
route.post('/', UsersView.create);
},
};Common Tasks And Feature Guides
Use this section once the project is already running and you need a specific feature, integration, or deployment-related behavior.
Reverse Proxies And Passenger
If HTTPS is terminated by Nginx, Apache, Passenger, or another reverse proxy before the Node process, set top-level trustProxy in settings.js or settings.ts:
export default {
trustProxy: 1,
};This is the AegisNode equivalent of app.set('trust proxy', 1) in raw Express. It makes req.secure, req.protocol, client IP detection, secure cookies, and HTTPS-aware auth logic behave correctly behind the proxy.
Prefer an exact value such as 1, 'loopback', or a subnet string instead of true.
AegisNode also supports Passenger-style startup using the generated loader.cjs.
Passenger setup (Apache, Nginx, Plesk, cPanel, and similar hosts):
- Set Application Root to your project folder.
- Set Startup File to
loader.cjs. - Install dependencies in the project root, for example
npm install --omit=dev. - Set environment variables. At minimum, make sure the resolved app env is production and let Passenger manage
PORT. - Restart the Node app from the hosting panel or service manager.
Plesk note: these map to Application Root and Application Startup File fields.
HTTPS note:
- If TLS is terminated by Passenger, Apache, or Nginx, keep
https.enabledoff and usetrustProxy. - Only enable
httpsinsettings.jsorsettings.tswhen Node itself should serve TLS directly.
How it works:
loader.cjsimportsapp.jsin JavaScript projects orapp.tsin TypeScript projects.app.jsorapp.tsstarts AegisNode with project root resolved from its own file location, so the same entry works under process managers and hosting panels.
Maintenance Mode
Enable maintenance mode in settings.js or settings.ts to serve a maintenance route with 503 Service Unavailable.
If that route is missing or does not respond, AegisNode renders its internal maintenance fallback view.
export default {
maintenance: {
enabled: true,
route: '/maintenance',
excludePaths: ['/health'],
retryAfter: 120,
},
};export default {
register(route) {
route.get('/maintenance', (req, res) => {
res.render('maintenance', {
title: 'Scheduled maintenance',
});
});
},
};Notes:
maintenance.routeis internally rewritten, so requests like/userscan display your maintenance page without a redirect.- If
maintenance.routeis not defined, or the route does not answer, the bundled fallback view is rendered. excludePathslets selected endpoints keep running during maintenance.retryAftersets the HTTPRetry-Afterheader.maintenance: trueuses the built-in default maintenance page.maintenance: '<html>...</html>'is still accepted as a shorthand for direct custom HTML.
File Uploads
AegisNode provides built-in upload middleware on route API as route.upload.
Storage location:
- Default folder:
<project-root>/uploads - Change with
settings.uploads.dir(relative or absolute path)
Recommended upload settings:
uploads: {
enabled: true,
dir: 'storage/uploads',
createDir: true,
preserveExtension: true,
maxFileSize: '5mb',
maxFiles: 5,
maxFields: 50,
maxFieldSize: '1mb',
allowedMimeTypes: ['image/png', 'image/jpeg'],
allowedExtensions: ['.png', '.jpg', '.jpeg'],
allowApiMultipart: true,
},Route middleware modes:
import UsersView from './views.js';
export default {
appName: 'users',
register(route) {
// One file -> req.file
route.post('/avatar', route.upload.single('avatar'), UsersView.uploadAvatar);
// Many files from one input name -> req.files (array)
route.post('/gallery', route.upload.array('photos', 6), UsersView.uploadGallery);
// Many named file inputs -> req.files.<fieldName> (array)
route.post(
'/documents',
route.upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'docs', maxCount: 3 },
]),
UsersView.uploadDocuments,
);
// Accept all file fields -> req.files (array)
route.post('/any-upload', route.upload.any(), UsersView.uploadAny);
// No files, parse multipart text fields only
route.post('/multipart-no-file', route.upload.none(), UsersView.multipartNoFile);
},
};req payload shape:
single():req.file+req.bodyarray():req.files(array) +req.bodyfields():req.filesobject (req.files.avatar,req.files.docs, ...) +req.body
Custom route with form fields + file:
// apps/users/routes.js
import UsersView from './views.js';
export default {
appName: 'users',
register(route) {
route.post('/profile/update', route.upload.single('avatar'), UsersView.updateProfile);
},
};// apps/users/views.js
class UsersView {
static updateProfile(_context, req, res) {
const { username, bio } = req.body;
const avatar = req.file || null;
return res.json({
username,
bio,
avatar: avatar ? {
name: avatar.filename,
originalName: avatar.originalname,
mimeType: avatar.mimetype,
size: avatar.size,
path: avatar.path,
} : null,
});
}
}
export default UsersView;<form action="/users/profile/update" method="POST" enctype="multipart/form-data">
<%= csrfToken %>
<input name="username" />
<textarea name="bio"></textarea>
<input type="file" name="avatar" />
<button type="submit">Save</button>
</form>Upload limits and rejections:
- Per-file size limit from
uploads.maxFileSizereturns413when exceeded. - Total files limit from
uploads.maxFilesreturns413when exceeded. allowedMimeTypes/allowedExtensionsmismatch returns415.
Important behavior:
- If
uploads.enabled=false, usingroute.upload.*throws at route registration. - For API mounts, multipart is allowed only when
uploads.allowApiMultipart=true. - For non-API form submissions, CSRF token is required by default.
API Apps
api does not create a separate app type. You still build a normal AegisNode app with routes.js, views.js, services.js, and validators.js.
The api setting only changes middleware behavior for selected app mounts.
Think of it this way:
apicontrols request/response behavior for an app mount.authcontrols who can access routes and how tokens are issued/verified.- You can use
apiwithout auth, auth withoutapi, or both together.
Common combinations:
apionly: public or internal JSON endpoints with no token auth.api+ JWT: first-party SPA/mobile/frontend calling your own backend.api+ OAuth2: third-party clients, machine-to-machine access, or standards-based authorization flows.
Quick start:
- Declare the app in
settings.appsand give it a mount. - Add that app name to
api.apps. - Mount the app at the same path in
routes.jswhenautoMountAppsis off. - Return JSON from your handlers.
- Send JSON for unsafe methods unless you intentionally allow multipart uploads.
Example settings.js:
export default {
apps: [
{ name: 'users', mount: '/users' },
],
api: {
apps: ['users'],
disableCsrf: true,
requireJsonForUnsafeMethods: true,
noStoreHeaders: true,
},
};Example root routes.js:
import users from './apps/users/routes.js';
export default {
register(route) {
route.use('/users', users); // keep this aligned with settings.apps[].mount
},
};Example apps/users/routes.js:
import UsersView from './views.js';
export default {
appName: 'users',
register(route) {
route.get('/', UsersView.index);
route.post('/', UsersView.create);
},
};Example apps/users/views.js:
class UsersView {
static async index({ service }, req, res, next) {
try {
const users = await service.list();
res.json({ data: users });
} catch (error) {
next(error);
}
}
static async create({ service, validator }, req, res, next) {
try {
const payload = validator.create(req.body || {});
const created = await service.create(payload);
res.status(201).json({ data: created });
} catch (error) {
next(error);
}
}
}
export default UsersView;Example requests:
curl http://127.0.0.1:3000/users
curl -X POST http://127.0.0.1:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice"}'What the API middleware changes:
POST,PUT,PATCH, andDELETEwith a request body must useapplication/jsonwhenrequireJsonForUnsafeMethods: true.multipart/form-datais still allowed for API mounts whenuploads.allowApiMultipart: true.- CSRF is skipped only for configured API app mounts when
disableCsrf: true. - API responses get
Cache-Control: no-storewhennoStoreHeaders: true.
What it does not change:
- It does not auto-generate CRUD endpoints.
- It does not force a separate
controllers/orapi/folder. - It does not convert a view into JSON automatically; your handler still decides what to return.
API And Auth Together
api and auth are separate features that are often used together:
apimakes an app behave like an API mount: JSON body enforcement, optional CSRF skip, andCache-Control: no-store.authadds token issuance, token verification, route protection, client registration, and revocation/introspection behavior.
Examples:
- Public JSON API: enable
api, leaveauth.enabledoff. - Protected JSON API for your own frontend/mobile app: enable
apiandauth.provider = 'jwt'. - Protected partner/developer API: enable
apiandauth.provider = 'oauth2'.
Rule of thumb:
- If you only need JSON routes, use
api. - If you need authenticated access, add
auth. - If outside clients need a standard auth protocol, choose OAuth2 instead of rolling custom JWT login flows.
Quick comparison:
| Setup | Use it when | What you configure |
| --- | --- | --- |
| api only | You need JSON endpoints without token auth. Good for public read APIs or trusted internal services. | api.apps = [...] |
| api + JWT | Your own frontend/mobile app talks to your backend and you control both sides. | api.apps = [...], auth.enabled = true, auth.provider = 'jwt', plus your own login/token routes |
| api + OAuth2 | External clients, partner apps, or machine clients need standard token flows. | api.apps = [...], auth.enabled = true, auth.provider = 'oauth2' |
Database Config
Use database.config for every dialect (SQL and MongoDB/Mongoose):
database: {
enabled: true,
dialect: 'pg', // pg | mysql | mssql | sqlite | oracle | mongo | mongodb | mongoose
config: {
// SQL example:
server: 'localhost',
port: 5432,
user: 'postgres',
password: 'postgres',
database: 'appdb',
// Mongo example:
// connectionString: 'mongodb://localhost:27017/appdb',
// or:
// server: 'localhost',
// port: 27017,
// database: 'appdb',
// user: 'mongo_user',
// password: 'mongo_pass',
},
options: {},
},Legacy note:
database.uriis still accepted for MongoDB, butdatabase.config.connectionStringis preferred.
Model usage for mongo / mongodb / mongoose:
dbClientis a QueryMesh client, so you can use the same fluent API as SQL models.
class UsersModel {
constructor({ dbClient }) {
this.db = dbClient;
}
async list() {
return this.db.table('users').select(['id', 'name']).get();
}
}Mongo _id / ObjectId handling:
- Use built-in helper
helpers.toObjectId(...)before filtering on_id. - Validate with
helpers.isObjectId(...)when needed. - Keep this conversion in model/service layer.
- If your collection stores string
_idvalues (not native MongoObjectId), skip conversion.
class UsersModel {
constructor({ dbClient, helpers }) {
this.db = dbClient;
this.helpers = helpers;
}
async findById(id) {
const _id = this.helpers.toObjectId(id);
if (!_id) throw new Error('Invalid Mongo ObjectId');
return this.db.table('users').where('_id', '=', _id).first();
}
}Environment Overrides (Single settings.js)
You can keep a single settings.js and define per-environment overrides:
export default {
env: process.env.NODE_ENV || 'development',
logging: {
level: 'info',
},
security: {
ddos: {
maxRequests: 300,
},
},
environments: {
development: {
logging: { level: 'debug' },
},
production: {
logging: { level: 'warn' },
security: { ddos: { maxRequests: 80 } },
},
},
};Behavior:
- Base config is loaded first.
environments.defaultis applied (if present).environments[env]is applied last.envcomes fromsettings.env(fallback:NODE_ENV, thendevelopment).
Auth (JWT Or OAuth2)
auth is independent from api.
You can protect normal web routes, API routes, or both.
If your app is already listed in api.apps, adding auth simply means those JSON routes can now require tokens.
Choose the provider based on who is calling your app:
provider: 'jwt'Best for first-party apps you control. You create your own login/token/refresh/logout routes and callauth.issue(...)yourself.provider: 'oauth2'Best when you need a standard authorization server. AegisNode mounts/oauth/*endpoints for you and supportsauthorization_code+ PKCE,client_credentials, andrefresh_token.
Quick decision guide:
- Use JWT when your own frontend/mobile app talks only to your backend.
- Use OAuth2 when external clients, partner apps, or machine-to-machine integrations need standard token flows.
- Use
auth.middleware()to protect routes in both modes.
Enable auth in settings.js:
auth: {
enabled: true,
provider: 'jwt', // or 'oauth2'
tablePrefix: 'aegisnode',
storage: {
// cache | memory | file | database
driver: 'cache',
filePath: 'storage/aegisnode-auth-store.json',
// Used by database driver for both SQL table and Mongo collection.
tableName: 'aegisnode_auth_store',
},
jwt: {
secret: 'replace-with-strong-secret',
algorithm: 'HS256',
expiresIn: '15m',
refreshExpiresIn: '7d',
issuer: 'blog',
audience: 'blog',
},
oauth2: {
accessTokenTtlSeconds: 3600,
refreshTokenTtlSeconds: 1209600,
authorizationCodeTtlSeconds: 600,
rotateRefreshToken: true,
requireClientSecret: true,
requirePkce: true,
allowPlainPkce: false,
grants: ['authorization_code', 'refresh_token', 'client_credentials'],
defaultScopes: [],
clientAuthMethod: 'client_secret_basic',
server: {
enabled: true,
basePath: '/oauth',
authorizePath: '/oauth/authorize',
tokenPath: '/oauth/token',
introspectionPath: '/oauth/introspect',
revocationPath: '/oauth/revoke',
metadataPath: '/.well-known/oauth-authorization-server',
issuer: '',
autoApprove: true,
requireAuthenticatedUser: true,
requireConsent: false,
allowHttp: false,
},
},
},tablePrefix is used for auth storage names when enabled:
${tablePrefix}_users${tablePrefix}_jwt_revocations${tablePrefix}_oauth_clients${tablePrefix}_oauth_authorization_codes${tablePrefix}_oauth_access_tokens${tablePrefix}_oauth_refresh_tokens
By default, these names are used as key namespaces.
- With
storage.driver = 'cache'ormemory, they are in-memory/cache key prefixes. - With
storage.driver = 'database', they are prefixes stored insideauth.storage.tableName(used as SQL table name or Mongo collection name).
Restart behavior:
auth.enabled = true: auth manager is available in context after restart.auth.enabled = false: auth manager stays safe;auth.middleware()returns503instead of crashing boot.auth.storage.driver = 'file': OAuth2 clients/tokens and JWT revocations persist across restarts.auth.storage.driver = 'database': OAuth2 clients/tokens and JWT revocations persist in your configureddatabasebackend.
JWT usage in routes:
- JWT does not create login routes for you.
- You define the endpoints that authenticate users and issue tokens.
- This is usually the simplest choice for a private API used only by your own frontend/mobile app.
export default {
register(route) {
const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
route.get('/auth/token', (req, res) => {
const token = req.aegis.auth.issue({ subject: 'u1', scope: ['read:users'] });
res.json({ token });
});
route.get('/auth/me', authGuard, (req, res) => {
res.json({ user: req.auth });
});
},
};OAuth2 built-in authorization server endpoints (when auth.provider='oauth2' and auth.oauth2.server.enabled=true):
GET /oauth/authorizePOST /oauth/authorizePOST /oauth/tokenPOST /oauth/introspectPOST /oauth/revokeGET /.well-known/oauth-authorization-server
Flows supported:
authorization_code(with PKCE)client_credentialsrefresh_token
OAuth2 is the better choice when:
- you need standards-based client registration and token exchange,
- you need machine clients as well as browser/mobile clients,
- or third parties must integrate without depending on your custom JWT login route shape.
Route Usage (JWT vs OAuth2)
startproject gives you one root route file: routes.js.
All your custom HTTP routes are defined there (or in app routes you mount with route.use(...)).
// routes.js
import users from './apps/users/routes.js';
export default {
register(route) {
route.use('/users', users);
// Your custom auth/business routes
route.post('/auth/login', (req, res) => {
const token = req.aegis.auth.issue({ subject: 'u1' });
res.json({ token });
});
},
};How this behaves:
provider: 'jwt': Aegis does not create JWT endpoints automatically. You define login/token/refresh/logout routes yourself inroutes.js(or mounted app routes).provider: 'oauth2': Aegis auto-mounts OAuth2 server endpoints (/oauth/authorize,/oauth/token,/oauth/introspect,/oauth/revoke, metadata). You only define your own extra routes (for example admin client setup, protected APIs, business routes).- Do not reuse built-in OAuth2 endpoint paths for your own handlers when OAuth2 server is enabled.
Typical setup patterns:
- API + JWT:
api.apps = ['users'],auth.provider = 'jwt', custom/auth/login, protect/users/*withreq.aegis.auth.middleware(). - API + OAuth2:
api.apps = ['users'],auth.provider = 'oauth2', use built-in/oauth/token, protect/users/*withreq.aegis.auth.middleware(). - Web app + JWT:
no
apiblock required if routes are normal form/web routes, but you can still use JWT for selected endpoints.
OAuth2 Full Usage
- Register clients (server-side only)
Register clients programmatically with auth.registerClient(...).
Do not expose this publicly in production without admin protection.
export default {
register(route) {
route.post('/admin/oauth/setup-clients', (req, res) => {
const webClient = req.aegis.auth.registerClient({
clientId: 'web',
clientSecret: 'secret',
redirectUris: ['https://client.example.com/callback'],
grants: ['authorization_code', 'refresh_token'],
scopes: ['read:users'],
});
const machineClient = req.aegis.auth.registerClient({
clientId: 'machine',
clientSecret: 'machine-secret',
grants: ['client_credentials'],
scopes: ['read:users'],
});
res.json({ webClient, machineClient });
});
},
};Notes:
- Secret is stored hashed (scrypt).
- Returned client object does not include the secret/hash.
authorization_codeclients must have at least oneredirectUri.
- Authorization Code + PKCE flow
Create PKCE verifier/challenge:
import crypto from 'crypto';
function b64url(buffer) {
return Buffer.from(buffer).toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
const codeVerifier = b64url(crypto.randomBytes(48));
const codeChallenge = b64url(crypto.createHash('sha256').update(codeVerifier).digest());Redirect user-agent to authorize endpoint:
GET /oauth/authorize
?response_type=code
&client_id=web
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&scope=read%3Ausers
&state=abc123
&code_challenge=<CODE_CHALLENGE>
&code_challenge_method=S256The server redirects back to:
https://client.example.com/callback?code=<AUTH_CODE>&state=abc123Exchange code for tokens:
curl -X POST http://127.0.0.1:3000/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u web:secret \
-d "grant_type=authorization_code" \
-d "code=<AUTH_CODE>" \
-d "redirect_uri=https://client.example.com/callback" \
-d "code_verifier=<CODE_VERIFIER>"Response shape:
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:users",
"refresh_token": "...",
"refresh_expires_in": 1209600
}- Protect API routes with OAuth2 access tokens
export default {
register(route) {
const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
route.get('/users/me', authGuard, (req, res) => {
res.json({
sub: req.auth.sub || null,
clientId: req.auth.clientId,
scope: req.auth.scope,
});
});
},
};Use token:
curl http://127.0.0.1:3000/users/me \
-H "Authorization: Bearer <ACCESS_TOKEN>"- Refresh token flow
curl -X POST http://127.0.0.1:3000/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u web:secret \
-d "grant_type=refresh_token" \
-d "refresh_token=<REFRESH_TOKEN>"- Client Credentials flow (machine-to-machine)
curl -X POST http://127.0.0.1:3000/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u machine:machine-secret \
-d "grant_type=client_credentials" \
-d "scope=read:users"This returns access token only (no refresh token).
- Introspection and revocation
Introspection:
curl -X POST http://127.0.0.1:3000/oauth/introspect \
-H "Content-Type: application/x-www-form-urlencoded" \
-u web:secret \
-d "token=<ACCESS_TOKEN>"Revocation:
curl -X POST http://127.0.0.1:3000/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-u web:secret \
-d "token=<ACCESS_OR_REFRESH_TOKEN>"- Custom subject/consent resolution
By default, /oauth/authorize resolves subject from:
req.user.idreq.user.subreq.auth.subsubject/user_idquery/body params
You can override with hooks in settings.js:
auth: {
provider: 'oauth2',
oauth2: {
server: {
resolveSubject: ({ req }) => req.user?.id || '',
resolveConsent: ({ req, client, subject }) => {
// return true to approve, false to deny
return true;
},
},
},
},- Production checklist
- Keep
auth.oauth2.server.allowHttp = false(default). - Use HTTPS with trusted reverse proxy config.
- Keep
requirePkce = true. - Use strong client secrets and rotate regularly.
- Restrict client setup endpoints to admins only.
- Set explicit
defaultScopesand per-client scopes. - Set
issuerto your public auth server URL.
Implementation notes:
- OAuth2 server in AegisNode is framework-native (custom implementation).
- CSRF checks are skipped for OAuth2 server endpoints (
/oauth/*+ metadata) by design. - This is OAuth2 (not OpenID Connect); no
id_tokenendpoint/flow.
Swagger (OpenAPI UI)
Enable Swagger in settings.js:
swagger: {
enabled: true,
docsPath: '/docs',
jsonPath: '/openapi.json',
documentPath: 'openapi.json',
explorer: true,
},Behavior:
- UI available at
docsPath(default/docs). - OpenAPI JSON available at
jsonPath(default/openapi.json). - If
openapi.jsonexists in project root, it is loaded. - If no file is found, AegisNode serves a default minimal OpenAPI document.
Templates (EJS + base.ejs)
Set template config in settings.js:
templates: {
enabled: true,
engine: 'ejs',
dir: 'templates',
base: 'base',
appBases: {
users: 'users/base',
admin: 'admin/base',
},
}Then in a route handler:
route.get('/', (req, res) => {
res.render('home', {
title: 'Home',
message: 'Welcome',
});
});home.ejs is rendered first, then injected into base.ejs.
Use <%- content %> (or <%- body %>) in your base.ejs to print page content.
Internationalization (i18n)
Configure i18n in settings.js:
i18n: {
enabled: true,
defaultLocale: 'en',
fallbackLocale: 'en',
supported: ['en', 'fr'],
queryParam: 'lang',
translations: {
en: {
home: {
title: 'Welcome {name}',
},
},
fr: {
home: {
title: 'Bienvenue {name}',
},
},
},
}You can also load locale JSON files directly (no import needed):
i18n: {
enabled: true,
defaultLocale: 'en',
supported: ['en', 'fr'],
translations: {
en: 'locales/en.json',
fr: 'locales/fr.json',
},
// optional single-file source (inline `translations` wins per locale key):
// translationsFile: 'locales/all.json',
}Route usage:
route.get('/i18n-demo', (req, res) => {
// Auto-detected from query/cookie/header.
res.json({
locale: req.aegis.locale,
title: req.aegis.t('home.title', { name: 'Jason' }),
});
});Choosing the API:
req.aegis.t('home.title')Shortcut forreq.aegis.i18n.t('home.title'). Use this in routes/views when you only need a translated string.req.aegis.i18nRequest-scoped i18n object. Use this when you also need locale metadata or helpers such aslocale,localeSource,setLocale(...),resolveLocale(...), orforLocale(...).- Injected
i18nin handlers/services/models/validators/controllers/subscribers/loaders Runtime-injected i18n bridge. During an HTTP request,i18n.t(...)resolves with the same active locale asreq.aegis.i18n.t(...), so the translation result is the same.
Important differences:
req.aegis.tandreq.aegis.i18n.treturn the same translation for the current request.- Injected
i18n.t(...)in a service/model/validator/subscriber is not the same object asreq.aegis.i18n, but during a request it produces the same translation result for the same key/options. - Outside a request, injected
i18n.t(...)falls back todefaultLocale. - In background jobs, loaders, or boot-time code, pass an explicit locale when needed:
i18n.t('home.title', { name: 'Jason' }, { locale: 'fr' }).
Service/model usage:
class UsersService {
constructor({ i18n }) {
this.i18n = i18n;
}
greeting(name) {
return this.i18n.t('home.title', { name });
}
}Template usage:
<html lang="<%= locale %>">
<body>
<h1><%= t('home.title', { name: 'Jason' }) %></h1>
</body>
</html>Manual locale switch inside a request:
route.get('/fr', (req, res) => {
req.aegis.setLocale('fr'); // persists in i18n cookie by default
res.send(req.aegis.t('home.title', { name: 'Jason' }));
});Persist user-selected language as default:
route.post('/lang', (req, res) => {
const selected = String(req.body?.lang || '').trim();
req.aegis.setLocale(selected); // writes i18n cookie (aegis_locale by default)
res.redirect('back');
});Language picker template (keeps selected option):
<form method="post" action="/lang">
<select name="lang">
<option value="en" <%= locale === 'en' ? 'selected' : '' %>>English</option>
<option value="fr" <%= locale === 'fr' ? 'selected' : '' %>>Français</option>
</select>
<button type="submit">Change</button>
</form>Notes:
defaultLocaleis used only when user has no saved locale.- After selection, cookie locale becomes the default for that user on next requests.
?lang=fralso persists automatically whendetectFromQueryis enabled.- Templates get
t,locale, andi18nin locals.
Configure mail transport in settings.js:
export default {
mail: {
enabled: true,
defaults: {
from: '[email protected]',
replyTo: '[email protected]',
},
transport: {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 587,
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
verifyOnStartup: true,
},
};Available mail APIs:
- Injected
mailin handlers/services/models/validators/controllers/subscribers/loaders Shared runtime mail manager. Usemail.send(...)ormail.sendMail(...). req.aegis.mailRequest bridge to the same mail manager used in handler context.
Handler usage:
route.post('/contact', async ({ mail }, req, res, next) => {
try {
const info = await mail.send({
to: '[email protected]',
subject: 'Contact form',
text: req.body?.message || '',
html: `<p>${req.body?.message || ''}</p>`,
});
res.status(202).json({ messageId: info.messageId });
} catch (error) {
next(error);
}
});Service usage:
class UsersService {
constructor({ mail }) {
this.mail = mail;
}
async sendWelcome(user) {
return this.mail.send({
to: user.email,
subject: 'Welcome',
html: `<h1>Hello ${user.name}</h1><p>Your account is ready.</p>`,
});
}
}Notes:
mail.send(...)andmail.sendMail(...)are the same method.- Messages must include at least one of
to,cc, orbcc. - Messages must include
from, or configuremail.defaults.from. - For tests or custom providers, you can set
mail.transporterormail.transportFactoryinstead ofmail.transport.
Helpers And jlive
Helpers and the jlive bridge are available in request context (req.aegis) and in EJS locals.
They are also available in:
- Service constructors (
constructor({ helpers, jlive, env, i18n, models, ... })) - Model constructors (
constructor({ helpers, jlive, env, i18n, dbClient, ... })) - Subscribers context (
registerSubscribers({ helpers, jlive, env, i18n, events, ... })) - Any view/handler via request bridge:
req.aegis.helpers,req.aegis.jlive,req.aegis.env,req.aegis.locale,req.aegis.t,req.aegis.i18n
Set helper defaults in settings.js (currency/locale):
helpers: {
locale: 'fr-FR',
money: {
currency: 'EUR',
currencyDisplay: 'code',
},
},Then helpers.money(2500) automatically uses your configured defaults unless you pass per-call overrides.
Route usage:
export default {
register(route) {
route.get('/tools', (req, res) => {
res.json({
price: req.aegis.helpers.money(1299.5, { currency: 'USD' }),
createdAgo: req.aegis.helpers.timeElapsed(Date.now() - 60_000),
createdAgoShort: req.aegis.helpers.timeElapsed(Math.floor(Date.now() / 1000) - 60, true),
progress: req.aegis.helpers.timeDifference(65, 0, 100),
summary: req.aegis.helpers.breakStr('AegisNode framework helper utilities', 18, '...', true),
objectIdValid: req.aegis.helpers.isObjectId('507f1f77bcf86cd799439011'),
objectIdString: req.aegis.helpers.toObjectId('507f1f77bcf86cd799439011')?.toString() || null,
secret: req.aegis.jlive.generate(32),
jliveAvailable: req.aegis.jlive.available,
});
});
},
};Template usage (res.render(...)):
<h1><%= title %></h1>
<p>Total: <%= money(1299.5, { currency: 'USD' }) %></p>
<p>Updated: <%= timeElapsed(updatedAt) %></p>
<p>Progress: <%= timeDifference(65, 0, 100) %>%</p>
<p>Summary: <%= breakStr("AegisNode framework helper utilities", 18, "...", true) %></p>
<p>With helper object: <%= helpers.number(1000000) %></p>Available EJS locals:
helpersjlivemoneynumberdateTimetimeElapsedtimeDifferencebreakStrisObjectIdtoObjectIdlocaletcsrfToken(raw hidden input HTML)csrfValue(token string)
timeElapsed supports both styles:
timeElapsed(value, { now, locale, numeric })(Intl relative style)timeElapsed(unixTime, true)(short legacy-style mode)
Mongo id helpers:
isObjectId(value)validates Mongo ObjectId format.toObjectId(value)returns a Mongo ObjectId instance ornullwhen invalid.
jlive behavior:
- If
jlivepackage is installed, bridge uses its methods. - If not installed,
jlive.generate()still works (crypto fallback), while crypto methods throwJLIVE_UNAVAILABLE.
Template Locals From Settings
You can inject custom functions/classes into all template renders from settings.js:
templates: {
enabled: true,
engine: 'ejs',
dir: 'templates',
base: 'base',
locals: {
formatCurrency: (value) => '$' + Number(value || 0).toFixed(2),
ViewBag: class ViewBag {
constructor(title) {
this.title = title;
}
},
},
},You can also pass already-defined class/function references by name (object shorthand):
import { formatCurrency, ViewBag } from './app/template-locals.js';
export default {
templates: {
locals: { formatCurrency, ViewBag },
},
};Note:
- Use JS references/imports (as above).
- String names like
{ formatCurrency: 'formatCurrency' }are not auto-resolved.
Then in EJS:
<p><%= formatCurrency(123.45) %></p>
<p><%= new ViewBag('Dashboard').title %></p>Full Settings Reference
Use this section as a config manual. It is intentionally reference-heavy and is easier to use after you already know which feature or runtime block you need to configure.
All fields below are supported in settings.js. If you omit a field, AegisNode uses the runtime default.
Merge order used at startup:
- Framework defaults (
defaultConfig) settings.js- Legacy
settings/index.js(if present) - Legacy
settings/db.js(merged intodatabase) - Legacy
settings/cache.js(merged intocache) - Legacy
settings/apps.js(used only whensettings.jsdoes not define apps) environments.defaultenvironments[env]whereenv = settings.env(fallbackNODE_ENV, thendevelopment)
Top-Level
| Key | Type / Default | Description |
| --- | --- | --- |
| appName | string / folder name | Application name used in logs and defaults. |
| env | string / process.env.NODE_ENV || 'development' | Active environment key for environments overrides. |
| host | string / process.env.HOST || '0.0.0.0' | Bind host for HTTP server. |
| port | number / process.env.PORT || 3000 | Bind port for HTTP server. |
| trustProxy | boolean \| number \| string / false | Express trust proxy value. Set this when HTTPS is terminated by a reverse proxy/load balancer. |
| https | object \| false / see HTTPS table | Direct TLS server settings for Node-hosted HTTPS. |
| staticDir | string \| null / null | Static assets directory, relative to project root (if set). |
| templates | object \| false / see templates table | EJS template engine + layout settings. |
| i18n | object / see i18n table | Built-in locale detection + translator bridge (req.aegis.t, injected i18n.t). |
| helpers | object / see helpers table | Runtime helper defaults (for example currency/locale for helpers.money). |
| security | object / see security tables | Security headers, DDoS limiter, CSRF settings, app secret. |
| logging | object / { level: 'info' } | Runtime logger level. |
| database | object / see database table | SQL or MongoDB connection settings. |
| cache | object / { enabled: true, driver: 'memory' } | Cache backend settings. |
| websocket | object / { enabled: true, cors: { origin: false } } | Socket.IO server options. |
| uploads | object / see uploads table | Built-in file upload middleware settings used by route.upload. |
| mail | object / see mail table | Nodemailer-backed mail manager available as injected mail and req.aegis.mail. |
| api | object / see API table | API-app middleware behavior (JSON enforcement, no-store, CSRF skip for API mounts). |
| auth | object / see auth tables | JWT or OAuth2 provider settings. |
| swagger | object / see swagger table | OpenAPI JSON + Swagger UI settings. |
| architecture | object / { strictLayers: false } | Layering enforcement mode. |
| autoMountApps | boolean / false | Auto-mount each app route file from settings.apps. |
| loaders | array / [] | Startup loaders run before routes mounting. |
| apps | array / [] | Declared apps with mount points. |
| environments | object / {} | Environment-specific deep overrides. |
Notes:
rootDiris internal and set by runtime; do not manage it manually.- Arrays are replaced (not merged) during deep merge.
HTTPS (https)
Use this only when Node should serve HTTPS directly. If HTTPS is handled by Passenger, Nginx, Apache, or another proxy, keep https.enabled off and set top-level trustProxy instead.
| Key | Type / Default | Description |
| --- | --- | --- |
| enabled | boolean / false | Create an HTTPS server instead of HTTP. |
| key | string \| Buffer / null | TLS private key content. |
| cert | string \| Buffer / null | TLS certificate content. |
| ca | string \| Buffer \| array / null | Optional CA/intermediate certificate content. |
| pfx | string \| Buffer / null | PFX/PKCS#12 archive content. Use instead of key + cert. |
| keyPath | string / '' | Path to TLS private key, relative to project root or absolute. |
| certPath | string / '' | Path to TLS certificate, relative to project root or absolute. |
| caPath | string \| string[] / null | Optional CA/intermediate certificate path(s). |
| pfxPath | string / '' | Path to PFX/PKCS#12 archive. |
| passphrase | string / '' | Optional passphrase for encrypted key/PFX files. |
| options | object / {} | Extra Node https.createServer options (for example minVersion). |
Direct HTTPS example:
export default {
host: '0.0.0.0',
port: 3443,
https: {
enabled: true,
keyPath: 'certs/localhost-key.pem',
certPath: 'certs/localhost-cert.pem',
options: {
minVersion: 'TLSv1.2',
},
},
};Reverse-proxy HTTPS example:
export default {
host: '127.0.0.1',
port: 3000,
trustProxy: 1,
};Notes:
httpsrequires eitherpfx/pfxPathor bothkey/keyPathandcert/certPath.- Paths resolve from project root unless absolute.
trustProxyaffectsreq.secure,req.protocol, secure cookies, and OAuth2 secure transport checks.- Prefer
1, a subnet, or another exact Expresstrust proxyvalue instead oftruewhen rate limiting is enabled.
Templates (templates)
| Key | Type / Default | Description |
| --- | --- | --- |
| enabled | boolean / true | Enable template engine. |
| engine | string / 'ejs' | Template engine. Only ejs is supported. |
| dir | string / 'templates' | Templates folder (absolute or relative to project root). |
| base | string \| false \| null / 'base' | Default layout template (without .ejs). Set false/null to disable layout wrapping globally. |
| appBases | object / {} | Per-app layout override map: { appName: 'layout/name' }. Set app value to false/null to disable layout for that app only. |
| locals | object \| function / {} | Global locals. If function, signature is ({ req, res, helpers, jlive, env }) => object. |
Layout notes:
res.render('view', data)rendersview.ejsand wraps it withbase.ejs(or configured layout).- Per-app layout override:
templates.appBases = { users: 'users/base', admin: 'admin/base' }. - In layout, both
<%- body %>and<%- content %>are available. - Per-render layout override: pass
layout: 'custom-layout'orlayout: falsein locals.
Internationalization (i18n)
| Key | Type / Default | Description |
| --- | --- | --- |
| enabled | boolean / false | Enable built-in i18n translator bridge. |
| defaultLocale | string / 'en' | Default locale used when detection fails. |
| fallbackLocale | string / 'en' | Fallback locale used when key is missing in active locale. |
| supported | string[] / ['en'] | Allowed locales. Values normalize to lowercase (for example en-US -> en-us). |
| queryParam | string / 'lang' | Query parameter used for locale selection (for example ?lang=fr). |
| cookieName | string / 'aegis_locale' | Cookie used to persist locale. |
| detectFromHeader | boolean / true | Enable locale detection from Accept-Language header. |
| detectFromCookie | boolean / true | Enable locale detection from configured cookie. |
| detectFromQuery | boolean / true | Enable locale detection from query parameter. |
| translations | object / {} | Translation map by locale. Values can be objects or JSON file paths: { en: { ... }, fr: 'locales/fr.json' }. Alias keys locales and messages are also accepted. |
| translationsFile | string / unset | Path to a JSON file containing all locales (example: { "en": {...}, "fr": {...} }). Inline translations overrides file values for same locale keys. |
i18n notes:
- Detection order: query -> cookie ->
Accept-Language->defaultLocale. - Use dotted keys like
home.title. - Placeholder interpolation supports
{name}style tokens. - Relative JSON paths resolve from project root (
settings.jslocation). - Injected
i18nis available in handlers, services, models, validators, controllers, subscribers, and loaders. Usei18n.t('key', vars, { locale }). - During a request, injected
i18n.t(...)follows the active request locale. Outside a request, it falls back todefaultLocale.
Helpers Defaults (helpers)
| Key | Type / Default | Description |
| --- | --- | --- |
| locale | string / 'en-US' | Default locale used by runtime helpers when locale is not passed explicitly. |
| money | object / { currency: 'USD' } | Default money formatting settings used by helpers.money. |
| money.currency | string / 'USD' | Default currency code for helpers.money(amount) when no currency option is provided. |
| money.locale | string / helpers.locale | Locale override only for helpers.money. |
| money.currencyDisplay | 'symbol' | 'code' | 'name' | 'narrowSymbol' / 'symbol' | Currency display style passed to Intl.NumberFormat. |
| money.minimumFractionDigits | number / unset | Optional minimum fraction digits for money formatting. |
| money.maximumFractionDigits | number / unset | Optional maximum fraction digits for money formatting. |
Helpers defaults notes:
- Per-call options always override these defaults.
- If
helpers.localeis not set, AegisNode falls back toi18n.defaultLocalefor helper locale. - Legacy shorthand keys are also accepted for compatibility:
helpers.currency, top-levelcurrency, andapp.currency.
Security (security)
| Key | Type / Default | Description |
| --- | --- | --- |
| appSecret | string / '' | Shared secret for signing security artifacts. Use at least 16 chars. |
| headers | object / see headers table | Helmet + CSP configuration. |
| ddos | object / see ddos table | express-rate-limit based protection. |
| csrf | object / see csrf table | CSRF cookie/token behavior. |
Security Headers (security.headers)
| Key | Type / Default | Description |
| --- | --- | --- |
| enabled | boolean / true | Enable Helmet middleware. |
| csp | object / see CSP table | Content Security Policy behavior. |
CSP (security.headers.csp)
| Key | Type / Default | Description |
| --- | --- | --- |
| enabled | boolean / true | Enable CSP header from Helmet. |
| reportOnly | boolean / false | Use report-only mode. |
| directives | object / {} | Directive overrides. Set a directive to false/null to remove it. |
Default CSP base includes safe defaults such as defaultSrc 'self', objectSrc 'none', frameAncestors 'none', and websocket-aware connectSrc.
Allow multiple external domains by adding them per directive (not globally):
security: {
headers: {
csp: {
directives: {
scriptSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
scriptSrcElem: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
styleSrcElem: ["'self'", 'https://cdn.jsdelivr.net'],
imgSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
connectSrc: ["'self'", 'https://api.example.com', 'wss://socket.example.com'],
},
},
},
},Notes:
- Add each origin to the exact directive needed (scripts, styles, images, API/WebSocket connections).
- If browser reports a
script-src-elemviolation, whitelist the domain inscriptSrcElem. - Prefer explicit origins instead of
*in production.
Google Fonts example:
security: {
headers: {
csp: {
directives: {
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
styleSrcElem: ["'self'", 'https://fonts.googleapis.com'],
fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
},
},
},
},DDoS / Rate Limit (security.ddos)
| Key | Type / Default | Description |
| --- | --- | --- |
| enabled | boolean / true | Enable rate limiter. |
| windowMs | number / 60000 | Rate limit window in milliseconds. |
| maxRequests | number / 300 | Max requests per window per key. |
| message | string / 'Too many requests, please try again later.' | JSON error message text. |
| statusCode | number / 429 | Response status code
