njin

Configuration

config.ts at the project root is the single source of truth for how your site runs. It exports defineConfig({ ... }):

import { defineConfig } from "@njinlabs/njin/config";

export default defineConfig({
  port: Number(process.env.PORT ?? 3000),
  models: [() => import("./src/models/post")],
});

njin reads the file once when the server starts, so restart the server after you change it. If there is no config.ts, njin runs with its defaults.

All options

Option What it does Default
port The port the server listens on 3000
publicUrl The public address of the site, such as https://example.com The address of each request
db The default database: its path, namespace, database and auth. See Database rocksdb://data
adapters.db Another database, instead of db. See Database SurrealDB from db
adapters.file Where uploaded files are stored. See File adapters Filesystem, ./uploads
img.hosts External hosts that /img may fetch images from. See Image optimization None
models, vars, helpers, hooks, events, routes What you register, each as a list of () => import(...) Empty
plugins Installed plugins Empty
mcp redirectUris and tools for AI agents Empty
static Pages to cache as static HTML None
edgeCache A shared cache in front of njin that is told about purges None
i18n The languages of the site. See Languages None
logger Log level and destinations. See Logging info, console
permissions, admin Extra permissions and admin pages for your own project. See Plugins None
rootDir The base folder that project-relative paths resolve against The current directory

Relative paths in the configuration, such as ./uploads and rocksdb://data, resolve against the folder the server is started from. Run njin from the project folder (or from out/ after a build).

Database

SurrealDB (the default)

SurrealDB is the default, and it runs embedded in your project, so no database server is needed:

export default defineConfig({
  db: {
    path: "rocksdb://data",
    namespace: "general",
    database: "general",
  },
});

path decides where the data is:

Path Where
rocksdb://data Embedded. Stored in the data/ folder. This is the default
surrealkv://data Embedded, with another storage engine
mem:// Embedded, in memory. Everything is lost when the server stops
ws://, wss://, http://, https:// A remote SurrealDB server

An embedded database can be opened by only one process at a time. Do not run two servers on the same folder.

For a remote server, add auth with either a token or a username and password:

db: {
  path: "wss://db.example.com",
  auth: process.env.DB_TOKEN
    ? process.env.DB_TOKEN
    : { username: process.env.DB_USERNAME!, password: process.env.DB_PASSWORD! },
},

Embedded engines take no auth.

SQLite, PostgreSQL and MySQL or MariaDB

To use another database, pass its adapter in adapters.db. When you set it, the db option is ignored:

import sqliteAdapter from "@njinlabs/njin/adapters/sqlite";
import postgresAdapter from "@njinlabs/njin/adapters/postgres";
import mysqlAdapter from "@njinlabs/njin/adapters/mysql";

// SQLite: one file, no server
adapters: { db: sqliteAdapter({ path: "data/app.db" }) },

// PostgreSQL
adapters: { db: postgresAdapter({ url: process.env.DATABASE_URL, max: 10, tls: true }) },

// MySQL or MariaDB
adapters: { db: mysqlAdapter({ url: process.env.DATABASE_URL, tls: true }) },
Adapter Options
sqliteAdapter path, a file such as "data/app.db" or ":memory:". busyTimeout, how long to wait for another process's write lock, in milliseconds
postgresAdapter url, a connection string. max, the pool size. tls, true or TLS options, for anything but a local database. schema, a schema that must already exist
mysqlAdapter url (mysql://user:pass@host:3306/database), max, tls, schema, a database that must already exist. One adapter serves both MySQL and MariaDB

Use tls for any database that is not on your own machine. For MySQL 8 on a trusted network without TLS, allowPublicKeyRetrieval: true lets an account that uses caching_sha2_password log in. Prefer tls: true.

You can choose the database by environment, so that development uses one and production another:

adapters: {
  db: process.env.DATABASE_URL
    ? postgresAdapter({ url: process.env.DATABASE_URL, tls: true })
    : sqliteAdapter({ path: "data/app.db" }),
},

The tables are created and updated for you when the server starts.

Logging

njin logs with pino. Use logger() in your own code. See Events.

export default defineConfig({
  logger: {
    level: "info",
    console: true,
    streams: [{ stream: myStream, level: "error" }],
  },
});
Option What it does
level The lowest level that is logged: fatal, error, warn, info, debug, trace or silent. Default info
console Pretty-printed output in the terminal. Default true. Set it to false to log only to streams
streams Extra destinations, written as JSON lines. A stream is anything pino accepts, so ready-made pino streams plug in directly. A stream's own level can only be stricter than the logger's

An uncaught exception or unhandled promise rejection is logged as fatal, and then the process exits. Run the server under something that restarts it. See Building and deploying.

Environment variables

njin does not define any environment variables of its own. Reading them is your choice, inside config.ts. Bun loads a .env file automatically, so a value you put there is available as process.env.NAME.

The starter config.ts reads these, and the starter .env.example lists them:

Variable What it sets
PORT port
DB_PATH, DB_NAMESPACE, DB_DATABASE The SurrealDB path, namespace and database
DB_TOKEN, or DB_USERNAME and DB_PASSWORD Auth for a remote SurrealDB
IMG_HOSTS A comma-separated list for img.hosts
FILE_DIR, S3_* Used if your config.ts passes them to a file adapter. For example bunFilesystemAdapter({ dir: process.env.FILE_DIR })

Keep .env out of version control. The starter .gitignore already does.