JSPM

  • Created
  • Published
  • Downloads 84
  • Score
    100M100P100Q101062F
  • License ISC

NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React integration for scalable full-stack applications.

Package Exports

  • @nitronjs/framework

Readme

NitronJS

A full-stack MVC framework built on Fastify and React Server Components.
Build production-ready Node.js applications with a built-in ORM, authentication, mail, validation, i18n, instant HMR, and a complete CI/CD pipeline.

npm version license node version

NitronJS is currently under active development. APIs may change before the stable release.


Table of Contents


Quick Start

npx -y @nitronjs/framework my-app
cd my-app
npm run storage:link
npm run dev

Your app will be running at http://localhost:3000


Core Concepts

Server Components (Default)

Every .tsx file in resources/views/ is a Server Component by default. They run on the server and have full access to your database and file system.

// resources/views/Site/Home.tsx
import User from '@models/User';

export default async function Home() {
    const users = await User.get();

    return (
        <div>
            <h1>Users</h1>
            <ul>
                {users.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </div>
    );
}

Client Components

Add "use client" at the top of a file to make it interactive. These components hydrate on the browser and can use React hooks.

"use client";
import { useState } from 'react';

export default function Counter() {
    const [count, setCount] = useState(0);

    return (
        <button onClick={() => setCount(count + 1)}>
            Count: {count}
        </button>
    );
}

Client components cannot import server-only modules (the database, DateTime, etc.). Pass server data down as props from a Server Component instead.

Layouts

Create Layout.tsx files to wrap pages. Layouts are discovered automatically by walking up the directory tree. During SPA navigation, layouts persist and only page content is updated — no full page reloads.

resources/views/
├── Layout.tsx             # Root layout (wraps everything)
├── Site/
│   └── Home.tsx           # Uses root Layout
└── Admin/
    ├── Layout.tsx         # Admin layout (nested inside root)
    └── Dashboard.tsx      # Uses both layouts

Routing

Define routes in routes/web.js:

import { Route } from '@nitronjs/framework';
import HomeController from '../app/Controllers/HomeController.js';
import UserController from '../app/Controllers/UserController.js';

// Basic routes
Route.get('/', HomeController.index).name('home');
Route.get('/about', HomeController.about).name('about');

// Route parameters
Route.get('/users/:id', UserController.show).name('user.show');

// RESTful routes
Route.post('/users', UserController.store).name('user.store');
Route.put('/users/:id', UserController.update).name('user.update');
Route.delete('/users/:id', UserController.destroy).name('user.destroy');

// Route groups with prefix, name prefix, and middleware
Route.prefix('/admin').name('admin.').middleware('auth').group(() => {
    Route.get('/', DashboardController.index).name('dashboard');
    Route.get('/users', AdminUserController.index).name('users');

    // Nested groups
    Route.prefix('/pages').name('pages.').group(() => {
        Route.get('/:id/edit', PagesController.edit).name('edit');
    });
});

// Middleware-only groups
Route.middleware('guest').group(() => {
    Route.get('/login', AuthController.getLogin).name('login');
    Route.post('/login', AuthController.postLogin);
});

CSRF protection is applied automatically to the HTTP methods configured in config/session.js — you do not add it to routes manually.

URL Generation

The global route() function is available everywhere — controllers, views, and client-side code:

// Basic
route('home')                                        // => "/"

// With parameters
route('user.show', { id: 1 })                        // => "/users/1"

// With query string
route('admin.users', {}, { page: 2, q: 'search' })   // => "/admin/users?page=2&q=search"

// Parameters + query string
route('admin.pages.edit', { id: 5 }, { tab: 'seo' }) // => "/admin/pages/5/edit?tab=seo"

Controllers

Controllers are plain classes with static async route handlers. Each handler receives req and res.

// app/Controllers/UserController.js
import User from '../Models/User.js';

class UserController {
    static async index(req, res) {
        const users = await User.get();

        return res.view('User/Index', {
            users
        });
    }

    static async show(req, res) {
        const user = await User.find(req.params.id);

        if (!user) {
            return res.code(404).view('Errors/NotFound');
        }

        return res.view('User/Show', {
            user
        });
    }

    static async store(req, res) {
        const user = new User();
        user.name = req.body.name;
        user.email = req.body.email;
        await user.save();

        return res.redirect(route('user.show', { id: user.id }));
    }
}

export default UserController;

Path Aliases

NitronJS provides built-in path aliases for clean imports in .tsx view files:

Alias Path
@models/* app/Models/*
@controllers/* app/Controllers/*
@middlewares/* app/Middlewares/*
@views/* resources/views/*
@css/* resources/css/*
@/* Project root

Request & Response

The req object

req is the Fastify request, with req.session and req.auth added by the framework.

Property / Method Description
req.params Route parameters (/users/:idreq.params.id)
req.body Parsed request body
req.query Query string parameters
req.headers Request headers
req.cookies Parsed cookies
req.ip Client IP address
req.url / req.method Request URL and HTTP method
req.isMultipart() true for multipart/form-data requests (file uploads)
req.locale Current locale (set by the i18n layer)
req.session Session instance — see Sessions
req.auth Authentication helper — see Authentication

The res object

res is the Fastify reply, with res.view() added by the framework.

Method Description
res.view(name, params = {}) Render a React Server Component view as HTML. Throws 404 if the view does not exist.
res.send(payload) Send a response. Objects/arrays are auto-serialized as JSON.
res.code(statusCode) Set the HTTP status code (chainable).
res.redirect(url) Redirect the client.
res.type(contentType) Set the Content-Type header.
res.header(key, value) Set a response header.
res.setCookie(name, value, options) Set a cookie.
res.sendFile(path) Stream a file as the response.
// HTML view
return res.view('User/Show', { user });

// JSON (object is auto-serialized)
return res.send({ status: 'success', data: user });

// JSON with a status code
return res.code(422).send({ errors: validation.errors() });

// Redirect
return res.redirect(route('home'));

Use res.code(...) to set the status — there is no res.status(). To return JSON, just pass an object to res.send(...) — there is no res.json().


Database

Models

A model is a class extending Model with a static table property. That is the only required configuration — the primary key is always id.

import { Model } from '@nitronjs/framework';

class User extends Model {
    static table = 'users';
}

export default User;

Query Builder

Static query methods are available directly on the model.

// Fetch
const users = await User.get();
const user = await User.find(1);
const first = await User.where('email', 'john@example.com').first();

// Where clauses
await User.where('role', 'admin').get();
await User.where('age', '>=', 18).get();
await User.where({ role: 'admin', active: 1 }).get();
await User.orWhere('role', 'editor').get();
await User.whereIn('id', [1, 2, 3]).get();
await User.whereNotIn('status', ['banned']).get();
await User.whereBetween('age', [18, 65]).get();
await User.whereNot('role', 'guest').get();

// Selecting, ordering, paginating
await User
    .select('id', 'name', 'email')
    .where('active', 1)
    .orderBy('created_at', 'desc')
    .limit(10)
    .offset(20)
    .get();

// Joins & grouping
await User
    .join('posts', 'users.id', '=', 'posts.user_id')
    .groupBy('users.id')
    .get();

// Aggregates
const total = await User.count();
const distinct = await User.countDistinct('email');
const maxAge = await User.max('age');
const minAge = await User.min('age');
const sumScore = await User.sum('score');
const avgScore = await User.avg('score');

// Raw expressions
await User.selectRaw('COUNT(*) as total, role').groupBy('role').get();

// Create
const user = new User();
user.name = 'John';
user.email = 'john@example.com';
await user.save();

// Update
await User.where('id', 1).update({ name: 'Jane' });

// Delete
await User.where('id', 1).delete();

// Serialize an instance to a plain object
const data = user.toJSON();

DB Facade

For queries that don't map cleanly to a single model, use the DB facade directly.

import { DB } from '@nitronjs/framework';

// Query builder against any table
const rows = await DB.table('users').where('active', 1).get();

// Raw SQL with bound parameters
const result = await DB.rawQuery('SELECT * FROM users WHERE id = ?', [1]);

// Raw expression inside a query builder chain
await DB.table('users').select(DB.rawExpr('COUNT(*) as total')).first();

Transactions

DB.transaction() runs a callback inside a database transaction. It commits on success and rolls back if the callback throws.

import { DB } from '@nitronjs/framework';

await DB.transaction(async (trx) => {
    await trx.table('accounts').where('id', 1).update({ balance: 900 });
    await trx.table('accounts').where('id', 2).update({ balance: 1100 });
    await trx.rawQuery('INSERT INTO transfers (amount) VALUES (?)', [100]);
});

The trx object exposes table(), rawQuery(), query(), and execute() — all scoped to the transaction's connection. A transaction has a 30-second default timeout.

Migrations

npm run make:migration create_posts_table

Migrations live in database/migrations/ and use the Schema builder:

import { Schema } from '@nitronjs/framework';

class CreatePostsTable {
    static async up() {
        await Schema.create('posts', (table) => {
            table.id();
            table.string('title');
            table.text('body');
            table.string('slug').unique();
            table.boolean('published').default(false);
            table.json('metadata').nullable();
            table.foreign('user_id').references('id').on('users').onDelete('CASCADE');
            table.timestamp('created_at');
            table.timestamp('updated_at').nullable();
        });
    }

    static async down() {
        await Schema.dropIfExists('posts');
    }
}

export default CreatePostsTable;

Schema Builder Reference

Column types: id(), increments(), bigIncrements(), integer(), bigInteger(), tinyInteger(), smallInteger(), mediumInteger(), float(), double(), decimal(), boolean(), string(), text(), mediumText(), longText(), json(), jsonb(), date(), dateTime(), timestamp(), timestamps(), softDeletes(), binary(), uuid(), enum()

Column modifiers (chainable): .nullable(), .default(value), .unique(), .primary(), .index(), .unsigned(), .autoIncrement(), .comment(text), .after(column), .first(), .change()

Indexes & keys: table.index(columns), table.unique(columns), table.primary(columns), table.foreign(column).references(col).on(table).onDelete(action).onUpdate(action)

Altering tables: table.dropColumn(name), table.dropColumns(...names), table.renameColumn(from, to)

Schema methods: Schema.create(), Schema.table() (alter), Schema.drop(), Schema.dropIfExists(), Schema.rename(), Schema.hasTable(), Schema.hasColumn()

npm run migrate              # Run pending migrations
npm run migrate:safe         # Run migrations after an automatic DB backup
npm run migrate:fresh        # Drop all tables and re-run
npm run migrate:fresh:seed   # Drop, migrate, and seed
npm run migrate:rollback     # Rollback last batch
npm run migrate:status       # Show migration status

Seeders

npm run make:seeder UserSeeder

Seeders live in database/seeders/. Each seeder is a class with a static async run() method:

import { Hash } from '@nitronjs/framework';
import User from '../../app/Models/User.js';

class UserSeeder {
    static async run() {
        const user = new User();
        user.name = 'Admin';
        user.email = 'admin@example.com';
        user.password = await Hash.make('password');
        await user.save();
    }
}

export default UserSeeder;

Run seeders with npm run seed (runs every seeder, alphabetically) or npx njs seed UserSeeder (one seeder). Seeders can call other seeders by importing them and calling .run() directly.


Authentication

Login & Logout

req.auth provides authentication scoped to the default guard.

class AuthController {
    static async postLogin(req, res) {
        const success = await req.auth.attempt({
            email: req.body.email,
            password: req.body.password
        });

        if (!success) {
            return res.view('Auth/Login', {
                error: 'Invalid credentials'
            });
        }

        return req.auth.home();
    }

    static async logout(req, res) {
        await req.auth.logout();

        return res.redirect(route('home'));
    }
}
Method Description
await req.auth.attempt(credentials) Validate credentials and log the user in. Returns boolean.
await req.auth.user() The authenticated user, or null.
req.auth.check() true if a user is authenticated (synchronous).
await req.auth.logout() End the session.
req.auth.home() Redirect to the guard's configured home route.
req.auth.redirect() Redirect to the guard's configured redirect route.
req.auth.guard(name) Same API, scoped to a named guard.

Guards

Guards let one app authenticate multiple user types (e.g. user and admin). They are defined in config/auth.js:

import User from '../app/Models/User.js';
import Admin from '../app/Models/Admin.js';

export default {
    defaults: {
        guard: 'user'
    },
    guards: {
        user: {
            provider: User,
            identifier: 'email',
            home: 'dashboard',
            redirect: 'login'
        },
        admin: {
            provider: Admin,
            identifier: 'username',
            home: 'admin.dashboard',
            redirect: 'admin.login'
        }
    }
};
// Use a specific guard
await req.auth.guard('admin').attempt({ username, password });
req.auth.guard('admin').check();
const admin = await req.auth.guard('admin').user();

Multi-Factor Authentication (MFA)

Each guard exposes a TOTP-based MFA helper at req.auth.mfa. The user's model must have an mfa column (the encrypted secret is stored there).

// 1. Start setup — returns a QR code to show the user
const { qrCode, secret, otpauthUri } = await req.auth.mfa.generate({
    issuer: 'My App',
    label: user.email
});

// 2. Confirm setup with a code from the user's authenticator app
const result = await req.auth.mfa.confirmSetup(req.body.code);
if (result.success) {
    // result.recoveryCodes — 8 single-use codes, show them once
}

// 3. At login time, after password check
const valid = await req.auth.mfa.verify(req.body.code);
const viaRecovery = await req.auth.mfa.verifyRecoveryCode(req.body.code);

// State helpers
await req.auth.mfa.enabled();   // boolean — MFA confirmed for this user
await req.auth.mfa.disable();   // clear MFA data (verify the password yourself first)
req.auth.mfa.isPending();       // session flag: password OK, awaiting MFA code
req.auth.mfa.setPending();
req.auth.mfa.clearPending();

Sessions

req.session is a per-request session instance.

// Read & write
req.session.set('cart_id', 42);
const cartId = req.session.get('cart_id');
const all = req.session.all();

// Flash messages — available only on the next request
req.session.flash('success', 'Profile updated!');
const message = req.session.getFlash('success');

// CSRF tokens
const token = req.session.getCsrfToken();          // current token (generates one if absent)
req.session.generateCsrfToken();                   // force a new token
req.session.verifyCsrfToken(submittedToken);       // timing-safe comparison

// Regenerate the session ID (anti-fixation, e.g. after login)
req.session.regenerate();

// Getters
req.session.id;
req.session.createdAt;

The session driver (none, file, memory, or redis) is configured in config/session.js.


Validation

import { Validator } from '@nitronjs/framework';

const validation = Validator.make(req.body, {
    name: 'required|string|min:2|max:100',
    email: 'required|email',
    password: 'required|string|min:8|confirmed',
    age: 'numeric|min:18',
    avatar: 'file|mimes:png,jpg|max:2097152'
});

if (validation.fails()) {
    return res.code(422).send({
        errors: validation.errors()
    });
}

const clean = validation.validated();
Method Description
Validator.make(data, rules) Build a validator.
validation.fails() / validation.passes() Whether validation failed/passed.
validation.errors() Error messages keyed by field.
validation.validated() The validated subset of the input data.

Middleware

Middleware is a class with a static async handler(req, res) method. Returning a response (res.send, res.redirect, res.view, or an auth redirect) halts the chain; returning nothing lets it continue.

// app/Middlewares/CheckAge.js
class CheckAge {
    static async handler(req, res) {
        if (req.query.age < 18) {
            return res.code(403).send('Access denied');
        }
    }
}

export default CheckAge;

Register middleware aliases in app/Kernel.js:

import Authentication from './Middlewares/Authentication.js';
import CheckAge from './Middlewares/CheckAge.js';

export default {
    routeMiddlewares: {
        'auth': Authentication,
        'check-age': CheckAge
    }
};

Reference middleware by its string alias in routes:

Route.get('/admin', AdminController.index).middleware('auth');

Middleware can receive a parameter — the framework's auth middleware, for example, accepts a guard name: static async handler(req, res, guardName = 'user').


Localization (i18n)

Translation files live in resources/langs/. Each locale is either a single JSON file (tr.json) or a folder of namespaced files (tr/messages.json).

// resources/langs/en.json
{
    "welcome": "Welcome",
    "greeting": "Hello :name",
    "cart": {
        "items": "{count} item|{count} items"
    }
}

Translate with the global __() function (or its alias lang()) — available in views, controllers, and client code:

__('welcome')                          // "Welcome"
__('greeting', { name: 'Burak' })       // "Hello Burak"
__('cart.items', { count: 1 })          // "1 item"
__('cart.items', { count: 5 })          // "5 items"

The Lang class is available for server-side use:

import { Lang } from '@nitronjs/framework';

Lang.get('greeting', { name: 'Burak' });
Lang.has('welcome');                    // boolean
Lang.locale();                          // current locale
Lang.setLocale('tr');                   // change locale for the current request
  • Dot notation drills into nested keys: cart.items.
  • A {count} parameter triggers pluralization on |-separated values.
  • :param and {param} placeholders are both replaced from the params object.
  • An unresolved key returns the key itself, so missing translations are obvious.
  • In dev mode, language files hot-reload on change.

Mail

import { Mail } from '@nitronjs/framework';

await Mail.from('noreply@app.com')
    .to('user@email.com')
    .subject('Welcome!')
    .html('<h1>Hello</h1>')
    .attachment({ filename: 'file.pdf', path: '/path/to/file.pdf' })
    .send();

// Using a view as the email body
await Mail.to('user@email.com')
    .subject('Welcome!')
    .view('emails/welcome', { name: 'Alice' })
    .send();

SMTP credentials are read from the MAIL_* environment variables.


Storage

import { Storage } from '@nitronjs/framework';

await Storage.put(file, 'upload_files', 'image.jpg');
const buffer = await Storage.get('upload_files/image.jpg');
await Storage.delete('upload_files/old.jpg');
await Storage.move('upload_files/a.jpg', 'upload_files/b.jpg');
Storage.exists('upload_files/image.jpg');
Storage.url('upload_files/image.jpg');  // => "/storage/upload_files/image.jpg"

Pass true as the last argument to get/put/delete/move/exists to operate on private storage (storage/app/private/), which is not web-accessible.


Views (Manual Rendering)

Controllers normally render with res.view(). To render a view to an HTML string yourself — for emails, previews, or background jobs — use the View class:

import { View } from '@nitronjs/framework';

const html = await View.render('emails/Receipt', { order });
const exists = View.exists('emails/Receipt');

Utilities

Encryption

import { AES } from '@nitronjs/framework';

const token = AES.encrypt({ userId: 1, expires: '2025-12-31' });
const data = AES.decrypt(token);  // Returns false on tamper

Hashing

import { Hash } from '@nitronjs/framework';

const hashed = await Hash.make('password123');
const valid = await Hash.check('password123', hashed);

The bcrypt cost factor is configured in config/hash.js.

Logging

import { Log } from '@nitronjs/framework';

Log.info('User registered', { userId: 1 });
Log.error('Payment failed', { orderId: 123 });
Log.warning('Slow query', { ms: 1200 });
Log.debug('Query executed', { sql: '...' });

DateTime

Use DateTime for all server-side date and time work — never new Date().

import { DateTime } from '@nitronjs/framework';

DateTime.toSQL();                          // "2026-05-14 10:30:00" (now, SQL format)
DateTime.toSQL(timestamp);                 // a specific timestamp in SQL format
DateTime.getDate(timestamp, 'Y-m-d H:i');  // formatted date string
DateTime.getTime(sqlDateTime);             // SQL datetime → millisecond timestamp
DateTime.addDays(7);                       // 7 days from now, SQL format
DateTime.addHours(3);
DateTime.addMinutes(30);
DateTime.subDays(7);
DateTime.subHours(2);
DateTime.subMinutes(15);

Faker

Built-in fake data generator for seeders and testing:

import { Faker } from '@nitronjs/framework';

Faker.fullName();              // "John Smith"
Faker.email();                 // "john@example.com"
Faker.phoneNumber();           // "+1 555 0142"
Faker.sentence();              // "Lorem ipsum dolor sit amet."
Faker.paragraph();             // multi-sentence text
Faker.int(1, 100);             // 42
Faker.float(0, 1, 2);          // 0.37
Faker.boolean();               // true
Faker.uuid();                  // "550e8400-e29b-41d4-a716-446655440000"
Faker.creditCard();            // Luhn-valid card number
Faker.hexColor();              // "#a3f29c"
Faker.city();                  // "Istanbul"
Faker.companyName();           // "Acme Inc."
Faker.imageUrl(640, 480);      // placeholder image URL
Faker.arrayElement(['a', 'b', 'c']);
Faker.oneOf('x', 'y', 'z');

String Utilities

import { Str } from '@nitronjs/framework';

Str.slug('Hello World');       // "hello-world"
Str.camel('user_name');        // "userName"
Str.pascal('user_name');       // "UserName"
Str.snake('userName');         // "user_name"
Str.kebab('userName');         // "user-name"
Str.title('hello world');      // "Hello World"
Str.ucfirst('hello');          // "Hello"
Str.random(32);                // random string
Str.uuid();                    // a UUID
Str.limit('Long text here', 9);// "Long text..."
Str.plural('post');            // "posts"
Str.singular('posts');         // "post"
Str.contains('hello', 'ell');  // true
Str.startsWith('hello', 'he'); // true

Global Functions

These functions are available everywhere — controllers, views, and client-side code — without an import:

Function Description
route(name, params?, query?) Generate a URL for a named route.
csrf() The current request's CSRF token. Use it in forms and fetch headers.
__(key, params?) Translate a key with the current locale.
lang(key, params?) Alias of __().
request() The current request object (Server Components only). Exposes path, method, query, params, headers, cookies, ip, isAjax, locale, session, auth.
// In a form
<input type="hidden" name="_csrf" value={csrf()} />

// In a fetch call
fetch('/api/posts', {
    method: 'POST',
    headers: { 'x-csrf-token': csrf() }
});

// In a Server Component
const tab = request().query.tab || 'general';

CLI Commands

# Development
npm run dev              # Start dev server with HMR
npm run build            # Build for production
npm run start            # Start production server

# Database
npm run migrate          # Run migrations
npm run migrate:safe     # Run migrations after an automatic DB backup
npm run migrate:fresh    # Fresh migration
npm run migrate:fresh:seed # Fresh migration + seed
npm run migrate:rollback # Rollback last batch
npm run migrate:status   # Show migration status
npm run seed             # Run seeders
npx njs db:backup        # Manual DB backup for the current deploy target
npx njs db:backups       # List all DB recovery points
npx njs db:restore <id>  # Restore the DB from a backup (takes a pre-restore backup)

# Code Generation
npm run make:controller <name>
npm run make:model <name>
npm run make:middleware <name>
npm run make:migration <name>
npm run make:seeder <name>

# Diagnostics
npx njs doctor           # Framework health check (auto-fixes safe drift)
npx njs deploy:doctor    # Deploy health check (cascades into doctor)

# Deployment
npx njs deploy:init      # Scaffold CI/CD files (workflow YAML, deploy.config.js)
npx njs deploy:runner    # Install a self-hosted GitHub Actions runner on a server
npx njs deploy:rollback  # Restore a previous snapshot interactively (on a server)

# Utilities
npm run storage:link     # Create storage symlink

Project Structure

my-app/
├── app/
│   ├── Controllers/      # Request handlers
│   ├── Middlewares/      # Custom middleware
│   ├── Models/           # Database models
│   └── Kernel.js         # Middleware alias registry
├── config/               # Configuration files
│   ├── app.js
│   ├── auth.js
│   ├── database.js
│   ├── hash.js
│   ├── server.js
│   └── session.js
├── database/
│   ├── migrations/       # Database migrations
│   └── seeders/          # Database seeders
├── public/               # Static assets
├── resources/
│   ├── css/              # Stylesheets
│   ├── langs/            # Translation files
│   └── views/            # React components (TSX)
├── routes/
│   └── web.js            # Route definitions
├── storage/              # File storage (logs, sessions, uploads, snapshots)
├── deploy.config.js      # Deployment targets (created by deploy:init)
└── .env                  # Environment variables

CSS & Tailwind

Put your CSS files in resources/css/. Tailwind CSS v4 is automatically detected and processed.

/* resources/css/global.css */
@import "tailwindcss";

Import in your .tsx files using the @css alias:

import "@css/global.css";

Configuration

Accessing config values

import { Config } from '@nitronjs/framework';

const appLocale = Config.get('app.locale');
const sessionDriver = Config.get('session.driver', 'file');  // with a default

Config.get(key, default?) reads from the files in config/. The first key segment is the file name (appconfig/app.js), the rest is the path into that file's exported object.

Config files

File Configures
config/app.js locale, fallback_locale, timezone, and the csp (Content Security Policy) whitelist.
config/auth.js defaults.guard and the guards map (provider model, login identifier, home / redirect routes).
config/database.js connections — per-driver settings (charset, collation, connection pool).
config/session.js driver, lifetime, cookieName, the cookie options, and csrf settings (which methods are protected, the token/header field names, route exceptions).
config/server.js web_server (body limit, multipart upload limits and security), cors, and log settings.
config/hash.js salt_rounds — the bcrypt cost factor.

Environment variables

Configuration that varies per environment lives in .env:

APP_NAME=my-app
APP_KEY=
APP_URL=http://localhost
APP_PORT=3000

FILESYSTEM_DRIVER=disk

# Driver: mysql | postgresql | mongodb | none
DATABASE_DRIVER=none
DATABASE_HOST=127.0.0.1
DATABASE_PORT=3306
DATABASE_NAME=my-app
DATABASE_USERNAME=root
DATABASE_PASSWORD=

MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_SECURE=

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

APP_KEY is used for encryption and signing — generate a random value before going to production. Set DATABASE_DRIVER to none to run without a database.


Deployment

NitronJS ships a complete CI/CD pipeline: one config file, one GitHub Actions workflow, and snapshot-based rollback for every target you deploy to.

How it works

deploy.config.js at your project root lists every deployment target. The GitHub Actions workflow reads it, builds your app once, and deploys to each target in parallel via self-hosted runners. Every deploy takes a snapshot first, so a broken deploy rolls back automatically — and you can roll back manually any time with njs deploy:rollback.

Setup (once per project)

npx njs deploy:init      # creates .github/workflows/deploy.yml + deploy.config.js

Then edit deploy.config.js to add your targets:

/**
 * @type {import("@nitronjs/framework").DeployConfig}
 */
export default {
    defaults: {
        snapshot_keep: 5,
        backup_keep: 30
    },
    targets: {
        "my-app": {
            server: "prod-server",
            path: "/var/www/my-app"
        }
    }
};

Commit and push — the workflow is now active.

Setup (once per server)

On each server that hosts a target, install a self-hosted runner:

npx njs deploy:runner    # interactive: asks for label, repo URL, registration token

The server field in deploy.config.js must match the runner's label.

Deploying

Push to main (or trigger the workflow manually from the GitHub UI). The workflow builds once and deploys to every enabled target. Each deploy:

  1. Snapshots the current code (and DB, if a migration is pending)
  2. Extracts the new build
  3. Runs npm ci (skipped when package-lock.json is unchanged)
  4. Runs migrations with an automatic DB backup
  5. Reloads the app with PM2 and verifies it stays online

If any step fails, the snapshot is restored automatically.

Rolling back

On the target server, inside the deployment folder:

npx njs deploy:rollback  # interactive: pick a snapshot, confirm, done

A pre-rollback snapshot is taken first, so the rollback itself is reversible.

Health checks

npx njs doctor           # framework conventions — auto-fixes safe drift
npx njs deploy:doctor    # deploy readiness — run before pushing

Requirements

  • Node.js 20+
  • MySQL 5.7+ or MariaDB 10.3+

License

ISC