Docs / Introduction & Philosophy

Introduction to JSango

JSango is a production-grade, batteries-included TypeScript backend framework inspired by Django’s convention-over-configuration philosophy, Laravel’s developer ergonomics, modern TypeScript type safety, and high-concurrency JavaScript runtimes (Node.js 20+ & Bun).

πŸ’‘
Why JSango? In the TypeScript ecosystem, constructing an enterprise backend traditionally means assembling dozens of disjointed libraries for routing, ORM, migrations, authentication, 2FA, admin panels, background queues, AI agents, and telemetry. JSango solves this fragmentation by offering a cohesive, strictly typed monorepo ecosystem with zero runtime reflection overhead.

Core Principles

1. Strict Type Safety

100% strict TypeScript throughout with compile-time query checking, inferable schemas, and zero any.

2. Batteries Included

Built-in ORM, Multi-DB Drivers, Auto-Admin SPA, 2FA, AI Agent Runtime, Queues, and WebSockets.

3. Extreme Concurrency

Custom SegRadix Trie router executing >7,700,000 lookups/second with zero allocations on hot paths.

4. Security by Default

RFC 6238 TOTP 2FA, Scrypt password hashing, CSRF/CORS protections, and secure multi-device session management.


5-Minute Quickstart

Create a project, generate the database tables from the example model, and start the server.

1. Create a New Project

Terminal
npx jsango new my-backend
cd my-backend
npm install

The project uses SQLite (./db.sqlite3) out of the box on Node.js 22.13+, so no database server is needed. To use PostgreSQL or MySQL, set DATABASE_URL in .env (see Connecting a Database).

2. Define a Model

src/models/product.ts
import { defineModel, fields } from 'jsango';

export const Product = defineModel(
  'Product',
  {
    id: fields.id(),
    name: fields.string({ maxLength: 255 }),
    price: fields.decimal({ precision: 10, scale: 2 }),
    inStock: fields.boolean({ defaultValue: true }),
  },
  { table: 'products', timestamps: true }
);

3. Create the Tables

Terminal
npx jsango makemigrations   # writes migrations/<timestamp>_create_products.ts
npx jsango migrate          # creates the tables

4. Use It in Routes

src/index.ts
import { createApp } from 'jsango';
import { configureDatabase, db } from './database.js';
import { Product } from './models/product.js';

configureDatabase();                 // all models use the configured database
const app = createApp();

app.get('/api/products', () => Product.where('inStock', true).orderBy('name').get());
app.crud('/api/crud/products', Product);   // full REST CRUD in one line

await db.verify();                   // fail fast if the database is unreachable
await app.listen(3000);

5. Start the Development Server

Terminal
npm run dev

Recommended Project Structure

JSango enforces clean architectural layering while giving you flexibility:

Directory Layout
my-backend/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ models/            # Declarative ORM models & relations
β”‚   β”œβ”€β”€ routes/            # HTTP Route handlers & route groups
β”‚   β”œβ”€β”€ middleware/        # Authentication, CORS & rate limit guards
β”‚   β”œβ”€β”€ agents/            # Autonomous AI agents & custom tools
β”‚   β”œβ”€β”€ workflows/         # Multi-agent orchestration workflows
β”‚   β”œβ”€β”€ jobs/              # Background queue workers
β”‚   β”œβ”€β”€ events/            # Strongly typed domain events
β”‚   └── index.ts           # App bootstrap & server initialization
β”œβ”€β”€ migrations/            # Version-controlled database migrations
β”œβ”€β”€ jsango.config.ts       # Framework & multi-database configuration
β”œβ”€β”€ .env                   # Environment variables
β”œβ”€β”€ tsconfig.json          # Strict TypeScript configuration
└── package.json

Configuration & Environment

Runtime settings come from environment variables (.env is loaded by dotenv in the app and automatically by the CLI). jsango.config.ts tells the CLI where your database, models and migrations are:

jsango.config.ts
import { defineConfig } from 'jsango';
import { db } from './src/database.js';

export default defineConfig({
  database: db,               // a DatabaseManager, or { default, connections } config object
  models: './src/models',     // files/folders with defineModel() calls
  migrations: './migrations', // migration files directory
});
.env
DATABASE_URL=postgres://app:secret@localhost:5432/myapp
PORT=3000
JWT_SECRET=change-me
KeyDefaultPurpose
databasefrom DATABASE_URL / DATABASE_*Database used by migrate, db:status, …
models./src/modelsWhere models are imported from (recursive)
migrations./migrationsMigration files directory
envFile.envEnv file loaded first (existing variables win)

SegRadix Trie Router

JSango features a custom Segment Radix Trie router built for extreme throughput (>7.7M lookups/sec) and sub-millisecond route resolution.

Typed Route Parameters

Constrain parameters inline with built-in segment validators like :uuid, :int, and :slug:

Router Example
// Automatically validates UUID format before triggering handler
app.get('/api/v1/users/:id:uuid', async (ctx) => {
  const userId = ctx.request.params['id'];
  const user = await User.find(userId);
  return user ? ctx.response.json(user) : ctx.response.notFound();
});

