Skip to content

stefanwerfling/figtree

Repository files navigation

Discord Ask DeepWiki

FigTree - Server Core/Backend Framework



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.


Table of Contents


Installation

npm install git+https://github.com/stefanwerfling/figtree.git

Install 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-simple

TypeScript 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.git

Quick Start

1. Define your config schema and type

import { ConfigBackend, ConfigBackendOptions } from 'figtree';
import { SchemaConfigBackendOptions } from 'figtree-schemas';

// Use the built-in ConfigBackendOptions or extend it
type MyConfig = ConfigBackendOptions;

2. Define CLI arguments

import { DefaultArgs } from 'figtree-schemas';

type MyArgs = DefaultArgs;

3. Create your backend

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());
    }
}

4. Start

const backend = new MyBackend();
await backend.start();

5. Config file (config.json)

{
    "server": {
        "port": 3000
    },
    "logging": {
        "dirname": "./logs"
    }
}

A full working example is available in src/Example/.


Configuration

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

Services are the building blocks of a FigTree application. Each service has a lifecycle: init → start → stop.

Built-in services

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

Registering services

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.

Custom service

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;
    }
}

Scheduled jobs

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
    }
}

HTTP Server & Routes

Defining a route

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();
    }
}

Route options

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

Protecting against brute force

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
    }
);

Global route middleware

export class MyRoute extends DefaultRoute {
    public constructor() {
        super();
        this._defaultParser = createBruteForceProtection({ limit: 20 });
    }
}

Registering routes

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

Swagger UI is automatically generated from your route schemas. It is available at /swagger when SwaggerUIRoute is registered.

Customizing the CSP policy

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']
        };
    }
}

Persistent session store

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() });
}

Reverse proxy

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.


Database

MariaDB

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.

Redis

import { RedisClient } from 'figtree';

const client = RedisClient.getInstance();
await client.set('key', { foo: 'bar' });
const value = await client.get<{ foo: string }>('key');

InfluxDB

import { InfluxDbHelper } from 'figtree';

const point = new Point('temperature').floatField('value', 23.5);
InfluxDbHelper.addPoint(point);

Plugin System

Creating a plugin

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"
}

Generating a plugin hash

Run in your plugin project directory after building:

npx figtree -create-plugin-hash

This generates a Merkle hash of all files in dist/. The hash is used by PluginManager to verify plugin integrity at load time.


Cluster Support

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:

{
    "cluster": {
        "enabled": true,
        "roles": { "http": 4, "cron": 1 },
        "sharedStore": { "type": "redis", "namespace": "myapp" }
    }
}

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();

Worker roles

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>'

Cluster-wide visibility

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 ClusterPublishableBackendApp 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.

Leader election

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.

Graceful shutdown

When the master receives a shutdown signal (SIGTERM / SIGINT by default):

  1. Respawn is disabled — workers that exit during shutdown are not replaced.
  2. Each worker receives SIGTERM, triggering BackendApp's async-exit-hook which runs ServiceManager.stopAll() (HTTP draining, DB connections closed, etc.).
  3. The master waits up to shutdownTimeoutMs for all workers to exit voluntarily.
  4. Any worker still alive after the timeout is killed with SIGKILL.
  5. 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.

Crash backoff & circuit breaker

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 after 5s, etc. (configurable via respawn.backoffMs).
  • Crashes are tracked across a rolling window (respawn.windowMs, default 60s).
  • If more than respawn.maxPerWindow (default 5) crashes occur within that window, the master halts the cluster with process.exit(1) so a process supervisor (systemd, Kubernetes, etc.) can restart it.

For more details see doc/cluster.md.


Features

  • 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)

Used By

  • 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.


UI Framework

To build a frontend for your FigTree API, the companion framework Bambooo can be used.

About

Figtree - Server/Backend Core

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors