Sign inSign up

nodeworks/nivaro

By nodeworks

•Updated 12 minutes ago

Headless CMS — REST + GraphQL API, React admin UI, workflows, RBAC, and extensions.

Image
Content management system
0

10K+

nodeworks/nivaro repository overview

⁠Nivaro

Headless CMS — Fastify REST + GraphQL API, React admin UI, TypeScript SDK, and a single-image Docker release.

Website⁠ · SDK on npm⁠ · Docker Hub⁠


⁠Features

  • RBAC — roles, policies, per-field permissions, workspace isolation
  • Microsoft OIDC — PKCE login, AD group sync
  • Workflow Engine — state machine with role-gated transitions, history
  • Pipeline / Owner Matrix — multi-dimensional ownership per workflow state
  • GraphQL API + Subscriptions — auto-built from metadata registry
  • Real-time — Socket.io + Redis adapter (notifications, item events)
  • Extension System — drop a folder into api/extensions/, no restart needed
  • Inngest Jobs — durable background functions (cron + event-triggered)
  • AI Features — field generation and record summarization via Claude
  • Dashboards & KPI Builder — drag-and-drop widget grid
  • Audit Log & Revisions — full snapshot + delta per mutation
  • Bulk Actions — delete, field update, workflow transition across selections
  • Comments & Mentions — per-record threaded comments with @mention notifications
  • Submission Forms — public-facing forms with token auth, rate limiting, password protection
  • Field Watches & Subscriptions — granular change notifications
  • Data Import Queue — CSV import with live progress via Socket.io
  • SLA Tracking — business-hours elapsed time, warning/breach alerts
  • Alert Engine — threshold-based alerts with cooldown + in-app/email delivery
  • Multi-Workspace — collections and roles scoped per workspace
  • Custom Queries — named, parameterized SQL endpoints with caching
  • External API Configs — managed credentials, live test panel
  • Reports, Schema Snapshots, Computed Fields

⁠Stack

LayerChoice
APIFastify v5 (TypeScript)
DatabaseKnex + MSSQL (tedious)
Authopenid-client PKCE — Microsoft OIDC
SessionsRedis (ioredis) + @fastify/session
Real-timeSocket.io + @socket.io/redis-adapter
JobsInngest self-hosted (Postgres + Redis)
Admin UIReact 19 + Vite 6 + shadcn/ui (Tailwind v3)
SDK@nivaro/sdk — ESM, fully typed
LinterBiome v2
Packagespnpm v11 workspaces

⁠Development

⁠Requirements
  • Node.js 22+
  • pnpm 11+
  • Redis (local or via Docker)
  • MSSQL database
⁠Setup
cp .env.example .env
# fill in DB_*, OIDC_*, SESSION_SECRET

pnpm install
pnpm migrate
pnpm dev
# API → http://localhost:3055
# Admin → http://localhost:3056

pnpm dev starts Redis (Docker), Inngest dev server, API, and admin concurrently.

⁠Full stack via Docker
pnpm dev:docker        # build + start all containers
pnpm dev:docker:down   # stop and remove
⁠Commands
CommandDescription
pnpm devRedis + Inngest + API (:3055) + admin (:3056)
pnpm dev:apiAPI only
pnpm dev:adminAdmin UI only
pnpm dev:redisRedis in Docker on :6379
pnpm dev:inngestInngest dev server on :8288
pnpm dev:dockerFull stack via Docker Compose
pnpm dev:wwwStatic www site via browser-sync on :3057
pnpm buildCompile API (tsc) + build admin (vite)
pnpm migrateRun pending DB migrations
pnpm migrate:rollbackRoll back last migration batch
pnpm checkBiome lint + format check
pnpm check:fixAuto-fix formatting and lint

⁠Releasing

All three release flows use git tags to trigger GitHub Actions.

⁠App image (@app-* → Docker Hub)
pnpm release patch     # bumps package.json, tags @app-x.x.x, pushes
pnpm release minor
pnpm release 2.3.0     # exact version

GitHub Actions builds Dockerfile.release and pushes to Docker Hub:

nodeworks/nivaro:2.3.0
nodeworks/nivaro:latest
⁠SDK (@sdk-* → npm)
pnpm sdk:release patch
pnpm sdk:release minor
pnpm sdk:release 1.7.0

Tags @sdk-x.x.x → GitHub Actions publishes @nivaro/sdk to npm.

⁠Website (@www-* → Vercel)
pnpm www:release patch

Tags @www-x.x.x → GitHub Actions deploys www/ to Vercel.


⁠Using the Release Image

docker pull nodeworks/nivaro:latest

Minimal docker-compose.yml for a consumer project:

