Config
What the deployment is. The class is the schema of the file: an unknown key is an error, and secrets live in separate files.
this.secret( "mail.passwordFile" )A TypeScript backend for people who build applications. Secure by default, small by design: routes, sessions, access rights, migrations, Postgres or SQLite, workers and logs in one package, written together.
npm install @r-libre/z4jsMost Node backends start as a blank Express app and grow by accretion.
The main concepts are deliberately few. Use TypeScript and Node when they already solve the problem, and add an abstraction only where it earns its place.
What the deployment is. The class is the schema of the file: an unknown key is an error, and secrets live in separate files.
this.secret( "mail.passwordFile" )What the database must look like. It inspects the real state at every start and brings it where it must be.
hasTable · hasFieldWhat a resource answers. An object that only notes its routes: created anywhere, tested alone, mounted in several groups.
this.get( "/item/:id", … )Who may reach it. A prefix and a decision, guarded or unprotected, written in the main.
RouteGroup.guarded( … )What a user may do. Four actions per resource, nothing else, checked in every handler.
notes/createWho the user is. Opaque tokens, no cookies, refresh tokens rotated on every use.
sessions.guardWhat runs beside the requests. Classes registered by name, messages both ways, named mutexes.
workers.call( … )What happened. One line per event: a stable name, then the data in JSON.
req.log.info( "note.created" )A group is either guarded or unprotected, and you have to say which one. An open route stands out in review instead of hiding behind a forgotten middleware.
const api = RouteGroup.guarded( "/api", sessions.guard )
.add( "/notes", new NotesEP( deps ) )
.add( "/live", live );
const auth = RouteGroup.unprotected( "/auth" )
.add( "/", sessions.endPoints );
await serve( {
config, logger,
groups: [api, auth],
statics: [{ path: "/", folder: config.www }]
} );Security is not a chapter at the end of the documentation. It is what happens when you write nothing special.
Anything not explicitly allowed is refused. The client gets a status and a short message, never an internal detail. Every value from a client is checked before you use it.
Opaque tokens, only their SHA-256 hash stored. 15 minute access tokens, refresh tokens rotated on every use: a reused one ends the session. A sensitive route adds a step-up filter.
this.del( "/item/:id", this.on_delete,
{ filter: sessions.stepUp } );
// without a recent confirmation of identity:
// 403 step-up required
An import creates, so it needs notes/create: no right is ever invented for a feature. In debug mode, a handler that forgets to check access is reported.
One central handler: the cause goes to the log, never to the response. Security events come from a closed list, never filtered, in their own file.
throw new HttpError( 404, "unknown note" );auth.login.failed · auth.unauthorized · auth.stepup.failedOne package provides them, written together, with the same rules everywhere.
A handler is a method. paramValue, bodyValue and queryValue read one named value, convert it, check it, and return the right TypeScript type. A missing, malformed, too long or out of range value is a 400 that names the parameter and never echoes its value.
async on_create( req: Request, res: Response ) {
await this.need( req, "notes/create" );
const title = this.bodyValue( req, "title", "string", { maxlength: 100, trim: true } );
const text = this.bodyValue( req, "text", "string", { maxlength: 10_000 } );
...
res.status( 201 ).json( { id } );
}No numbered migration files. A model inspects the real state of the database and brings it where it must be, because in development the database is often edited by hand. Every model migrates in one transaction: it all succeeds, or nothing changed.
override async onMigrate( db: Db, version: number ): Promise<number> {
if( !await this.hasTable( db, "notes" ) ) {
await db`create table notes ( ... )`;
}
if( !await this.hasField( db, "notes", "author" ) ) {
await db`alter table notes add column author text`;
}
return Math.max( version, 2 );
}Refused unless declared. Each file field has its size limit, files are streamed to disk under a random name, and whatever is not kept is deleted when the answer is sent.
Classes registered by name in a single entry file. Messages go both ways, a worker processes them one at a time, and its log lines are written by the main thread.
await workers.start( "stats" );
const result = await workers.call(
"stats", "count", { texts } );Mounted in a group like end points. In a guarded group, a socket opens only with a one-time ticket, bound to the endpoint and the user, valid one second.
POST /api/live → ticket WS /api/live?ticket=… → socket
Postgres through postgres.js for sites, SQLite through node:sqlite for small projects and desktop apps. Both are written the same way, with tagged templates, and values are always bound. No ORM, no query builder.
const [note] = await sql`select * from notes where id = ${id}`;await sql`insert into notes ${sql( note )}`;await sql`update notes set ${sql( changes, "title", "text" )} where id = ${id}`;Five packages, that is all. No ORM, no validation library, no logger, no session store, no JWT library, no upload middleware. Sources are published as TypeScript, as is: what you debug is what was written.
z4js keeps the daily work short: readable logs, an API description read from your sources, and threads you can actually debug.
$ git clone https://github.com/rlibre/z4js
$ cd z4js/demo/frontend
$ npm install && npm run build
$ cd ../backend
$ npm install && npm run build
$ npm run start:log
# a notes application: z4js backend, x4js frontendz4js ships an AI guide (the rules) and an AI context (the API). Agents can also inspect the TypeScript sources shipped in node_modules/@r-libre/z4js.
x4js is the UI counterpart of z4js: the same philosophy on both sides. Plain TypeScript classes, very few dependencies, and code that runs when you call it. More than 50 components for dense, professional interfaces.