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).
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
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
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
npx jsango makemigrations # writes migrations/<timestamp>_create_products.ts
npx jsango migrate # creates the tables
4. Use It in Routes
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
npm run dev
Recommended Project Structure
JSango enforces clean architectural layering while giving you flexibility:
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:
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
});
DATABASE_URL=postgres://app:secret@localhost:5432/myapp
PORT=3000
JWT_SECRET=change-me
| Key | Default | Purpose |
|---|---|---|
database | from DATABASE_URL / DATABASE_* | Database used by migrate, db:status, β¦ |
models | ./src/models | Where models are imported from (recursive) |
migrations | ./migrations | Migration files directory |
envFile | .env | Env 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:
// 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
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:
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:
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.
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:
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.
| Database | driver | Install in your app | URL example |
|---|---|---|---|
| PostgreSQL 12+ | postgres (pg, postgresql) | npm install pg | postgres://user:pass@host:5432/db |
| MySQL 8+ / MariaDB | mysql (mariadb) | npm install mysql2 | mysql://user:pass@host:3306/db |
| SQLite | sqlite | nothing on Node 22.13+ (else better-sqlite3) | sqlite:./db.sqlite3 |
| MongoDB 5+ | mongodb (mongo) | npm install mongodb | mongodb://user:pass@host:27017/db / mongodb+srv://β¦ |
| In-memory | memory | β | tests only |
Configure with Environment Variables
databaseConfigFromEnv() reads DATABASE_URL (preferred) or the individual DATABASE_* variables. This is what jsango new generates:
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;
}
# 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
DATABASE_URL (p@ss β p%40ss), or use the separate DATABASE_PASSWORD variable.Multiple Connections & Options
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
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]);
});
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/.
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' },
},
}
);
nullable: true for optional columns.Field Types
| Field | PostgreSQL | MySQL | SQLite | JS value |
|---|---|---|---|---|
fields.id() | SERIAL PRIMARY KEY | INT AUTO_INCREMENT | INTEGER PK AUTOINCREMENT | number |
fields.string({ maxLength }) | VARCHAR(n) | VARCHAR(n) | VARCHAR(n) | string |
fields.text() | TEXT | LONGTEXT | TEXT | string |
fields.integer() / bigint() | INTEGER / BIGINT | INT / BIGINT | INTEGER / BIGINT | number / bigint |
fields.float() / decimal({ precision, scale }) | DOUBLE PRECISION / NUMERIC | DOUBLE / DECIMAL | REAL / NUMERIC | number |
fields.boolean() | BOOLEAN | TINYINT(1) | INTEGER (0/1) | boolean |
fields.dateTime() / date() / time() | TIMESTAMPTZ / DATE / TIME | DATETIME(3) (UTC) | ISO text | Date |
fields.json() | JSONB | JSON | TEXT | object / array |
fields.uuid() / binary() | UUID / BYTEA | CHAR(36) / LONGBLOB | VARCHAR(36) / BLOB | string / Uint8Array |
| Field option | Effect |
|---|---|
nullable: true | Allow NULL (default NOT NULL) |
unique: true | Unique constraint uq_<table>_<column> |
indexed: true | Index idx_<table>_<column> |
defaultValue: x | Default for new rows (a function is evaluated per insert) |
maxLength, precision, scale | Column size |
columnName | Column name when it differs from the property |
primaryKey, autoIncrement | Custom keys, e.g. fields.uuid({ primaryKey: true, defaultValue: () => crypto.randomUUID() }) |
Relations & Eager Loading (No N+1)
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
| Type | Meaning | foreignKey is on |
|---|---|---|
belongsTo | this row points to one parent | this model |
hasOne / hasMany | children point to this row | target model |
manyToMany | via 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
// 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();
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:
// 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
// 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
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:
npm install mongodb
# .env
DATABASE_URL=mongodb://app:secret@localhost:27017/myapp
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 operation | On MongoDB |
|---|---|
| create table | createCollection + $jsonSchema validator (required fields & types) + indexes |
| unique / index | createIndex (unique on nullable fields ignores missing values, like SQL NULLs) |
| add column | backfill the default into existing documents, then update the validator |
| drop / rename column | update the validator, then $unset / $rename the field and rebuild its indexes |
| foreign keys | skipped β relations are resolved by the ORM |
mongod --replSet rs0 and run rs.initiate() once. Migrations do not need transactions and work on a standalone server.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:
npx jsango makemigrationsreplays 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.npx jsango migrateruns pending files in order and records each in thejsango_migrationstable so it never runs twice.
Everyday Workflow
# 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)
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], 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
npx jsango makemigrations rename_user_name --empty
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
| PostgreSQL | SQLite | MySQL / MariaDB | MongoDB | |
|---|---|---|---|---|
| Each migration in a transaction | β | β | β (DDL auto-commits) | β |
| Failure mid-migration | fully rolled back | fully rolled back | earlier statements stay applied | earlier commands stay applied |
| ALTER COLUMN, add/drop FK | native | automatic table rebuild (data kept) | native | validator update; FKs skipped |
A migration lock prevents two deploys from migrating at the same time.
Production & CI
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
| Message | Fix |
|---|---|
No database is configured for this project | Add jsango.config.ts or set DATABASE_URL in .env. |
PostgreSQL support requires the 'pg' package | npm install pg (or mysql2, better-sqlite3, mongodb). |
ECONNREFUSED | The database server is not running or the host/port is wrong. |
password authentication failed | Wrong credentials; URL-encode special characters in DATABASE_URL. |
database "x" does not exist | Create it first: CREATE DATABASE x; |
no such table / relation "users" does not exist | Run npx jsango makemigrations then npx jsango migrate. |
Migration run contains destructive changes | Review with migrate --dry-run, then migrate --yes. |
MongoDB transactions need a replica set | Run mongod --replSet rs0 + rs.initiate(), or use Atlas. |
Document failed validation | A MongoDB document misses a required field or has the wrong type; make the field nullable: true or pass a value. |
Could not acquire migration lock | Another 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:
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
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:
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:
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:
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:
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:
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:
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)
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:
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)
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.
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
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
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.
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:
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
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. |