npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

AegisNode

AegisNode Banner

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:

  1. Quick Start: get a project created, installed, and running.
  2. Core Concepts And App Structure: understand where code lives and how layers fit together.
  3. Common Tasks And Feature Guides: jump to the feature you need once the basics are clear.
  4. Full Settings Reference: use this when you already know which config block you are looking for.
  5. 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 blog

Create 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 runserver

If 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 users

createapp 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 with aegisnode runserver.
  • Non-development (env !== 'development'): start with node loader.cjs.
  • node app.js and node loader.cjs are blocked in development mode.
  • aegisnode runserver is 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 updatedeps

What 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: restores loader.cjs and app.js when startup files are missing.
  • updatedeps: rewrites package dependency ranges to the current npm latest versions and reinstalls.

Run project tests with:

npm test

JavaScript vs TypeScript Projects

The project type is chosen once at startproject time:

  • aegisnode startproject blog scaffolds a JavaScript project in the current directory
  • aegisnode startproject blog --typescript scaffolds a TypeScript project in the current directory

After that, the rest of the CLI follows the project automatically:

  • createapp generates views.js / services.js / routes.js in JavaScript projects, or views.ts / services.ts / routes.ts in TypeScript projects
  • generate creates artifacts with the same extension as the project, for example profile.view.js or profile.view.ts
  • fix, doctor, and generateloader also 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.js or views.ts
  • models.js or models.ts
  • services.js or services.ts
  • validators.js or validators.ts
  • routes.js or routes.ts
  • subscribers.js or subscribers.ts
  • utils.js or utils.ts
  • tests/

Other scaffold rules to know:

  • createapp auto-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.apps and mounted in the root routes.js or routes.ts.
  • Only apps declared in settings.apps are allowed to load or mount.
  • --mount accepts 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>/:id
    • PUT /<mount>/:id
    • DELETE /<mount>/:id
  • Default app tests generated by createapp are:
    • 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

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/END markers; createapp updates this list automatically.
  • startproject writes a local .env with a generated APP_SECRET and also embeds the same generated secret in settings.js or settings.ts as a fallback.
  • The scaffold already includes staticDir: 'public' and a default templates block, 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.js
  • apps/<app>/models.js
  • apps/<app>/services.js
  • apps/<app>/utils.js
  • apps/<app>/subscribers.js
  • apps/<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). Keep views.js thin: prefer only the view class and its imports. Avoid defining extra local helper/utility functions in the view file. Move reusable pure logic to utils.js and app workflows to services.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.js is a plain module, not an injected runtime layer. If a utility needs jlive, 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 example app.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.js for pure app-local helpers such as string formatting, slug generation, payload shaping, or small mappers.
  • Use services.js for 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 with services.forApp('<app>').get('<name>') or services.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.js or 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 (loaders entry 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):

  1. Set Application Root to your project folder.
  2. Set Startup File to loader.cjs.
  3. Install dependencies in the project root, for example npm install --omit=dev.
  4. Set environment variables. At minimum, make sure the resolved app env is production and let Passenger manage PORT.
  5. 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.enabled off and use trustProxy.
  • Only enable https in settings.js or settings.ts when Node itself should serve TLS directly.