Route Groups & Scoped Middleware

Route Groups
app.group('/api/v1/admin', (router) => {
  router.use(requireAdminAuth);
  router.get('/analytics', analyticsHandler);
  router.post('/settings', settingsHandler);
});

Request & Response Context

Every route handler receives an isolated RequestContext containing typed request data, headers, cookies, and high-performance response helpers:

src/routes/profile.ts
app.post('/api/users/profile', async (ctx) => {
  // Read parsed JSON body & query params
  const body = ctx.request.body;
  const query = ctx.request.query;

  // Read headers and cookies
  const authHeader = ctx.request.headers.get('Authorization');
  const sessionCookie = ctx.request.cookies.get('session_id');

  // Return standard JSON response with status
  return ctx.response.json({ success: true, user: body }, { status: 201 });
});

1-Line CRUD Generation

Generate complete RESTful API endpoints (GET /, GET /:id, POST /, PUT /:id, DELETE /:id) for any ORM model in a single line of code with built-in pagination, filtering, and validation:

1-Line CRUD Resource
import { createApp, model, fields } from 'jsango';

export const Article = model('Article', {
  id: fields.id(),
  title: fields.string({ maxLength: 200 }),
  content: fields.text(),
  published: fields.boolean({ defaultValue: false }),
});

const app = createApp();

// Generates GET, POST, PUT, DELETE endpoints automatically
app.crud('/api/articles', Article, {
  pagination: { defaultLimit: 25, maxLimit: 100 },
  allowedFilters: ['published', 'title'],
});

await app.listen(3000);

Onion Middleware Pipeline

JSango uses a zero-allocation onion middleware pipeline for logging, authentication guards, CORS, and request rate limiting.

src/middleware/auth.ts
import { RequestContext, NextFunction } from 'jsango';

export async function authGuard(ctx: RequestContext, next: NextFunction) {
  const token = ctx.request.headers.get('Authorization')?.replace('Bearer ', '');
  if (!token) {
    return ctx.response.unauthorized('Authentication token required');
  }

  // Attach verified user to request state
  ctx.state.user = await verifyJwtToken(token);
  return next();
}

Schema Validation

Validate request payloads with high-speed built-in schema builders that compile directly to native JavaScript validators:

Validation Example
import { object, string, number, validate } from 'jsango';

const CreateProductSchema = object({
  name: string().min(3).max(100),
  price: number().positive(),
  category: string().optional(),
});

app.post('/api/products', async (ctx) => {
  const result = validate(CreateProductSchema, ctx.request.body);
  if (!result.success) {
    return ctx.response.badRequest({ errors: result.errors });
  }

  const newProduct = await Product.create(result.data);
  return ctx.response.json(newProduct, { status: 201 });
});

Connecting a Database

JSango talks to PostgreSQL, MySQL/MariaDB, SQLite and MongoDB through one DatabaseManager. The same models, queries and migration commands work on all of them; jsango generates the right SQL (quoting, placeholders, types, RETURNING) or MongoDB commands (filters, pipelines, validators) for each.

DatabasedriverInstall in your appURL example
PostgreSQL 12+postgres (pg, postgresql)npm install pgpostgres://user:pass@host:5432/db
MySQL 8+ / MariaDBmysql (mariadb)npm install mysql2mysql://user:pass@host:3306/db
SQLitesqlitenothing on Node 22.13+ (else better-sqlite3)sqlite:./db.sqlite3
MongoDB 5+mongodb (mongo)npm install mongodbmongodb://user:pass@host:27017/db / mongodb+srv://…
In-memorymemoryβ€”tests only

Configure with Environment Variables

databaseConfigFromEnv() reads DATABASE_URL (preferred) or the individual DATABASE_* variables. This is what jsango new generates:

src/database.ts
import { DatabaseManager, databaseConfigFromEnv, setDatabaseManager } from 'jsango';

export const db = new DatabaseManager(databaseConfigFromEnv());

export function configureDatabase() {
  setDatabaseManager(db); // every model now uses this database
  return db;
}
.env
# Option A β€” one URL (wins if set)
DATABASE_URL=postgres://app:secret@localhost:5432/myapp

# Option B β€” individual settings
DATABASE_DRIVER=mysql
DATABASE_HOST=127.0.0.1
DATABASE_PORT=3306
DATABASE_NAME=myapp
DATABASE_USER=app
DATABASE_PASSWORD=secret
DATABASE_SSL=true          # true/require, no-verify, false
DATABASE_POOL_MAX=10
DATABASE_FILE=./db.sqlite3 # SQLite only
πŸ’‘
Special characters in passwords must be URL-encoded inside DATABASE_URL (p@ss β†’ p%40ss), or use the separate DATABASE_PASSWORD variable.

Multiple Connections & Options