services:
  nivaro:
    image: nodeworks/nivaro:latest
    ports:
      - "3055:3055"
    env_file: .env
    environment:
      REDIS_URL: redis://redis:6379
      INNGEST_BASE_URL: http://inngest:8288
      INNGEST_EVENT_KEY: ${INNGEST_SIGNING_KEY_DOCKER:-deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe}
      INNGEST_SIGNING_KEY: ${INNGEST_SIGNING_KEY_DOCKER:-deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe}
    volumes:
      - ./extensions:/app/api/extensions
      - uploads:/app/uploads
    depends_on:
      redis:
        condition: service_healthy
      inngest:
        condition: service_healthy

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: inngest
      POSTGRES_PASSWORD: inngest
      POSTGRES_DB: inngest
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U inngest -d inngest"]
      interval: 5s
      timeout: 3s
      retries: 10

  inngest:
    image: inngest/inngest:latest
    command: inngest start --host 0.0.0.0
    ports:
      - "8288:8288"
    environment:
      INNGEST_EVENT_KEY: ${INNGEST_SIGNING_KEY_DOCKER:-deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe}
      INNGEST_SIGNING_KEY: ${INNGEST_SIGNING_KEY_DOCKER:-deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe}
      INNGEST_POSTGRES_URI: postgres://inngest:inngest@postgres:5432/inngest
      INNGEST_REDIS_URI: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  uploads:
  redis_data:
  postgres_data:

Set INNGEST_SIGNING_KEY_DOCKER to a real 64-character hex string in production.

See examples/my-project/ for a full working example with a custom extension.


⁠SDK

npm install @nivaro/sdk
import { createNivaro, readItems, createItem, _eq, desc } from '@nivaro/sdk'

const nivaro = createNivaro('https://your-nivaro-host', { token: 'your-static-token' })

// List items with filter + sort
const { data, total } = await nivaro.request(
  readItems('projects', {
    filter: { status: _eq('active') },
    sort: [desc('created_at')],
    limit: 25,
  })
)

// GraphQL
const result = await nivaro.graphql(`
  query { projects(filter: { status: { _eq: "active" } }) { data { id name } total } }
`)

// Realtime
import { createRealtime } from '@nivaro/sdk'
const rt = createRealtime()
rt.connect('https://your-nivaro-host', 'your-token')
const unsub = rt.subscribe('projects', { event: 'update' }, (data) => console.log(data))

Full SDK docs: npmjs.com/package/@nivaro/sdk⁠


⁠Extensions

Drop a folder into api/extensions/<name>/ with a compiled index.js. The loader auto-discovers it on startup — no restart needed for new extensions in dev.

api/extensions/
└── my-extension/
    ├── src/index.ts
    ├── index.js         ← compiled output (loaded by Nivaro)
    ├── package.json
    └── tsconfig.json
// src/index.ts
import type { FastifyInstance } from 'fastify'
import type { Knex } from 'knex'

interface ExtensionContext {
  app: FastifyInstance
  database: Knex
  logger: FastifyInstance['log']
  hooks: {
    before(collection: string | '*', action: string | '*', fn: (...args: unknown[]) => unknown): void
    after(collection: string | '*', action: string | '*', fn: (...args: unknown[]) => unknown): void
  }
  cron: {
    schedule(id: string, expression: string, fn: () => void | Promise<void>): void
  }
  callExternalApi(nameOrId: string | number, options?: Record<string, unknown>): Promise<unknown>
}

export default {
  id: 'my-extension',
  async register({ app, database, logger, hooks, cron, callExternalApi }: ExtensionContext) {
    // Custom route
    app.register(async (f) => {
      f.get('/my-route', async () => ({ ok: true }))
    }, { prefix: '/api' })

    // Hook into mutations
    hooks.after('projects', 'create', async ({ item }) => {
      logger.info({ item }, 'project created')
    })

    // Scheduled job
    cron.schedule('daily-cleanup', '0 2 * * *', async () => {
      await database('my_temp_table').where('created_at', '<', new Date()).delete()
    })
  },
}

See api/extensions/example-inngest/ and api/extensions/example-socketio/ for more examples.


⁠Environment variables

Key variables — see .env.example for the full list.

VariableDescription
DB_HOST / DB_DATABASEMSSQL connection
DB_ENCRYPT=trueRequired for Azure SQL / most cloud MSSQL
NODE_TLS_REJECT_UNAUTHORIZED=0Required for self-signed or corporate-CA certs with the tedious MSSQL driver (it doesn't use the system CA store). Omit in production if your SQL Server has a publicly-trusted cert.
REDIS_URLe.g. redis://localhost:6379
OIDC_ISSUER / OIDC_CLIENT_ID / OIDC_CLIENT_SECRETMicrosoft OIDC
OIDC_REDIRECT_URIe.g. http://localhost:3055/api/auth/callback
SESSION_SECRET32+ character random string
COOKIE_SECURE=falseMust be false for plain HTTP (Docker default)
INNGEST_EVENT_KEY / INNGEST_SIGNING_KEYlocal for dev; real hex for production
INNGEST_SIGNING_KEY_DOCKERProduction override — prevents local leaking into Docker
PUBLIC_URLe.g. http://localhost:3055
ANTHROPIC_API_KEYOptional — enables AI features

⁠License

MIT — see LICENSE⁠.

Tag summary

Content type

Image

Digest

sha256:7a9c1dd8d…

Size

540 MB

Last updated

12 minutes ago

docker pull nodeworks/nivaro