How it works:

  • loader.cjs imports app.js in JavaScript projects or app.ts in TypeScript projects.
  • app.js or app.ts starts 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.route is internally rewritten, so requests like /users can display your maintenance page without a redirect.
  • If maintenance.route is not defined, or the route does not answer, the bundled fallback view is rendered.
  • excludePaths lets selected endpoints keep running during maintenance.
  • retryAfter sets the HTTP Retry-After header.
  • maintenance: true uses 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.body
  • array(): req.files (array) + req.body
  • fields(): req.files object (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.maxFileSize returns 413 when exceeded.
  • Total files limit from uploads.maxFiles returns 413 when exceeded.
  • allowedMimeTypes / allowedExtensions mismatch returns 415.

Important behavior:

  • If uploads.enabled=false, using route.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:

  • api controls request/response behavior for an app mount.
  • auth controls who can access routes and how tokens are issued/verified.
  • You can use api without auth, auth without api, or both together.

Common combinations:

  • api only: 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:

  1. Declare the app in settings.apps and give it a mount.
  2. Add that app name to api.apps.
  3. Mount the app at the same path in routes.js when autoMountApps is off.
  4. Return JSON from your handlers.
  5. 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, and DELETE with a request body must use application/json when requireJsonForUnsafeMethods: true.
  • multipart/form-data is still allowed for API mounts when uploads.allowApiMultipart: true.
  • CSRF is skipped only for configured API app mounts when disableCsrf: true.
  • API responses get Cache-Control: no-store when noStoreHeaders: true.

What it does not change:

  • It does not auto-generate CRUD endpoints.
  • It does not force a separate controllers/ or api/ 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:

  • api makes an app behave like an API mount: JSON body enforcement, optional CSRF skip, and Cache-Control: no-store.
  • auth adds token issuance, token verification, route protection, client registration, and revocation/introspection behavior.

Examples:

  • Public JSON API: enable api, leave auth.enabled off.
  • Protected JSON API for your own frontend/mobile app: enable api and auth.provider = 'jwt'.
  • Protected partner/developer API: enable api and auth.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.uri is still accepted for MongoDB, but database.config.connectionString is preferred.

Model usage for mongo / mongodb / mongoose:

  • dbClient is 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 _id values (not native Mongo ObjectId), 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.default is applied (if present).
  • environments[env] is applied last.
  • env comes from settings.env (fallback: NODE_ENV, then development).

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 call auth.issue(...) yourself.
  • provider: 'oauth2' Best when you need a standard authorization server. AegisNode mounts /oauth/* endpoints for you and supports authorization_code + PKCE, client_credentials, and refresh_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' or memory, they are in-memory/cache key prefixes.
  • With storage.driver = 'database', they are prefixes stored inside auth.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() returns 503 instead 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 configured database backend.

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/authorize
  • POST /oauth/authorize
  • POST /oauth/token
  • POST /oauth/introspect
  • POST /oauth/revoke
  • GET /.well-known/oauth-authorization-server

Flows supported:

  • authorization_code (with PKCE)
  • client_credentials
  • refresh_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 in routes.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/* with req.aegis.auth.middleware().
  • API + OAuth2: api.apps = ['users'], auth.provider = 'oauth2', use built-in /oauth/token, protect /users/* with req.aegis.auth.middleware().
  • Web app + JWT: no api block required if routes are normal form/web routes, but you can still use JWT for selected endpoints.

OAuth2 Full Usage

  1. 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_code clients must have at least one redirectUri.
  1. 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=S256

The server redirects back to:

https://client.example.com/callback?code=<AUTH_CODE>&state=abc123

Exchange 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
}
  1. 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>"
  1. 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>"
  1. 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).

  1. 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>"
  1. Custom subject/consent resolution

By default, /oauth/authorize resolves subject from:

  • req.user.id
  • req.user.sub
  • req.auth.sub
  • subject/user_id query/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;
      },
    },
  },
},
  1. 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 defaultScopes and per-client scopes.
  • Set issuer to 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_token endpoint/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.json exists 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 for req.aegis.i18n.t('home.title'). Use this in routes/views when you only need a translated string.
  • req.aegis.i18n Request-scoped i18n object. Use this when you also need locale metadata or helpers such as locale, localeSource, setLocale(...), resolveLocale(...), or forLocale(...).
  • Injected i18n in handlers/services/models/validators/controllers/subscribers/loaders Runtime-injected i18n bridge. During an HTTP request, i18n.t(...) resolves with the same active locale as req.aegis.i18n.t(...), so the translation result is the same.

Important differences:

  • req.aegis.t and req.aegis.i18n.t return the same translation for the current request.
  • Injected i18n.t(...) in a service/model/validator/subscriber is not the same object as req.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 to defaultLocale.
  • 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:

  • defaultLocale is used only when user has no saved locale.
  • After selection, cookie locale becomes the default for that user on next requests.
  • ?lang=fr also persists automatically when detectFromQuery is enabled.
  • Templates get t, locale, and i18n in locals.

Mail

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 mail in handlers/services/models/validators/controllers/subscribers/loaders Shared runtime mail manager. Use mail.send(...) or mail.sendMail(...).
  • req.aegis.mail Request 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(...) and mail.sendMail(...) are the same method.
  • Messages must include at least one of to, cc, or bcc.
  • Messages must include from, or configure mail.defaults.from.
  • For tests or custom providers, you can set mail.transporter or mail.transportFactory instead of mail.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:

  • helpers
  • jlive
  • money
  • number
  • dateTime
  • timeElapsed
  • timeDifference
  • breakStr
  • isObjectId
  • toObjectId
  • locale
  • t
  • csrfToken (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 or null when invalid.

jlive behavior:

  • If jlive package is installed, bridge uses its methods.
  • If not installed, jlive.generate() still works (crypto fallback), while crypto methods throw JLIVE_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:

  1. Framework defaults (defaultConfig)
  2. settings.js
  3. Legacy settings/index.js (if present)
  4. Legacy settings/db.js (merged into database)
  5. Legacy settings/cache.js (merged into cache)
  6. Legacy settings/apps.js (used only when settings.js does not define apps)
  7. environments.default
  8. environments[env] where env = settings.env (fallback NODE_ENV, then development)

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:

  • rootDir is 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:

  • https requires either pfx/pfxPath or both key/keyPath and cert/certPath.
  • Paths resolve from project root unless absolute.
  • trustProxy affects req.secure, req.protocol, secure cookies, and OAuth2 secure transport checks.
  • Prefer 1, a subnet, or another exact Express trust proxy value instead of true when 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) renders view.ejs and wraps it with base.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' or layout: false in 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.js location).
  • Injected i18n is available in handlers, services, models, validators, controllers, subscribers, and loaders. Use i18n.t('key', vars, { locale }).
  • During a request, injected i18n.t(...) follows the active request locale. Outside a request, it falls back to defaultLocale.

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.locale is not set, AegisNode falls back to i18n.defaultLocale for helper locale.
  • Legacy shorthand keys are also accepted for compatibility: helpers.currency, top-level currency, and app.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-elem violation, whitelist the domain in scriptSrcElem.
  • 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