src/database.ts
export const db = new DatabaseManager({
  default: 'primary',
  connections: {
    primary: {
      driver: 'postgres',
      host: process.env.DB_HOST,
      database: 'myapp',
      username: 'app',
      password: process.env.DB_PASSWORD,
      ssl: true,
      pool: { max: 20, connectionTimeoutMs: 10_000 },
    },
    analytics: { url: process.env.ANALYTICS_URL }, // driver inferred from the URL
    local: { driver: 'sqlite', filename: './data/local.sqlite3' },
  },
});

// models: defineModel('Event', {...}, { connection: 'analytics' })

Models without a connection option use the default connection; the name 'default' always resolves to it.

Verify, Health Checks & Raw SQL

src/index.ts
await db.verify();                 // throws a descriptive error if it can't connect
await app.listen(3000);
process.once('SIGTERM', async () => { await db.close(); process.exit(0); });

// raw SQL: always write ? placeholders (converted to $1, $2 for PostgreSQL)
const { rows } = await db.query('SELECT id, email FROM users WHERE created_at > ?', [since]);
await db.transaction(async (tx) => {
  await tx.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [100, from]);
  await tx.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [100, to]);
});
Terminal
npx jsango db:status
# Connection  Driver    Target                     Status      Latency
# default     postgres  app@localhost:5432/myapp   βœ“ HEALTHY   4ms

Connection failures name the target (never the password) and include a hint, e.g. Failed to connect to PostgreSQL at app@127.0.0.1:5432/myapp: connect ECONNREFUSED … Hint: Is the database running and listening on this host/port?


Defining Models

Models are the source of truth for your schema: migrations are generated from them. Put them in src/models/.

src/models/user.ts
import { defineModel, fields } from 'jsango';

export const User = defineModel(
  'User',
  {
    id: fields.id(),                                        // auto-increment primary key
    email: fields.string({ maxLength: 255, unique: true }),
    name: fields.string({ maxLength: 120, nullable: true }),
    role: fields.string({ maxLength: 20, defaultValue: 'member', indexed: true }),
    isActive: fields.boolean({ defaultValue: true }),
    settings: fields.json({ nullable: true }),
  },
  {
    table: 'users',     // default: lower-cased name + "s"
    timestamps: true,   // createdAt / updatedAt, maintained automatically
    softDelete: false,  // true: delete() sets deletedAt instead of removing the row
    relations: {
      posts: { type: 'hasMany', target: 'Post', foreignKey: 'userId' },
    },
  }
);
⚠️
Fields are NOT NULL by default. Add nullable: true for optional columns.

Field Types

FieldPostgreSQLMySQLSQLiteJS value
fields.id()SERIAL PRIMARY KEYINT AUTO_INCREMENTINTEGER PK AUTOINCREMENTnumber
fields.string({ maxLength })VARCHAR(n)VARCHAR(n)VARCHAR(n)string
fields.text()TEXTLONGTEXTTEXTstring
fields.integer() / bigint()INTEGER / BIGINTINT / BIGINTINTEGER / BIGINTnumber / bigint
fields.float() / decimal({ precision, scale })DOUBLE PRECISION / NUMERICDOUBLE / DECIMALREAL / NUMERICnumber
fields.boolean()BOOLEANTINYINT(1)INTEGER (0/1)boolean
fields.dateTime() / date() / time()TIMESTAMPTZ / DATE / TIMEDATETIME(3) (UTC)ISO textDate
fields.json()JSONBJSONTEXTobject / array
fields.uuid() / binary()UUID / BYTEACHAR(36) / LONGBLOBVARCHAR(36) / BLOBstring / Uint8Array
Field optionEffect
nullable: trueAllow NULL (default NOT NULL)
unique: trueUnique constraint uq_<table>_<column>
indexed: trueIndex idx_<table>_<column>
defaultValue: xDefault for new rows (a function is evaluated per insert)
maxLength, precision, scaleColumn size
columnNameColumn name when it differs from the property
primaryKey, autoIncrementCustom keys, e.g. fields.uuid({ primaryKey: true, defaultValue: () => crypto.randomUUID() })

Relations & Eager Loading (No N+1)

src/models/post.ts
export const Post = defineModel(
  'Post',
  {
    id: fields.id(),
    title: fields.string(),
    userId: fields.integer(),   // foreign key column
  },
  {
    table: 'posts',
    relations: {
      // posts.userId -> users.id; the migration creates a FOREIGN KEY (ON DELETE CASCADE)
      author: { type: 'belongsTo', target: 'User', foreignKey: 'userId' },
    },
  }
);

const posts = await Post.query().with('author').orderBy('id', 'DESC').limit(10).get();
posts[0].author.email; // all authors loaded with ONE extra query
TypeMeaningforeignKey is on
belongsTothis row points to one parentthis model
hasOne / hasManychildren point to this rowtarget model
manyToManyvia a pivot model (through, pivotForeignKey, pivotTargetKey)pivot model

target may be the model name ('User') or () => User. Nullable foreign keys default to ON DELETE SET NULL; override with options: { onDelete: 'RESTRICT' }.


