FigTree is a Node.js backend framework for rapid development of server applications. It provides an integrated foundation with HTTP server, database support, plugin architecture, schema validation, and service lifecycle management.
- Installation
- Quick Start
- Configuration
- Services
- HTTP Server & Routes
- Database
- Plugin System
- Cluster Support
- CLI Tool
- Features
- Used By
npm install git+https://github.com/stefanwerfling/figtree.gitInstall peer dependencies for the features you use:
# Core (always required)
npm install express express-rate-limit express-session helmet cookie-parser csurf async-exit-hook winston winston-daily-rotate-file uuid ets vts
# MariaDB
npm install typeorm mysql2
# Redis
npm install redis
# InfluxDB
npm install @influxdata/influxdb-client
# ChromaDB
npm install chromadb
# Vite integration
npm install vite
# Plugins / Crypto
npm install node-forge node-schedule rbac-simpleTypeScript types:
npm install --save-dev \
@types/express \
@types/express-session \
@types/async-exit-hook \
@types/cookie-parser \
@types/node \
@types/uuid \
git+https://github.com/stefanwerfling/node-forge-types.gitimport { ConfigBackend, ConfigBackendOptions } from 'figtree';
import { SchemaConfigBackendOptions } from 'figtree-schemas';
// Use the built-in ConfigBackendOptions or extend it
type MyConfig = ConfigBackendOptions;import { DefaultArgs } from 'figtree-schemas';
type MyArgs = DefaultArgs;import { BackendApp } from 'figtree';
import { HttpService } from 'figtree';
export class MyBackend extends BackendApp<MyArgs, MyConfig> {
protected override _getConfigInstance() {
return new ConfigBackend<MyConfig>(SchemaConfigBackendOptions);
}
protected override async _initServices(): Promise<void> {
this._serviceManager.add(new HttpService());
}
}const backend = new MyBackend();
await backend.start();{
"server": {
"port": 3000
},
"logging": {
"dirname": "./logs"
}
}A full working example is available in src/Example/.
FigTree loads configuration from a JSON file (default: config.json) and validates it against a VTS schema. The config path is read from the --config CLI argument or falls back to config.json in the working directory.
const backend = new MyBackend();
await backend.start();Environment variables can be loaded from a .env file by passing --envargs 1 on the command line. ConfigBackend maps env variables to config fields — override _loadEnv() to implement your own mapping.
Services are the building blocks of a FigTree application. Each service has a lifecycle: init → start → stop.
| Service | Description |
|---|---|
HttpService |
Starts the HTTP/HTTPS server |
MariaDBService |
Connects to MariaDB via TypeORM |
RedisDBService |
Connects to Redis |
InfluxDBService |
Connects to InfluxDB |
ChromaDBService |
Connects to ChromaDB |
PluginService |
Loads and manages plugins |
protected override async _initServices(): Promise<void> {
this._serviceManager.add(new MariaDBService());
this._serviceManager.add(new HttpService(['mariadb'])); // depends on mariadb
}Services with dependencies are started in the correct order automatically.
import { ServiceAbstract, ServiceStatus } from 'figtree';
export class MyService extends ServiceAbstract {
public static NAME = 'myservice';
public constructor() {
super(MyService.NAME);
}
public override async start(): Promise<void> {
this._status = ServiceStatus.Progress;
// your init logic
this._status = ServiceStatus.Success;
}
public override async stop(): Promise<void> {
// your cleanup logic
this._status = ServiceStatus.None;
}
}import { ServiceJobAbstract } from 'figtree';
export class MyJob extends ServiceJobAbstract {
public static NAME = 'myjob';
public constructor() {
super(MyJob.NAME, '*/5 * * * *'); // every 5 minutes (cron syntax)
}
protected override async _execute(): Promise<void> {
// runs on schedule
}
}import { DefaultRoute } from 'figtree';
import { Router } from 'express';
export class MyRoute extends DefaultRoute {
public getExpressRouter(): Router {
this._get(
this._getUrl('v1', 'example', 'hello'),
false, // no login required
async (_req, _res, _data) => {
return { statusCode: 200, msg: 'Hello World' };
},
{
description: 'Hello World endpoint',
responseBodySchema: SchemaMyResponse
}
);
return super.getExpressRouter();
}
}| Field | Description |
|---|---|
bodySchema |
Validate request body |
querySchema |
Validate query parameters |
pathSchema |
Validate path parameters |
headerSchema |
Validate request headers |
cookieSchema |
Validate cookies |
sessionSchema |
Validate and initialize session |
responseBodySchema |
Validate response body (also generates Swagger docs) |
parser |
Middleware(s) — RequestHandler | RequestHandler[] |
aclRight |
Required ACL right for this route |
useLocalStorage |
Enable AsyncLocalStorage request context |
import { createBruteForceProtection } from 'figtree';
this._post(
this._getUrl('v1', 'auth', 'login'),
false,
loginHandler,
{
parser: createBruteForceProtection({ limit: 10, windowMs: 15 * 60 * 1000 }),
bodySchema: SchemaLoginRequest,
responseBodySchema: SchemaDefaultReturn,
sessionSchema: SchemaSessionData
}
);export class MyRoute extends DefaultRoute {
public constructor() {
super();
this._defaultParser = createBruteForceProtection({ limit: 20 });
}
}Subclass HttpRouteLoader and override loadRoutes(), then pass the class to HttpService:
import { HttpRouteLoader, IDefaultRoute } from 'figtree';
export class MyRouteLoader extends HttpRouteLoader {
public static override async loadRoutes(): Promise<IDefaultRoute[]> {
return [new MyRoute()];
}
}Swagger UI is automatically generated from your route schemas. It is available at /swagger when SwaggerUIRoute is registered.
Override _getCspDirectives() in your HttpServer subclass:
import { HttpServer } from 'figtree';
export class MyHttpServer extends HttpServer {
protected override _getCspDirectives(): Record<string, string[]> {
return {
...super._getCspDirectives(),
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.example.com']
};
}
}Override _getSessionStore() in your HttpServer subclass to replace the default in-memory store:
protected override _getSessionStore(): Store {
const RedisStore = RedisStoreFactory(session);
return new RedisStore({ client: RedisClient.getInstance().getClient() });
}FigTree is designed to run behind a reverse proxy in production. For local development, a temporary self-signed certificate is generated automatically when no cert/key is configured. Use FlyingFish, Nginx, or similar in production.
Extend DBRepository or DBRepositoryUnid for your entities:
import { DBRepository, DBBaseEntityId } from 'figtree';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class UserEntity extends DBBaseEntityId {
@Column()
public name!: string;
}
export class UserRepository extends DBRepository<UserEntity> {
public constructor() {
super(UserEntity);
}
}Available transformers for TypeORM columns: BoolTransformer, IntTransformer, DecimalTransformer, EncryptionTransformer, ZeroPaddingTransformer.
import { RedisClient } from 'figtree';
const client = RedisClient.getInstance();
await client.set('key', { foo: 'bar' });
const value = await client.get<{ foo: string }>('key');import { InfluxDbHelper } from 'figtree';
const point = new Point('temperature').floatField('value', 23.5);
InfluxDbHelper.addPoint(point);import { APlugin } from 'figtree';
export default class MyPlugin extends APlugin {
public override async onEnable(): Promise<void> {
// plugin loaded
}
}Each plugin requires a plugin.json definition file:
{
"name": "my-plugin",
"version": "1.0.0",
"main": "dist/index.js"
}Run in your plugin project directory after building:
npx figtree -create-plugin-hashThis generates a Merkle hash of all files in dist/. The hash is used by PluginManager to verify plugin integrity at load time.
FigTree supports multi-process clustering via BackendCluster. Workers share state via IPCSharedStore (single host) or RedisSharedStore (distributed).
The simplest entry point reads config.json and decides between cluster mode and single-process mode automatically:
import { bootstrap } from 'figtree';
import { MyBackend } from './MyBackend.js';
await (await bootstrap(() => new MyBackend())).start();config.json:
Or instantiate BackendCluster directly without config:
import { BackendCluster } from 'figtree';
const cluster = new BackendCluster({
appFactory: () => new MyBackend(),
workers: 4, // optional, defaults to os.cpus().length (ignored when `roles` is set)
shutdownTimeoutMs: 15_000, // optional, default 15s
shutdownSignals: ['SIGTERM', 'SIGINT'], // optional
respawn: { // optional
backoffMs: [0, 1000, 5000, 15_000, 30_000],
maxPerWindow: 5,
windowMs: 60_000
},
roles: { // optional — see "Worker roles" below
http: 4,
cron: 1
}
});
await cluster.start();Workers can be assigned logical roles, propagated via the WORKER_ROLE env variable. Services can then be filtered by role at registration time:
protected override async _initServices(): Promise<void> {
// runs on every worker (no role filter)
this._serviceManager.add(new MariaDBService());
this._serviceManager.add(new RedisDBService());
// runs only on http workers
this._serviceManager.add(new HttpService(), ['http']);
// runs only on the cron worker (singleton in the cluster)
this._serviceManager.add(new MyReportJob(), ['cron']);
}In single-process mode (no BackendCluster), the role filter is inactive — every service runs. Inside a role-based cluster, services are only registered on workers whose role matches.
Helpers:
BackendCluster.getWorkerRole(); // 'http' | 'cron' | 'default'
BackendCluster.getWorkerId(); // '<hostname>:<pid>'Any class can publish its state cluster-wide via ClusterRegistry. Implement ClusterPublishable (two methods: getNamespace(), serialize()), register the instance with the registry, and the merged view is queryable on every worker — including across hosts when RedisSharedStore points to a shared Redis.
import { ClusterRegistry, RedisSharedStore } from 'figtree';
protected override async _initServices(): Promise<void> {
const store = new RedisSharedStore(redisClient, 'myapp');
await store.init();
ClusterRegistry.initialize(store); // BackendApp auto-wires the rest
this._serviceManager.add(new HttpService(), ['http']);
this._serviceManager.add(new MyCronJob(), ['cron']);
}ServiceManager already implements ClusterPublishable — BackendApp registers it automatically when a ClusterRegistry singleton exists. The HTTP endpoint GET /v1/service/status/cluster (provided by ServiceRoute) aggregates ServiceInfoEntry lists across all workers and all hosts.
For tasks that must run exactly once cluster-wide (cron master, migration runner, scheduled cleanup) regardless of how many hosts are running, use ClusterLeader:
import { ClusterLeader } from 'figtree';
const leader = new ClusterLeader(store, { name: 'cron-master' });
leader.onLeaderElected(() => startCronJobs());
leader.onLeaderLost(() => stopCronJobs());
await leader.start();Backed by an atomic distributed lease (Redis SET NX PX + tiny Lua scripts for compare-and-set, IPC for single-host). At-most-one leader cluster-wide; failover on the order of the lease TTL.
For the full picture (architecture diagram, custom publishables, Pub/Sub, leases) see doc/cluster.md.
When the master receives a shutdown signal (SIGTERM / SIGINT by default):
- Respawn is disabled — workers that exit during shutdown are not replaced.
- Each worker receives
SIGTERM, triggeringBackendApp'sasync-exit-hookwhich runsServiceManager.stopAll()(HTTP draining, DB connections closed, etc.). - The master waits up to
shutdownTimeoutMsfor all workers to exit voluntarily. - Any worker still alive after the timeout is killed with
SIGKILL. - The master exits with code
0.
shutdownTimeoutMs should be larger than the worker-side timeout (10s in BackendApp.start()) so the worker has a chance to finish first.
When a worker exits unexpectedly, respawn is delayed by a progressive backoff and bounded by a circuit breaker:
- The 1st crash respawns instantly, the 2nd after
1s, the 3rd after5s, etc. (configurable viarespawn.backoffMs). - Crashes are tracked across a rolling window (
respawn.windowMs, default60s). - If more than
respawn.maxPerWindow(default5) crashes occur within that window, the master halts the cluster withprocess.exit(1)so a process supervisor (systemd, Kubernetes, etc.) can restart it.
For more details see doc/cluster.md.
- Schema declaration & validation (VTS)
- Environment variable loading (dotenv)
- JSON config loading with schema validation
- Logging (Winston with daily rotation)
- MariaDB (TypeORM), Redis, InfluxDB, ChromaDB
- HTTP/HTTPS server (Express, rate limiting, helmet, session, CSRF)
- Swagger UI with auto-generated docs from schemas
- Unix socket HTTP server
- File upload helper
- AsyncLocalStorage request context
- Vite integration for frontend dev
- WebSocket server
- Service manager with dependency resolution and scheduling
- Provider system
- Plugin manager with Merkle-hash validation
- Plugin signing (CA)
- Crypto: PEM helper, certificate generator
- ACL / RBAC
- Cluster support with shared state (IPC / Redis)
- DB history (change tracking)
-
PuppeteerCast — Converts web browser content into live video streams via HTTP endpoints.
-
FlyingFish (coming soon) — Reverse proxy manager with WebUI, DNS server, SSH server, DynDNS, UPNP, Let's Encrypt and more.
-
MWPA (coming soon) — Scientific observational data acquisition for marine mammal research.
To build a frontend for your FigTree API, the companion framework Bambooo can be used.
{ "cluster": { "enabled": true, "roles": { "http": 4, "cron": 1 }, "sharedStore": { "type": "redis", "namespace": "myapp" } } }