Querying, Pagination & Transactions

CRUD
// Create
const user = await User.create({ email: 'a@example.com', name: 'Ada' });
user.id;        // assigned by the database
await Post.bulkCreate([{ title: 'One', userId: user.id }, { title: 'Two', userId: user.id }]);

// Read
await User.find(1);                                  // or null
await User.findOrFail(1);                            // throws ModelNotFoundError
await User.where('email', 'a@example.com').first();
await User.where({ role: 'admin', isActive: true }).get();

// Update
user.name = 'Ada Lovelace';
await user.save();                                   // only changed columns are written
await User.where('isActive', false).update({ role: 'inactive' });

// Delete
await user.delete();
await User.where('role', 'spam').delete();
Filters, sorting & pagination
const users = await User.query()
  .where('createdAt', '>=', since)
  .whereIn('role', ['admin', 'editor'])
  .whereNotNull('name')
  .orWhere('email', 'ILIKE', '%@example.com')
  .orderBy('createdAt', 'DESC')
  .limit(20)
  .offset(40)
  .get();

await User.count();
await User.where('isActive', true).exists();

const page = await User.query().orderBy('id').paginate({ page: 2, pageSize: 25 });
// page.items, page.total, page.page, page.pageSize, page.totalPages

for await (const u of User.query().cursor(500)) {
  // stream a large table in batches of 500
}

Operators: = != > >= < <= LIKE NOT LIKE ILIKE IN NOT IN; where('x', null) becomes IS NULL. Anything else is rejected, so user input can't inject SQL through the operator. Keys that aren't declared fields are ignored on insert/update.

Advanced Queries

Identical on PostgreSQL, MySQL, SQLite and MongoDB:

Advanced queries
// genre = 'fiction' AND (price < 10 OR pages > 500)
await Book.where('genre', 'fiction')
  .where((q) => q.where('price', '<', 10).orWhere('pages', '>', 500))
  .get();

await Book.whereNot('status', 'draft').get();
await Book.whereBetween('price', [10, 25]).get();
await Book.whereLike('title', '%guide%').get();     // case-insensitive
await Book.select('genre').distinct().get();
await Book.orderBy('price').pluck('title');
await Book.latest().first();

// aggregates
await Book.sum('price');
await Book.where('genre', 'science').avg('price');
await Book.min('price');
await Book.max('pages');

// GROUP BY / HAVING -> plain rows
await Book.query().groupBy(
  ['genre'],
  { total: ['sum', 'price'], books: ['count'] },
  { having: [['books', '>=', 2]], orderBy: [['total', 'DESC']] }
); // [{ genre: 'science', total: 42.5, books: 2 }, ...]

// atomic counters, find-or-create, batches, row locks
await Author.where('active', true).increment('logins');
await post.increment('views');
await Tag.firstOrCreate({ slug: 'news' }, { label: 'News' });
await Setting.updateOrCreate({ key: 'theme' }, { value: 'dark' });
await User.query().chunk(500, async (users) => { /* ... */ });
await transaction(async () => {
  const acc = await Account.where('id', id).lockForUpdate().first();
});

// escape hatch
await User.whereRaw('LOWER(email) = ?', [email]).get();      // SQL
await User.whereRaw({ tags: { $all: ['a', 'b'] } }).get();   // MongoDB

Soft Deletes

Soft deletes
// model option: softDelete: true
await post.delete();                  // sets deletedAt
await Post.query().get();             // excludes soft-deleted rows
await Post.withTrashed().get();       // includes them
await Post.onlyTrashed().get();       // only them
await post.delete({ force: true });   // real DELETE

Transactions

Transactions
import { transaction } from 'jsango';

await transaction(async () => {
  const user = await User.create({ email });
  await Profile.create({ userId: user.id });   // automatically in the same transaction
  if (!ok) throw new Error('abort');            // any error rolls everything back
});

await transaction(async () => { /* ... */ }, { isolationLevel: 'SERIALIZABLE' });

// explicit alternative
await db.transaction(async (tx) => {
  await User.create({ email }, { connection: tx });
  await User.query().using(tx).where('id', 1).update({ name: 'x' });
});

MongoDB

Models, queries, relations, transactions and migrations all work on MongoDB. Install the driver and point DATABASE_URL at your server or Atlas cluster:

Terminal
npm install mongodb
# .env
DATABASE_URL=mongodb://app:secret@localhost:27017/myapp
src/models/post.ts
export const Post = defineModel('Post', {
  id: fields.objectId({ primaryKey: true }),  // maps to _id, new ObjectId on create
  title: fields.string(),
  authorId: fields.objectId(),                // ObjectId reference
  tags: fields.json({ nullable: true }),      // stored natively
}, {
  table: 'posts',                             // collection
  timestamps: true,
  relations: { author: { type: 'belongsTo', target: 'Author', foreignKey: 'authorId' } },
});

const post = await Post.create({ title: 'Hello', authorId: author.id });
post.id;                   // '65f1c2…' (ObjectId as string)
await Post.find(post.id);  // converted back to ObjectId in queries

where becomes a filter ($eq, $in, $regex for LIKE, $or/$and/$nor for groups, with SQL precedence); aggregates and groupBy run as aggregation pipelines; increment uses $inc.

Migration operationOn MongoDB
create tablecreateCollection + $jsonSchema validator (required fields & types) + indexes
unique / indexcreateIndex (unique on nullable fields ignores missing values, like SQL NULLs)
add columnbackfill the default into existing documents, then update the validator
drop / rename columnupdate the validator, then $unset / $rename the field and rebuild its indexes
foreign keysskipped β€” relations are resolved by the ORM
πŸ’‘
Transactions need a replica set or sharded cluster (Atlas always has one). Locally: mongod --replSet rs0 and run rs.initiate() once. Migrations do not need transactions and work on a standalone server.
Data migrations & native access
export default defineMigration({
  id: '20260930130000_backfill_roles',
  async up(ctx) {
    await ctx.execute({ op: 'updateMany', collection: 'users', filter: { role: null }, update: { $set: { role: 'member' } } });
  },
});

// anything else: the native driver
import type { Db } from 'mongodb';
const mongo = await db.mongo<Db>();
await mongo.collection('events').aggregate([...]).toArray();

Migrations

Migrations turn changes in your models into versioned, reviewable changes to the real database β€” the same workflow as Django:

  1. npx jsango makemigrations replays your existing migration files to know what the database should look like (no database connection needed), compares it with your models and writes a new file with just the difference.
  2. npx jsango migrate runs pending files in order and records each in the jsango_migrations table so it never runs twice.

Everyday Workflow

Terminal
# 1. change a model, e.g. add  age: fields.integer({ nullable: true })  to User

npx jsango makemigrations          # or: npx jsango migrate:generate add_age_to_users
#  βœ” Created migration migrations/20260930101500_add_age_to_users.ts
#    [+] Add column users.age (integer)

npx jsango migrate --dry-run       # optional: print the exact SQL
npx jsango migrate                 # apply
npx jsango migrate:status          # applied / pending
npx jsango migrate:rollback        # undo the last batch (--steps 1 for one migration)
migrations/20260930101500_add_age_to_users.ts (generated)
import { AddColumnOperation, Migration } from 'jsango';

export const id = '20260930101500_add_age_to_users';
export const name = 'add_age_to_users';

export default new Migration({
  id,
  name,
  operations: [
    new AddColumnOperation('users', { "name": "age", "type": "integer", "nullable": true }),
  ],
});

Generated migrations are reversible automatically. Review the file and commit it together with the model change.

⚠️
Destructive changes (dropping a table/column, changing a type, shrinking a column) are marked [destructive], and migrate refuses them until you confirm with --yes. Renames look like "drop + add" to the generator and would lose data β€” write them by hand with ctx.renameColumn(). Adding a NOT NULL column without a default to a table with rows fails on every database; the generator warns you.

Hand-written Migrations

Terminal
npx jsango makemigrations rename_user_name --empty
migrations/20260930120000_rename_user_name.ts
import { defineMigration } from 'jsango';

export default defineMigration({
  id: '20260930120000_rename_user_name',
  async up(ctx) {
    await ctx.renameColumn('users', 'name', 'fullName');

    await ctx.createTable('audit_logs', (t) => {
      t.id();
      t.integer('userId').references('users');   // FK -> users.id ON DELETE CASCADE
      t.string('action', 50).index();
      t.json('payload').nullable();
      t.timestamps();
    });

    await ctx.sql("UPDATE users SET role = 'member' WHERE role IS NULL"); // data migration
  },
  async down(ctx) {
    await ctx.dropTable('audit_logs');
    await ctx.renameColumn('users', 'fullName', 'name');
  },
});

Helpers: createTable, dropTable, renameTable, addColumn, dropColumn, alterColumn, renameColumn, addIndex, dropIndex, addUnique, dropUnique, addForeignKey, dropForeignKey, sql. Prefer the helpers for schema changes β€” makemigrations can see them; ctx.sql() is invisible to it.

How Each Database Runs Migrations

PostgreSQLSQLiteMySQL / MariaDBMongoDB
Each migration in a transactionβœ…βœ…βŒ (DDL auto-commits)❌
Failure mid-migrationfully rolled backfully rolled backearlier statements stay appliedearlier commands stay applied
ALTER COLUMN, add/drop FKnativeautomatic table rebuild (data kept)nativevalidator update; FKs skipped

A migration lock prevents two deploys from migrating at the same time.

Production & CI

Release step
npm ci
npx jsango migrate:check --skip-db   # CI: fail if a model change has no migration
npx jsango migrate                   # apply pending migrations (locked)
npm start

Deploy the migrations/ folder and jsango.config.ts with the app. TypeScript migrations run without a build step. Full guide: docs/database/README.md.

Troubleshooting

MessageFix
No database is configured for this projectAdd jsango.config.ts or set DATABASE_URL in .env.
PostgreSQL support requires the 'pg' packagenpm install pg (or mysql2, better-sqlite3, mongodb).
ECONNREFUSEDThe database server is not running or the host/port is wrong.
password authentication failedWrong credentials; URL-encode special characters in DATABASE_URL.
database "x" does not existCreate it first: CREATE DATABASE x;
no such table / relation "users" does not existRun npx jsango makemigrations then npx jsango migrate.
Migration run contains destructive changesReview with migrate --dry-run, then migrate --yes.
MongoDB transactions need a replica setRun mongod --replSet rs0 + rs.initiate(), or use Atlas.
Document failed validationA MongoDB document misses a required field or has the wrong type; make the field nullable: true or pass a value.
Could not acquire migration lockAnother migrate is running; the lock of a crashed run expires after 15 minutes.

Auto-Generated React Admin Console

Similar to Django's revered admin interface, JSango automatically generates a full-featured, responsive single-page application (SPA) with CRUD tables, dynamic search, faceted filters, batch bulk actions, CSV export, RBAC security, and live System Diagnostics.

1-Line Instant Admin Setup

Mount a complete admin console directly on your JSango application in a single method call:

src/index.ts
import { createApp, model, fields } from 'jsango';

export const Product = model('Product', {
  id: fields.id(),
  name: fields.string({ maxLength: 255 }),
  price: fields.number(),
  inStock: fields.boolean({ defaultValue: true }),
});

const app = createApp();

// Mount Admin SPA with custom prefix, models & super admin credentials
app.admin({
  prefix: '/admin',
  title: 'My Store Admin',
  resources: [Product],
  auth: {
    email: 'admin@jsango.dev',
    password: process.env.JSANGO_ADMIN_PASSWORD || 'admin123',
    name: 'Super Administrator',
  }
});

await app.listen(3000);

Default Super Admin Credentials

Out-of-the-box, the admin console initializes with the following default super admin credentials:

Field Default Value Environment Variable Override
Email / Username admin@jsango.dev or admin JSANGO_ADMIN_USER or JSANGO_ADMIN_EMAIL
Password admin123 JSANGO_ADMIN_PASSWORD

πŸ’‘ Tip: You can customize super admin credentials at bootstrap via app.admin({ auth: { email: '...', password: '...' } }) or change your password anytime inside the Profile & Security page in the Admin Panel.

Defining Custom Admin Resources with Rules

src/admin/user-resource.ts
import { createAdminResource } from '@jsango/admin-core';
import { User } from '../models/user.js';

export const UserAdminResource = createAdminResource(User, {
  label: 'User Accounts',
  icon: 'users',
  listFields: ['id', 'email', 'fullName', 'role', 'isActive'],
  searchFields: ['email', 'fullName'],
  filters: [
    { field: 'role', type: 'select', options: ['admin', 'editor', 'viewer'] },
    { field: 'isActive', type: 'boolean' }
  ],
  export: { csv: true, json: true },
});

Real-time System Diagnostics

The Admin Panel includes a dedicated System Diagnostics dashboard displaying live V8 Heap Memory, Resident Set Size (RSS), Node.js runtime version, process uptime, and status indicators for ORM database pools, HTTP kernel, RBAC authentication, and audit logs.


AI Agents & Tool Calling

JSango includes a native, provider-agnostic AI agent runtime with strongly typed tool execution and human-in-the-loop approvals:

src/agents/support.ts
import { agent, tool, object, string, number, createApp } from 'jsango';

// 1. Define tools with automatic schema validation
const getOrder = tool({
  name: 'getOrder',
  description: 'Fetch order details',
  schema: object({ orderId: string() }),
  execute: async ({ orderId }) => await Order.find(orderId),
});

const refundOrder = tool({
  name: 'refundOrder',
  description: 'Process a customer refund',
  schema: object({ orderId: string(), amount: number() }),
  requiresApproval: true, // Human approval gate
  execute: async ({ orderId, amount }) => await Stripe.refund(orderId, amount),
});

// 2. Create the Agent
export const supportAgent = agent({
  name: 'SupportAgent',
  model: 'openai:gpt-4o',
  instructions: 'Help customers with orders and refunds.',
  tools: { getOrder, refundOrder },
});

// 3. Expose over HTTP / SSE and WebSockets in 1 line
const app = createApp();
app.agent('/api/support', supportAgent);
app.wsAgent('/ws/support', supportAgent);

Universal LLM Providers

Switch between OpenAI, Anthropic Claude, Google Gemini, and Local Ollama models with seamless configuration:

Provider Configuration
import { OpenAiProvider, AnthropicProvider, GeminiProvider, OllamaProvider, agent } from 'jsango';

// 1. Google Gemini
const geminiAgent = agent({
  provider: new GeminiProvider({ apiKey: process.env.GEMINI_API_KEY }),
  model: 'gemini-1.5-pro',
  instructions: 'Expert data analyst',
});

// 2. Local Ollama (100% offline, zero cloud cost)
const localAgent = agent({
  provider: new OllamaProvider({ baseUrl: 'http://localhost:11434' }),
  model: 'llama3.2',
  instructions: 'Privacy-focused assistant',
});

Multi-Agent Workflows & Orchestration

Compose sequential, parallel, and branching pipelines between multiple specialized agents:

src/workflows/research.ts
import { workflow, agent } from 'jsango';

const pipeline = workflow('document-review')
  .step('research', researcherAgent)
  .step('draft', writerAgent)
  .branch(
    (state) => state.requiresApproval,
    legalReviewerAgent,
    fastTrackAgent
  );

const result = await pipeline.run({ input: 'Analyze quarterly earnings' });

RAG & Vector Search

Embed, store, and retrieve semantic document chunks with zero external dependencies:

src/ai/knowledge.ts
import { knowledge, InMemoryVectorStore } from 'jsango';

export const docs = knowledge({
  name: 'handbook',
  vectorStore: new InMemoryVectorStore(),
  chunkSize: 500,
});

await docs.ingest({
  id: 'doc_1',
  text: 'JSango is a high-performance TypeScript backend framework with built-in AI agents.',
});

const matches = await docs.query({ query: 'What is JSango?', limit: 3 });

Model Context Protocol (MCP)

Expose JSango tools over standardized JSON-RPC MCP servers or consume remote MCP servers:

src/mcp/server.ts
import { McpServer, tool, object, string } from 'jsango';

const server = new McpServer({ name: 'JSango Tools', version: '1.0.9' });

server.registerTool(tool({
  name: 'fetchInventory',
  description: 'Check stock inventory for SKU',
  schema: object({ sku: string() }),
  execute: async ({ sku }) => ({ inStock: true, count: 42 }),
}));

// Ready to handle standard MCP JSON-RPC protocol messages

Streaming SSE & WebSockets

Stream agent thoughts, token chunks, and tool approvals in real-time:

Streaming Endpoints
const app = createApp();

// Server-Sent Events (SSE) streaming
app.agent('/api/agent/stream', supportAgent);

// Bi-directional WebSocket Agent
app.wsAgent('/ws/agent', supportAgent);

Session, JWT & Two-Step Auth (TOTP 2FA)

Security is a first-class concern in JSango. It includes an RFC 6238 compliant Time-Based One-Time Password (TOTP) engine, active device session tracking, and remote session revocation.

Two-Factor Authentication (TOTP)

2FA TOTP Service
import { TotpService } from '@jsango/auth';

const totp = new TotpService();

// 1. Generate Base32 secret and otpauth:// URI
const secret = totp.generateSecret();
const qrUri = totp.generateKeyUri({
  secret,
  accountName: 'developer@example.com',
  issuer: 'JSango App'
});

// 2. Verify 6-digit token with sliding window
const isValid = totp.verifyToken({
  secret,
  token: '123456',
  window: 1
});

Active Device Sessions & Revocation

Inspect active login sessions across devices and terminate compromised sessions with a single call:

Session Control
app.post('/api/auth/logout-all', async (ctx) => {
  const userId = ctx.state.user.id;
  await sessionManager.revokeAllForUser(userId);
  return ctx.response.json({ message: 'All sessions successfully revoked' });
});

Role-Based Access Control (RBAC)

RBAC Guard
import { authorize } from 'jsango';

app.delete('/api/posts/:id', authorize(['admin', 'editor']), async (ctx) => {
  await Post.delete(ctx.request.params.id);
  return ctx.response.noContent();
});

Background Job Queues & Caching

Execute heavy background tasks reliably with automatic retries, dead-letter store management, and concurrent worker loops.

Defining & Dispatching Jobs
import { jobs } from 'jsango';

// 1. Define job handler
jobs.define('send-welcome-email', async (payload) => {
  await mailer.send({ to: payload.email, subject: 'Welcome!' });
});

// 2. Dispatch job asynchronously with retry policy
await jobs.dispatch('send-welcome-email', { email: 'user@example.com' }, {
  retries: 3,
  delay: 5000,
});

High-Performance Caching

Cache Operations
import { cache } from 'jsango';

// Cache expensive computation or query with remember()
const topProducts = await cache.remember('top-products', 300, async () => {
  return await Product.where('sales', '>', 1000).get();
});

// Invalidate on update
await cache.delete('top-products');

Typed Event Emitter

Domain Events
import { events } from 'jsango';

events.on('user:registered', async ({ user }) => {
  console.log('New user registered:', user.email);
});

await events.emit('user:registered', { user: newUser });

Realtime WebSockets

Multi-room WebSocket architecture with heartbeat monitoring, user identity binding, and backpressure management.

WebSocket Server
app.ws('/ws/chat', (ws) => {
  ws.on('message', (msg) => {
    // Broadcast to room
    ws.broadcast('chat-room', { user: 'Alice', text: msg });
  });

  ws.on('disconnect', () => {
    console.log('Client disconnected');
  });
});

Prometheus Metrics & Tracing

Built-in Prometheus metrics exposition and distributed tracing hooks for production observability:

Observability
import { MetricRegistry, HealthRegistry } from 'jsango';

// Metrics endpoint exposed for Prometheus scraping
app.get('/metrics', async (ctx) => {
  return ctx.response.text(await MetricRegistry.exportPrometheus());
});

// Liveness & readiness probes for Kubernetes
app.get('/health', async (ctx) => {
  const status = await HealthRegistry.check();
  return ctx.response.json(status);
});

OpenAPI 3.1 & Swagger UI Documentation

JSango automatically generates compliant OpenAPI 3.1 JSON schemas by introspecting your route signatures, input validation schemas, and ORM models, and renders an interactive, high-contrast dark-themed Swagger UI dashboard out of the box.

1-Line Auto Documentation

src/index.ts
import { createApp } from 'jsango';
import { Product } from './models/product.js';

const app = createApp();

// Mount CRUD routes
app.crud('/api/products', Product);

// Enable automated OpenAPI 3.1 spec and interactive Swagger UI docs
app.openapi({
  path: '/openapi.json',
  title: 'E-Commerce Store API',
  version: '1.0.9',
  description: 'Production REST API with real-time inventory and search',
});

await app.listen(3000);
// Interactive Swagger UI is live at: http://localhost:3000/docs
// OpenAPI JSON schema at: http://localhost:3000/openapi.json

CLI Commands Reference

Run npx jsango help for the full list and npx jsango <command> --help for options. Every command accepts --json.

Command Description
npx jsango new <name> Scaffold a project (SQLite configured, example model, migrations folder).
npx jsango makemigrations [name] Create a migration from model changes (migrate:generate). --dry-run, --empty.
npx jsango migrate Apply pending migrations. --dry-run prints SQL, --yes confirms destructive changes, --target <id>.
npx jsango migrate:status Show applied and pending migrations.
npx jsango migrate:rollback Undo the last batch; --steps N or --target <id>.
npx jsango migrate:check Fail when models have unmigrated changes or migrations are pending (CI). --skip-db.
npx jsango migrate:reset --yes Roll back all migrations; --fresh re-applies them. Refused in production.
npx jsango db:status Test every configured database connection.
npx jsango model:list / model:show <Name> Inspect registered ORM models.
npx jsango route:list List HTTP routes.
npx jsango make:admin <Model> Generate an admin resource.
npx jsango make:agent <Name> Generate an AI agent and tool.
npx jsango openapi:generate Generate an OpenAPI 3.1 document.
npx jsango queue:work Run a background job worker.
npx jsango doctor Check environment, database and migrations.

Modular Packages Directory

JSango is built as an interconnected suite of 28 specialized packages. You can import from the monolithic jsango meta-package or install individual modular packages:

Package Name Version Description & Key Exports
jsango v1.3.0 Primary batteries-included meta-package (createApp, model, agent, tool).
@jsango/ai v1.3.0 AI Platform: Autonomous Agents, Multi-Agent Workflows, RAG Vector Search, MCP Protocol.
@jsango/orm v1.3.0 Declarative ORM, Relations, Eager Loading (No N+1), AST Query Builder.
@jsango/database v1.3.0 Database Drivers: PostgreSQL, MySQL/MariaDB, SQLite, MongoDB, In-Memory Engine.
@jsango/router v1.3.0 Ultra-fast SegRadix Trie Router (>7.7M ops/sec) with inline parameter validators.
@jsango/http v1.3.0 Typed HTTP Request, Response, RequestContext, Cookies, and HTTP Status Codes.
@jsango/admin-server v1.3.0 Backend server & REST endpoints powering the dynamic Admin Console.
@jsango/admin-ui v1.3.0 React Single-Page Application (SPA) dashboard for database records & system diagnostics.
@jsango/admin-core v1.3.0 Resource registry, faceted filters, search rules, and CSV/JSON export definitions.
@jsango/auth v1.3.0 RFC 6238 TOTP 2FA, Scrypt Password Hashing, JWT Tokens, Multi-device Sessions.
@jsango/queue v1.3.0 Background job worker loops, automatic exponential retries, dead-letter storage.
@jsango/cache v1.3.0 High-speed caching layer supporting memory and Redis drivers with TTL & tags.
@jsango/events v1.3.0 Strongly typed in-memory and distributed Pub/Sub event bus.
@jsango/websocket v1.3.0 Realtime WebSockets with room grouping, client broadcasting, and heartbeat pings.
@jsango/validation v1.3.0 Compiled schema validators (string, number, object, array).
@jsango/migrations v1.3.0 Automated schema diffing, batch migration runner, and rollback engine.
@jsango/openapi v1.3.0 OpenAPI 3.1 specification generation and dark-themed Swagger UI dashboard.
@jsango/observability v1.3.0 Structured logging, Prometheus metric exposition, and distributed tracing hooks.
@jsango/cli v1.3.0 Command-line interface for scaffolding, model generation, migrations, and diagnostics.