BlunxAI BlunxAI

Documentation

Installation, configuration, API contract and technical reference for BlunxAI.
Laravel & Node.js Web SDK API REST

Overview

BlunxAI turns your database into an agentic collaborator: your users ask questions in natural language and receive answers, charts, tables and automatic insights - directly inside your application.

The principle

BlunxAI is not an external platform to which you would export your data: it is an SDK that you embed in your own infrastructure. SQL is executed locally, in read-only mode, and the multi-agent system is driven by the Blunx server (the cloud endpoint /api/v1/*).

Architecture

Your application (frontend)
The Web SDK integrates into your frontend: HTML, Laravel Blade, React, Vue, Angular, Svelte...
SDK backend
The SDK for your language (Laravel and Node.js today, Django, Go, Spring Boot... coming) serves the local API /api/blunx/* and runs SQL against your database.
Blunx server
The Blunx server (/api/v1/*) orchestrates the AI agents and talks to the LLM provider of your choice.
LLM
OpenAI, Claude, Gemini, Ollama, Groq, DeepSeek... through the OpenAI-compatible adapter or the native format.
SQL execution is local. Only query plans, schemas and metadata travel to the Blunx server - never your data rows.

Key takeaways

  • Strictly read-only: only SELECT / WITH statements are executed, guarded at several levels (see Security).
  • Backend-agnostic: the Web SDK only expects a REST contract /api/blunx/*. The Laravel and Node.js packages implement it today; Django, Go and Spring Boot SDKs will follow without any frontend change.
  • One server, several keys: the backend SDK authenticates to the Blunx server with an application key (X-Blunx-Key) and an LLM key (X-Blunx-LLM-Key).
  • Your database, your rules: MySQL, MariaDB, PostgreSQL, SQLite and SQL Server are supported; you control access by role (RBAC).
Where should you start?

Go to the Quick start section: end-to-end installation, with tabs per backend language.

Quick start

Install Blunx in your application in ~5 minutes. Choose your backend (Laravel or Node.js): the outline is identical, only the SDK changes. Each step is minimal - the details are in the guides.

What you are going to do: 1 · get your keys → 2 · install the SDK → 3 · configure the .env → 4 · connect Blunx and exclude sensitive tables → 5 · generate your database schema → 6 · integrate the interface (Web SDK) into your pages. Once in place, Blunx turns your users' questions into SQL in read-only mode against your database: it never modifies your data.

Requirements

RequirementDetails
ApplicationLaravel 9-12 (PHP 8.1+) or Node.js 18+ (Express, Fastify, Koa, NestJS, plain HTTP)
DatabaseMySQL, MariaDB, PostgreSQL, SQLite or SQL Server (your app's database)
Blunx accountAn application created in the dashboard → your application key
LLM providerAn account with an API key (OpenAI, Claude, Gemini, Ollama, Groq, DeepSeek...)

1 · Get your keys

Gather these 4 pieces of information - you will need them at step 3:

To retrieveVariableExample
Application keyBLUNX_API_KEYblunx_app_live_xxxx
LLM keyBLUNX_LLM_API_KEYsk-xxxxxxxxxxxx
Endpoint LLMBLUNX_LLM_ENDPOINThttps://api.openai.com/v1
ModelBLUNX_LLM_MODELgpt-4o
No LLM yet?

Ollama runs locally, without an account: endpoint http://localhost:11434/v1, model llama3.1. See LLM providers.

2

Install the package

Terminal Bash
composer require blunx/ai
php artisan vendor:publish --tag=blunx-config

Provider in auto-discovery, no migration to publish.

3

Fill in your .env

Add your 4 keys from step 1:

.env Env
BLUNX_API_KEY=blunx_app_live_xxxxxxxxxxxx
BLUNX_LLM_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
BLUNX_LLM_ENDPOINT=https://api.openai.com/v1
BLUNX_LLM_MODEL=gpt-4o

Then: php artisan config:clear. Your app's default database connection is used automatically.

4

Exclude the tables to ignore

At init, Blunx scans all the tables in your database. Laravel's technical tables (migrations, jobs, sessions, cache...) and the internal blunx_* tables are already excluded by default. Add your sensitive or useless tables to the excluded_tables key of the published config/blunx.php file (step 2):

config/blunx.php PHP
// Already excluded by default: migrations, failed_jobs, cache, jobs, sessions,
// password_reset_tokens, personal_access_tokens... + all blunx_* tables
'excluded_tables' => [
    'admin_logs', 'notifications', 'settings', 'audit_trail',   // ▶ your tables
],

Then clear the config cache so that blunx:init re-reads the file: php artisan config:clear.

Do not confuse with the RBAC rules

An excluded table is never scanned nor offered to the assistant. A forbidden table (forbidden_tables, file blunx_access.php) stays in the schema but is blocked according to the role. Details in Configuration.

5

Generate and lock the schema

Terminal Bash
php artisan blunx:init    # scans the database → storage/app/blunx_schema.json
php artisan blunx:setup   # locks the schema + creates the 8 tables

blunx:init calls the Blunx server to enrich the descriptions: the LLM key must be valid.

6

Add getUserRoleName() to the User model

To apply your access rules (RBAC), Blunx automatically calls Auth::user()?->getUserRoleName() on every request. You must therefore add a method named exactly getUserRoleName() to your User model - this is not an alternative: this is the method Blunx will actually run.

app/Models/User.php PHP
public function getUserRoleName(): ?string
{
    return $this->role->name ?? null;
}
What this method must do

It must return the role label of the logged-in user (a string, e.g. 'admin', 'sales_rep'), or null if they have no role. The example above assumes a role relation and a name column on your roles table. If your schema differs (direct role column, roles table with a label column...), adapt only the body of the method - keep the same name and the same return type.

7

Integrate the Web SDK

Paste this snippet where you want the Blunx assistant to appear. Two possible cases:

  • On a full page - create a dedicated view (e.g. resources/views/blunx.blade.php) with a full-screen container (height:100vh): this is exactly the example below.
  • Inside an existing page - simply add the <div id="blunx"> (with the height of your choice, e.g. height:600px) where you want it, and the same <script> tag at the end of the <body>. Nothing else to change.
resources/views/blunx.blade.php Blade
<div id="blunx" style="height:100vh;"></div>
<script src="https://cdn.jsdelivr.net/npm/@blunx/web-sdk/dist/blunx-loader.min.js"
        data-container="#blunx"
        data-view="auto"></script>

data-container="#blunx" tells the SDK which div to mount into; data-view="auto" lets the SDK choose the view. The /api/blunx/* API is served by your Laravel (same origin): data-api-base-url stays empty. The session is sent automatically.

2

Install the package

Terminal Bash
npm install blunx-ai

The .env is loaded automatically (dotenv). No migration to publish.

3

Fill in your .env

Add your 4 keys from step 1 plus your database (the BLUNX_DB_URL line). This line is read by the CLI (step 6) and by your application (step 4):

.env Env
BLUNX_API_KEY=blunx_app_live_xxxxxxxxxxxx
BLUNX_LLM_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
BLUNX_LLM_ENDPOINT=https://api.openai.com/v1
BLUNX_LLM_MODEL=gpt-4o
# Your database (ADAPT) - mysql://user:password@host:port/database
BLUNX_DB_URL=mysql://root:secret@127.0.0.1:3306/my_database

Other supported databases: postgres://..., sqlite:///path/to/database.sqlite, sqlsrv://....

4

Initialize Blunx in your server

Two possible situations - pick the one that fits you:

Case A - You already have an Express server (the most common)

No need to create a file or touch your entry point. Simply add these 3 blocks to your existing server code:

Your existing server file JavaScript
const { Blunx, blunxExpress } = require('blunx-ai');   // 1. import

// 2. Blunx runtime - keys (BLUNX_API_KEY, BLUNX_LLM_*) and database (BLUNX_DB_URL)
//    read from the .env. The 2 hooks below are MANDATORY:
const blunx = Blunx.init({
  getUserRole: (userId) => null,   // TODO: return the role label (e.g. 'admin')
  getUser: (userId) => null,       // TODO: return { name, email } or null
});

// 3. Mount the Blunx router AFTER your authentication middleware
//    (the latter must have populated req.user beforehand)
app.use('/api/blunx', blunxExpress(blunx));
A concrete example of the 2 hooks

Typical implementation to adapt (here a users table):

Hooks - example to adapt JavaScript
getUserRole: (userId) => users[userId]?.role ?? null,
getUser: (userId) => {
  const u = users[userId];
  return u ? { name: u.name, email: u.email } : null;
},
Case B - You start from scratch (no existing server)

Create a complete app.js file (Express). Adapt the 2 places marked ADAPT then run node app.js: the file handles auth, mounts Blunx and starts the server.

app.js - Express (complete file) JavaScript
const express = require('express');                   // HTTP framework
const { Blunx, blunxExpress } = require('blunx-ai');   // Blunx SDK

const app = express();
app.use(express.json());   // parses the JSON body

// ── Demo users (ADAPT: come from your users table) ──
const USERS = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'   },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

// ── Authentication (ADAPT: Passport, JWT, session...) ──
// Blunx returns 401 if req.user is missing: your auth middleware
// must run BEFORE the Blunx router and populate req.user.
app.use((req, res, next) => {
  req.user = USERS[1];   // demo: user 1 is logged in
  next();
});

// ── Blunx runtime (keys + database read from the .env) ──
const blunx = Blunx.init({
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

// ── Blunx router mounted AFTER auth ──
app.use('/api/blunx', blunxExpress(blunx));

// ── Startup ──
app.listen(3000, () => console.log('Blunx ready on http://localhost:3000'));

Other frameworks (Fastify, Koa, NestJS, plain HTTP): complete files in Node.js backend.

Option (advanced) - Manage the configuration in TypeScript files

You do not need it to get started: the .env is enough. This option does not replace the integration above - it only changes the source of the configuration: instead of passing everything to Blunx.init(), you load 2 versionable TypeScript files (the equivalent of vendor:publish). The hooks and the router mounting stay identical:

Terminal Bash
cp node_modules/blunx-ai/config/blunx.config.ts ./config/blunx.config.ts
cp node_modules/blunx-ai/config/blunx-access.config.ts ./config/blunx-access.config.ts
app.ts — TypeScript TypeScript
import blunxConfig from './config/blunx.config';            // your edited copy
import blunxAccessConfig from './config/blunx-access.config'; // your RBAC rules

const blunx = Blunx.init({
  ...blunxConfig,             // serverUrl, apiKey, llm, currency, locale...
  access: blunxAccessConfig,  // RBAC rules
  // hooks identical to those in Case A / B above (USERS):
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

Two files for two roles: blunx.config.ts = general runtime configuration (keys, server, LLM, currency, language); blunx-access.config.ts = your RBAC access rules. Details in Configuration.

5

Exclude the tables to ignore

At init, Blunx scans all the tables in your database. By default, only the internal blunx_* tables are ignored. Add your technical tables (migrations, sessions...) and sensitive ones (logs, notifications, settings...) to the excludedTables option, by editing the config/blunx.config.ts file loaded in Blunx.init():

config/blunx.config.ts TypeScript
export default {
  // Internal blunx_* tables are already ignored by default.
  // ▶ Add your technical / sensitive tables that must not be scanned:
  excludedTables: ['migrations', 'sessions', 'log_errors', 'admin_logs', 'notifications'],
};
Do not confuse with the RBAC rules

An excluded table is never scanned nor offered to the assistant. A forbidden table (forbidden_tables, file blunx-access.config.ts) stays in the schema but is blocked according to the role. Details in Configuration.

6

Generate and lock the schema

Terminal Bash
npx blunx-ai init    # scans the database → storage/app/blunx_schema.json
npx blunx-ai setup   # locks the schema + creates the 8 tables

Run it from the root of your project: the CLI reads the .env to build its connection.

7

Integrate the Web SDK

index.html HTML
<div id="blunx" style="height:100vh;"></div>
<script src="https://cdn.jsdelivr.net/npm/@blunx/web-sdk/dist/blunx-loader.min.js"
        data-container="#blunx"
        data-view="auto"></script>

The /api/blunx/* API is served by your Node app (same origin): data-api-base-url stays empty.

Check that everything works

Log in with a user of your application and open the page. Ask a question (e.g. "How many customers do we have in total?"): the cockpit displays the pipeline then the result.

401 or 422 error?

It is almost always the BLUNX_API_KEY / BLUNX_LLM_API_KEY keys or an incomplete LLM configuration. See Errors & troubleshooting.

Guide complet

To go further (database, jobs, auth, RBAC...): Laravel backend or Node.js backend.

Compatibility

Supported versions and environments, per integration layer. Backend SDKs: Laravel and Node.js are available, the others will follow.

Databases

Blunx works with your application's connection and runs SQL in read-only mode. The following databases are supported by the Laravel SDK and by the Blunx server:

Databasedb_connection valueNotes
MySQLmysqlDefault
MariaDBmariadbHandled as MySQL on the SDK side
PostgreSQLpgsqlScan limited to the BLUNX_PGSQL_SCHEMA schema (default public)
SQLitesqliteLocal file
SQL ServersqlsrvDriver sqlsrv / sqlserver

Backend (server SDK)

TechnologyStatusNotes
Laravel (package blunx/ai)● AvailableLaravel 9 to 12 (tested on 12), PHP 8.1+
Node.js (package blunx-ai)● AvailableNode.js 18+, npm, same /api/blunx/* contract
Django (Python)● Coming soonSame /api/blunx/* contract
Go● Coming soonSame /api/blunx/* contract
Spring Boot (Java)● Coming soonSame /api/blunx/* contract

Frontend (Web SDK @blunx/web-sdk)

TechnologyMethodConstraint
Plain HTML / PHP / Laravel Blade<script> + loader (auto-update)None
React (Vite, CRA, Next.js)npm install + init()/destroy()Next.js: client side only ('use client' / dynamic(..., {ssr:false}))
Vue 3 (Vite, Nuxt)npm install + init()/destroy()Call init() inside onMounted
Angularnpm install + init()/destroy()Call init() inside ngAfterViewInit
Svelte / SvelteKitnpm install + init()/destroy()Client-side loading only
Isolated widget (iframe)Host page embedding the SDKMaximum isolation recommended

LLM providers

The openai driver is an OpenAI-compatible adapter: it works with almost every provider, proprietary AND open-source. See LLM providers for details.

DriverProviders
openaiOpenAI, Azure OpenAI, Ollama (local), Groq, DeepSeek, Mistral, Together, Fireworks, OpenRouter, vLLM, LM Studio, llama.cpp...
anthropicAnthropic Claude (native format /v1/messages)
geminiGoogle Gemini (native generateContent format)

Browsers & environments

ItemSupport
BrowsersChrome, Edge, Firefox, Safari (ES2019+, no IE)
BundlersVite, webpack, esbuild, Rollup (ESM module)
SSRNot supported natively - must run client side only
CDNjsDelivr (loader + bundle), unpkg

Laravel backend

The blunx/ai package serves the /api/blunx/* contract expected by the Web SDK, runs SQL in read-only mode against your database and proxies to the Blunx server. Requirements: Laravel 9 to 12 (tested on 12), PHP 8.1+.

Installation

Terminal Bash
composer require blunx/ai
php artisan vendor:publish --tag=blunx-config
  • Provider in auto-discovery - no manual declaration.
  • No migration to publish: the 8 Blunx tables are created by blunx:setup.

Configuration

Two files published into config/: blunx.php (read from your BLUNX_* variables) and blunx_access.php (RBAC rules). The full variable reference is in Configuration.

The getUserRoleName() method is mandatory

The package calls Auth::user()?->getUserRoleName() on every request to resolve the access rules. This method must therefore exist with exactly that name on your User model and return the user's role label (a string, or null if they have no role). The example below assumes a role relation with a name column - if your schema differs, adapt the body of the method but keep the name and the return type:

app/Models/User.php PHP
public function getUserRoleName(): ?string
{
    return $this->role->name ?? null;
}

Artisan commands

CommandPurpose
blunx:initScans the database and generates storage/app/blunx_schema.json (enriched through the Blunx server).
blunx:editManual editing of descriptions or re-indexing after the database evolves.
blunx:setupLocks the schema and creates the 8 Blunx tables (idempotent).
blunx:feedbackReviews unresolved user feedback.
blunx:run-insightsImmediately runs the "due" insights (next_run_at passed), with retries - the manual equivalent of the hourly scheduler. If it still fails after the retries, the job retries in 30 min.

Workflow: blunx:init → (optional) blunx:edit → blunx:setup. Re-run blunx:edit after every change to your database.

Tables created

All of them have an auto-increment id + a unique uuid (routes are bound by uuid):

TablePurpose
blunx_conversationsChat conversations
blunx_messagesMessages (user|assistant)
blunx_feedbacksUser feedback
blunx_dashboardsDashboards
blunx_dashboard_widgetsWidgets (chart|table) + reference_queries
blunx_insightsInsights + report_pdf
blunx_widget_insight_settingsPer-widget insight settings
blunx_cacheCache (including the 48 h schema_id)

Authentication & routes

All /api/blunx/* routes are in the blunx middleware group = session + auth (logged-in user required, no CSRF) + throttle:60,1. Nothing to configure: an unauthenticated request is blocked by the framework (401/redirect).

blunx.dashboard route

The email view references route('blunx.dashboard', ...). That route is not defined by the package: define it in your application.

Queue & insights

  • GenerateWidgetInsightJob: ShouldQueue, tries=5, timeout=200 - runs the widget, generates the PDF report and the email.
  • BlunxInsightMail: email colour-coded by severity, PDF attached.
  • Scheduling: hourly for "due" settings; a new setting runs immediately.
  • Failure after retries (e.g. server call in error): the job does not advance to the next slot - it retries in 30 min (next_run_at = now + 30 min, last_run_at unchanged until the job succeeds).
  • Locally: php artisan queue:work --once (or QUEUE_CONNECTION=sync in dev).
Next steps

The step-by-step integration guide is in Quick start. The endpoint reference is in API contract, and errors in Errors & troubleshooting.

Node.js backend

The blunx-ai package serves the /api/blunx/* contract expected by the Web SDK, runs read-only SQL against your database and proxies to the Blunx server. Requirements: Node.js 18+, framework-agnostic (Express, Fastify, Koa, NestJS or native HTTP).

Installation

Terminal Bash
npm install blunx-ai
  • The .env file is loaded automatically (dotenv) - no config publishing required.
  • No migration to publish: the 8 Blunx tables are created by blunx-ai setup.

Initialization

Create the Blunx runtime once, with your authentication hooks. Configuration precedence: Blunx.init() option > BLUNX_* environment variable > default (see Configuration).

Your database connection can be provided in 3 ways (from the simplest to the most integrated):

MethodHowWhen to use it
BLUNX_DB_URL in the .env (recommended)A connection URL, read by the CLI and the runtimeMost cases - a single config for everything.
database: { client, connection }Connection config passed to Blunx.init()When the CLI is not used (runtime only).
knex: knexInstanceYour existing knex instanceYour app already uses knex - we reuse it.
app.js - initialization (keys + database from .env) JavaScript
const { Blunx } = require('blunx-ai');

// Keys (BLUNX_API_KEY, BLUNX_LLM_*) and database (BLUNX_DB_URL) are read from .env.
// Sample user resolution (ADAPT THIS: load from your own database):
const USERS = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'    },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

const blunx = Blunx.init({
  // Resolves the user role (RBAC) - equivalent to Laravel's getUserRoleName()
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  // Resolves {name, email} for /api/blunx/user - equivalent to Laravel's Auth::user()
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

Option (advanced) - publish and load the config files

You do not need this to get started: the .env file is enough. This option does not replace the initialization above - it only changes the source of the configuration. Use it if you want to version your settings and RBAC rules in TypeScript files rather than in the .env file. Copy the 2 templates (no dependency), edit them, then load them in Blunx.init():

Terminal Bash
cp node_modules/blunx-ai/config/blunx.config.ts ./config/blunx.config.ts
cp node_modules/blunx-ai/config/blunx-access.config.ts ./config/blunx-access.config.ts
app.ts - load the files TypeScript
import blunxConfig from './config/blunx.config';            // your edited copy
import blunxAccessConfig from './config/blunx-access.config'; // your RBAC rules

const blunx = Blunx.init({
  ...blunxConfig,             // serverUrl, apiKey, llm, currency, locale...
  access: blunxAccessConfig,  // RBAC rules
  // same hooks as above (USERS):
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

The database is passed through BLUNX_DB_URL (.env) or the database option. RBAC rules are detailed in Configuration.

Commands (CLI blunx-ai)

CommandPurpose
npx blunx-ai initScans the database and generates storage/app/blunx_schema.json (enriched through the Blunx server).
npx blunx-ai editManual editing of descriptions or re-indexing after a database change.
npx blunx-ai setupLocks the schema and creates the 8 Blunx tables (idempotent).
npx blunx-ai feedbackReviews unresolved user feedback.
npx blunx-ai run-insightsRuns the "due" insights right now (equivalent to the hourly scheduler). If it fails after the retries, the job retries in 30 min.

Workflow: blunx-ai init → (optional) blunx-ai edit → blunx-ai setup. The CLI builds its own connection from the .env file (BLUNX_DB_URL or BLUNX_DB_*) - run it from the root of your project.

Created tables

Identical to the Laravel package: blunx_conversations, blunx_messages, blunx_feedbacks, blunx_dashboards, blunx_dashboard_widgets, blunx_insights, blunx_widget_insight_settings, blunx_cache (all with a unique id + uuid).

Authentication & routes

The package protects the routes itself: with no authenticated user (req.user / ctx.state.user missing), it returns 401 {"message":"Unauthenticated."} - equivalent to the auth middleware of Laravel's blunx group. No requireAuth guard to add.

app.js — Express JavaScript
const express = require('express');
const { Blunx, blunxExpress } = require('blunx-ai');

const app = express();
app.use(express.json());

// Sample users (ADAPT THIS: load from your own database)
const USERS = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'    },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

// 1. Authentication: your auth middleware (Passport, JWT, session...)
//    populates req.user. Demo example:
app.use((req, res, next) => {
  req.user = USERS[1];
  next();
});

const blunx = Blunx.init({
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

// 2. Blunx mounted AFTER: it checks req.user and returns 401 when it is missing
app.use('/api/blunx', blunxExpress(blunx));

app.listen(3000);

Blunx reads req.user.id (user_id filtering), req.user.name/email (the /user endpoint) and optionally req.user.getUserRoleName() (otherwise your getUserRole hook is called). Other adapters: blunxFastify, blunxKoa, createBlunxNestController, blunxNodeHttp.

Rate limit

Laravel applies throttle:60,1. In Node, add your rate limiter on the Blunx router (e.g. express-rate-limit). Do not compress the /stream response (SSE).

Insight scheduling

  • Scheduler (node-cron): every hour, it finds the "due" settings and runs GenerateWidgetInsightJob. Enabled by default (enableScheduler: true), it can be disabled through Blunx.init({ enableScheduler: false }).
  • npx blunx-ai run-insights: immediate manual run.
  • Failure after retries (e.g. a failing server call): the job does not move to the next slot - it retries in 30 min (next_run_at = now + 30 min, last_run_at unchanged until the job succeeds).
  • sendInsightMail (nodemailer): email colored by severity, PDF as an attachment (generated by InsightPdfGenerator - 100% JS rendering, no browser).
Email links (dashboardUrl) & PDF

In Laravel this is the blunx.dashboard route; in Node, provide the dashboardUrl: (uuid) => '...' hook to Blunx.init(). For the PDF, no browser or path to configure: HTML→PDF rendering through Puppeteer (bundled Chromium, no Chrome installed on the machine), like dompdf on the PHP side.

Complete examples per framework

Each example is a complete, copy-pastable file: sample users, authentication, runtime and startup. Replace the blocks marked ADAPT THIS (database, auth, roles) with your real data. Keys + database in the .env file.

app.js — Express JavaScript
const express = require('express');                   // HTTP framework
const { Blunx, blunxExpress } = require('blunx-ai');   // Blunx SDK

const app = express();
app.use(express.json());   // parses the JSON body

// -- Sample users (ADAPT THIS: load from your users table) --
const USERS = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'    },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

// -- Authentication (ADAPT THIS: Passport, JWT, session...) --
app.use((req, res, next) => {
  req.user = USERS[1];   // demo: user 1 is signed in
  next();
});

// -- Blunx runtime (keys + database from .env) --
const blunx = Blunx.init({
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

// -- Blunx router mounted AFTER authentication --
app.use('/api/blunx', blunxExpress(blunx));

app.listen(3000, () => console.log('Blunx ready on http://localhost:3000'));
app.js — Fastify JavaScript
const fastify = require('fastify');
const { Blunx, blunxFastify } = require('blunx-ai');

const app = fastify();

// Sample users (ADAPT THIS)
const USERS = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'    },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

// Authentication (ADAPT THIS): populates req.user before the Blunx router
app.addHook('onRequest', (req, reply, done) => {
  req.user = USERS[1];
  done();
});

// Blunx runtime (keys + database from .env)
const blunx = Blunx.init({
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

// Mounting the Blunx router
blunxFastify(app, blunx);

app.listen({ port: 3000 }).then(() => console.log('Blunx ready on http://localhost:3000'));
app.js — Koa JavaScript
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');   // required: parses the JSON body
const { Blunx, blunxKoa } = require('blunx-ai');

const app = new Koa();
app.use(bodyParser());

// Sample users (ADAPT THIS)
const USERS = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'    },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

// Authentication (ADAPT THIS): populates ctx.state.user before the Blunx router
app.use(async (ctx, next) => {
  ctx.state.user = USERS[1];
  await next();
});

// Blunx runtime (keys + database from .env)
const blunx = Blunx.init({
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

// Mounting the Blunx router
app.use(blunxKoa(blunx));

app.listen(3000, () => console.log('Blunx ready on http://localhost:3000'));
blunx.module.ts — NestJS TypeScript
import { Module } from '@nestjs/common';
import { Blunx, createBlunxNestController } from 'blunx-ai';

// Sample users (ADAPT THIS)
const USERS: Record<number, any> = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'    },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

// Blunx runtime (keys + database from .env)
const blunx = Blunx.init({
  getUserRole: (userId: any) => USERS[userId]?.role ?? null,
  getUser: (userId: any) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

// Blunx controller (your Nest auth guard must populate req.user - ADAPT THIS)
const BlunxController = createBlunxNestController(blunx);

@Module({ controllers: [BlunxController] })
export class BlunxModule {}
server.js - native HTTP JavaScript
const http = require('http');
const { Blunx, blunxNodeHttp } = require('blunx-ai');

// Sample users (ADAPT THIS)
const USERS = {
  1: { name: 'Amir',  email: 'amir@x.com',  role: 'admin'    },
  2: { name: 'Sarah', email: 'sarah@x.com', role: 'sales_rep' },
};

// Blunx runtime (keys + database from .env)
const blunx = Blunx.init({
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

http.createServer((req, res) => {
  // Authentication (ADAPT THIS): populates req.user before the Blunx router
  req.user = USERS[1];

  if (blunxNodeHttp(blunx, req, res)) return;   // request handled by Blunx
  res.writeHead(404).end();
}).listen(3000, () => console.log('Blunx ready on http://localhost:3000'));
Next steps

The step-by-step integration guide is in Quick start. The endpoint reference is in API contract, and the errors in Errors & troubleshooting.

Web SDK

The Web SDK (@blunx/web-sdk) is a self-contained, embeddable front-end application: chat + canvas cockpit, dashboards and insights. It mounts into a plain DOM container. Integration is identical whatever your backend (Laravel, Node.js...).

Distribution formats

FileFormatUsage
dist/blunx-loader.min.jsIIFEPlain <script>, auto-update (always the latest version)
dist/blunx.min.jsIIFEDirect <script> → window.Blunx
dist/blunx.esm.jsESMnpm install @blunx/web-sdk → import { init }
Automatic locale - nothing to configure

The language is retrieved from /api/blunx/user (the locale field). Do not pass locale at initialization: the server value is authoritative.

Integration per framework

The SDK mounts into a plain <div>. Two integrations are possible: on a full page (full-screen container, e.g. height:100vh) or inside an existing page (place the div where you need it, with the height of your choice, and the <script> tag at the end of the <body>). In both cases the snippet is the same - only the container changes.

Loader (recommended, automatic updates):

index.html / Vue Blade HTML
<div id="blunx" style="height:100vh;"></div>
<script src="https://cdn.jsdelivr.net/npm/@blunx/web-sdk/dist/blunx-loader.min.js"
        data-container="#blunx"
        data-view="auto"></script>

Direct bundle (pinned version):

index.html HTML
<div id="blunx" style="height:100vh;"></div>
<script src="https://cdn.jsdelivr.net/npm/@blunx/web-sdk/dist/blunx.min.js"></script>
<script>
  Blunx.init({ container: '#blunx', view: 'auto' })
    .then(function (info) { console.log('[Blunx] ready', info); })
    .catch(function (err) { console.error('[Blunx] failed', err); });
</script>

Since the API is served by your backend (same origin), apiBaseUrl stays empty. The session cookie is sent automatically.

Terminal Bash
npm install @blunx/web-sdk
src/App.jsx JSX
import { useEffect } from 'react';
import { init, destroy, navigate } from '@blunx/web-sdk';

export default function App() {
    useEffect(() => {
        let mounted = true;

        init({
            container: '#blunx',
            apiBaseUrl: '', // cross-origin - leave empty for same origin
            view: 'auto',
            onReady: (info) => console.log('[React] Blunx ready', info),
            onError: (err) => console.error('[React] Blunx error', err),
        })
            .then((info) => { if (mounted) console.log('[React] mounted', info); })
            .catch((err) => console.error('[React] init failed', err));

        return () => { mounted = false; destroy(); }; // unmount (StrictMode-safe)
    }, []);

    return (
        <div className="host">
            <header className="host-top">
                <strong>React host</strong>
                <span>SDK mounted in <code>#blunx</code></span>
            </header>
            <div id="blunx" className="blunx-zone" />
        </div>
    );
}

The SDK touches window/document at evaluation time: client-side only. In the App Router we therefore import it dynamically inside a useEffect (a 'use client' component).

src/app/page.js JS — App Router
'use client';

import { useEffect, useRef } from 'react';

export default function Home() {
    const apiRef = useRef(null);

    useEffect(() => {
        let mounted = true;

        import('@blunx/web-sdk')
            .then(({ init, destroy, navigate }) => {
                apiRef.current = { destroy, navigate };
                return init({
                    container: '#blunx',
                    apiBaseUrl: '', // cross-origin - leave empty for same origin
                    view: 'auto',
                    onReady: (info) => console.log('[Next] Blunx ready', info),
                    onError: (err) => console.error('[Next] Blunx error', err),
                });
            })
            .then((info) => { if (mounted) console.log('[Next] mounted', info); })
            .catch((err) => console.error('[Next] init failed', err));

        return () => { mounted = false; apiRef.current?.destroy?.(); };
    }, []);

    return (
        <main className="host">
            <header className="host-top">
                <strong>Next.js host</strong>
                <span>SDK imported dynamically (SSR-safe)</span>
            </header>
            <div id="blunx" className="blunx-zone" />
        </main>
    );
}
Terminal Bash
npm install @blunx/web-sdk
src/App.vue Vue
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { init, destroy, navigate } from '@blunx/web-sdk';

onMounted(async () => {
    try {
        const info = await init({
            container: '#blunx',
            apiBaseUrl: '', // cross-origin - leave empty for same origin
            view: 'auto',
            onReady: (i) => console.log('[Vue] Blunx ready', i),
            onError: (e) => console.error('[Vue] Blunx error', e),
        });
        console.log('[Vue] mounted', info);
    } catch (e) {
        console.error('[Vue] init failed', e);
    }
});

onUnmounted(() => {
    destroy(); // unmounts the SDK when the component is removed
});
</script>

<template>
    <div class="host">
        <header class="host-top">
            <strong>Vue 3 host</strong>
            <span>SDK mounted in <code>#blunx</code></span>
        </header>
        <div id="blunx" class="blunx-zone" />
    </div>
</template>
src/app/app.component.ts TypeScript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { init, destroy, navigate } from '@blunx/web-sdk';

@Component({
    selector: 'app-root',
    standalone: true,
    template: `
        <div class="host">
            <header class="host-top">
                <strong>Angular host</strong>
                <span>SDK mounted in <code>#blunx</code></span>
            </header>
            <div id="blunx" class="blunx-zone"></div>
        </div>
    `,
})
export class AppComponent implements OnInit, OnDestroy {
    async ngOnInit(): Promise<void> {
        try {
            const info = await init({
                container: '#blunx',
                apiBaseUrl: '', // cross-origin - leave empty for same origin
                view: 'auto',
                onReady: (i) => console.log('[Angular] Blunx ready', i),
                onError: (e) => console.error('[Angular] Blunx error', e),
            });
            console.log('[Angular] mounted', info);
        } catch (e) {
            console.error('[Angular] init failed', e);
        }
    }

    ngOnDestroy(): void {
        destroy(); // unmounts the SDK when the component is destroyed
    }

}
src/App.svelte Svelte
<script>
    import { onMount, onDestroy } from 'svelte';
    import { init, destroy, navigate } from '@blunx/web-sdk';

    onMount(async () => {
        try {
            const info = await init({
                container: '#blunx',
                apiBaseUrl: '', // cross-origin - leave empty for same origin
                view: 'auto',
                onReady: (i) => console.log('[Svelte] Blunx ready', i),
                onError: (e) => console.error('[Svelte] Blunx error', e),
            });
            console.log('[Svelte] mounted', info);
        } catch (e) {
            console.error('[Svelte] init failed', e);
        }
    });

    onDestroy(() => {
        destroy(); // unmounts the SDK when the component is destroyed
    });
</script>

<div class="host">
    <header class="host-top">
        <strong>Svelte host</strong>
        <span>SDK mounted in <code>#blunx</code></span>
    </header>
    <div id="blunx" class="blunx-zone"></div>
</div>

Public API & options

Public API JS
import { init, destroy, navigate, version } from '@blunx/web-sdk';

init(options)  // → Promise<{ view: 'cockpit'|'dashboard', version }>
destroy()      // unmounts and cleans up the hashchange listener + the container content
navigate(path) // internal navigation, e.g. navigate('/dashboard/abc-123') → '#/dashboard/abc-123'
version        // string, e.g. "0.1.0"
OptionTypeDefaultDescription
containerstring | HTMLElement'#blunx'Selector / element the application mounts into
apiBaseUrlstring''API base (empty = same origin, recommended)
view'auto' | 'cockpit' | 'dashboard''auto'Initial view ('auto' follows the URL hash)
dashboardUuidstring | nullnullDashboard UUID when view: 'dashboard'
onReadyfunctionnullCalled after the first mount: (info) => {}
onErrorfunctionnullCalled if init() fails (container, routing). The promise also rejects.

With a script tag, options are passed as data-* attributes: data-container, data-api-base-url, data-view, data-dashboard-uuid.

Pitfalls & best practices

Container & height

The selector must exist when init() runs, and the container must have a height (height:100vh or 70vh), otherwise the rendering may collapse.

SSR / Next.js

The module accesses window/document at evaluation time: import it on the client side only ('use client' or ssr: false).

Hash conflict

The SDK reacts to every hashchange. If your app already uses the hash for its own routing, a conflict may occur.

Cross-origin: session on the stream

In cross-origin mode (apiBaseUrl on another domain), the /api/blunx/stream fetch does not send credentials. Prefer a same-origin setup.

Host page dependency versions

If the host page already loads jQuery/Bootstrap/DataTables/ECharts, the SDK reuses them. An incompatible version (e.g. Bootstrap 4) silently degrades some components.

Configuration

This page is the full reference: every environment variable, every Blunx.init() option and the RBAC access rules. In most cases, the .env is enough.

The 3 ways to configure

MethodLaravelNode.jsWhen to use it
.env (recommended) Loaded by Laravel Loaded automatically (dotenv) Almost every case: keys, LLM, database.
config/blunx.php ↔ Blunx.init() config/blunx.php Blunx.init({...}) options To force a value (currency, exclusions...) or pass the RBAC rules.
RBAC rules config/blunx_access.php init({ access }) File/option only - no environment variable.
Reloading the configuration

Laravel: php artisan config:clear. Node: nothing to do (the .env is re-read on every start).

Environment variables

Alternative column: blunx.* key (Laravel) / init() option (Node).

VariableDefaultPurposeAlternative (Laravel → Node)
BLUNX_SERVER_URLhttps://blunxai.comBlunx server URL (/api/v1/*)server_url → serverUrl
BLUNX_REQUEST_TIMEOUT600Timeout (s) for each request to the Hub (SSE chat + JSON) - 0 = none (a heavy question that makes the LLM think is never cut off)request_timeout → requestTimeout
BLUNX_API_KEY—Application key → X-Blunx-Keyapi_key → apiKey
BLUNX_LLM_API_KEY—LLM key → X-Blunx-LLM-Keyllm_api_key → llmApiKey
BLUNX_LLM_DRIVERopenaiopenai (compatible: Ollama, Groq, DeepSeek...) | gemini | anthropicllm.driver → llm.driver
BLUNX_LLM_ENDPOINT—Your provider's endpointllm.endpoint → llm.endpoint
BLUNX_LLM_MODEL—Model used by the AIllm.model → llm.model
BLUNX_LLM_SUPPORTS_JSON_FORMATtrueDoes the model support response_format: json_object? (false on older local models)llm.supports_json_format → llm.supportsJsonFormat
BLUNX_DB_CONNECTIONmysqlSQL dialect sent to the Blunx server (the real local connection comes from your app)db_connection → dbConnection
BLUNX_DB_URL—Connection URL (DSN): overrides the default connection, SSL possibledb_url → databaseUrl
BLUNX_READONLY_DB_USERNAME / PASSWORD—Read-only database role (optional - see connection modes)readonly_db → readonlyDb
BLUNX_PGSQL_SCHEMApublicPostgreSQL schema scannedpgsql_schema → pgsqlSchema
BLUNX_CURRENCY_CODE / SYMBOL / LOCALEEUR / € / en_USAmount display formatcurrency.* → currency.*
BLUNX_LOCALEenLanguage of prompts, messages and viewslocale → locale
BLUNX_SCHEMA_PATHstorage/app/blunx_schema.jsonPath of the schema file— → schemaPath

The tables excluded from the scan (excluded_tables in Laravel, excludedTables in Node) and the RBAC rules (access) have no environment variable: they are passed by option/file (see the sections below).

The 4 essential keys

BLUNX_API_KEY, BLUNX_LLM_API_KEY, BLUNX_LLM_ENDPOINT and BLUNX_LLM_MODEL. Everything else has a usable default.

Blunx.init() - every option (Node.js)

Complete reference of what you can pass to Blunx.init(). Priority: option > environment variable > default. The TypeScript template config/blunx.config.ts (exported, dependency-free) uses these keys in camelCase; snake_case keys (server_url, api_key, db_url...) are also accepted (mirroring config/blunx.php).

OptionTypeDefaultEnvDescription
enabledbooleantrue—Enables / disables the SDK
serverUrlstringhttps://blunxai.comBLUNX_SERVER_URLBlunx Hub URL (/api/v1/*)
requestTimeoutnumber600BLUNX_REQUEST_TIMEOUTTimeout (s) for each request to the Hub (SSE + JSON) - 0 = no timeout
apiKeystring | nullnullBLUNX_API_KEYApplication key → header X-Blunx-Key
llmApiKeystring | nullnullBLUNX_LLM_API_KEYLLM key → header X-Blunx-LLM-Key
dbConnectionstring | nullnullBLUNX_DB_CONNECTIONSQL dialect sent to the Hub (mysql, pgsql...) - not the local connection
databaseUrlstring | nullnullBLUNX_DB_URLDatabase connection URL (DSN) - takes precedence (mysql://, postgres://, sqlite://...)
readonlyDb{ username, password }{ null, null }BLUNX_READONLY_DB_*Read-only database role (optional)
pgsqlSchemastringpublicBLUNX_PGSQL_SCHEMAPostgreSQL schema scanned by init/edit
llm{ driver, endpoint, model, supportsJsonFormat }openai / null / null / trueBLUNX_LLM_*LLM config relayed to the Hub (keys stay in headers)
users{ userTable, primaryKey }users / id—Host user table and key
currency{ code, symbol, locale }EUR / € / en_USBLUNX_CURRENCY_*Amount display format
excludedTablesstring[]blunx_* (8)—Tables ignored during the schema scan (see the dedicated section below)
localestringenBLUNX_LOCALELanguage of messages and views
tables{ conversations, messages, feedbacks, dashboards, widgets, insights, insightSettings, cache }blunx_*—Custom Blunx table names
schemaPathstringstorage/app/blunx_schema.jsonBLUNX_SCHEMA_PATHPath of the schema file
accessAccessConfigdefault—RBAC rules (see below)
knexinstance——Host's knex instance (main connection)
readonlyKnexinstance——Read-only knex instance
database{ client, connection, useNullAsDefault, pool }——Knex connection config (if no instance is provided)
getUserRole(userId) => string | null——Resolves the user's role - equivalent of getUserRoleName()
getUser(userId) => { name, email } | null——Resolves name/email for /api/blunx/user - equivalent of Auth::user()
dashboardUrl(uuid) => string——Dashboard URL (the "View dashboard" link in emails)
mailMailOptions——Email SMTP: host, port, secure, user, pass, from, transport (nodemailer)
enableSchedulerbooleantrue—Enables the hourly insight scheduler
Important rules
  • apiKey / llmApiKey are never sent in the body (headers only).
  • dbConnection is not the local connection: it is the dialect sent to the Hub. The real connection comes from databaseUrl, database or knex.
  • readonlyDb: if username is empty, Blunx falls back to the default connection (defense in depth).

Loading the config files (Node.js)

Complete example: we publish config/blunx.config.ts + config/blunx-access.config.ts (TypeScript templates, no dependency), load them, and add the database + the hooks:

app.ts TypeScript
import blunxConfig from './config/blunx.config';            // your edited copy
import blunxAccessConfig from './config/blunx-access.config'; // your RBAC rules

const blunx = Blunx.init({
  ...blunxConfig,          // serverUrl, apiKey, llm, currency, locale...
  access: blunxAccessConfig,  // RBAC rules
  // Database: via BLUNX_DB_URL in the .env (recommended), or:
  // database: { client: 'mysql2', connection: { host, user, password, database } },
  getUserRole: (userId) => USERS[userId]?.role ?? null,
  getUser: (userId) => {
    const u = USERS[userId];
    return u ? { name: u.name, email: u.email } : null;
  },
});

RBAC access rules (access)

NEGATIVE approach: you define what the user is NOT allowed to see. In Laravel: config/blunx_access.php. In Node: the access option of Blunx.init() (file config/blunx-access.config.ts). No environment variable.

Three levels of restriction per role:

LevelTypePurpose
forbidden_tablesstring[]Tables entirely forbidden to the role
forbidden_columns{ table: string[] }Forbidden columns per table ("*" = all tables)
row_level{ table: { column, value } }Automatic row filtering (e.g. WHERE user_id = X)

Complete example (file config/blunx-access.config.ts):

config/blunx-access.config.ts TypeScript
export default {
  "sales_rep": {
    "forbidden_tables": ["salaries", "accounting"],
    "forbidden_columns": {
      "users": ["salary", "ssn", "full_address"],
      "*": ["password", "api_token"]
    },
    "row_level": {
      "orders":   { "column": "user_id", "value": "USER_ID" },
      "products": { "column": "vendor_id", "value": "USER_ID" }
    }
  },
  "admin":   { "forbidden_tables": [], "forbidden_columns": [], "row_level": {} },
  "default": { "forbidden_tables": ["users"], "forbidden_columns": {}, "row_level": {} },
  "enabled": true
}
  • USER_ID: placeholder replaced by the logged-in user's ID.
  • The SDK merges default + the role's rules (recursive merge, like array_replace_recursive).
  • If no role can be resolved → null → SSE error Access rules not resolved.
  • enabled: turns the whole permission system on or off.

Tables excluded from the scan (excluded_tables / excludedTables)

When the schema is generated (blunx:init / npx blunx-ai init), Blunx scans all the tables in your database and makes them queryable by the assistant. List here the technical, sensitive or useless tables that must never appear in the schema. No environment variable: the list is declared by file/option.

LaravelNode.js
Whereconfig/blunx.php → 'excluded_tables'config/blunx.config.ts → excludedTables
DefaultLaravel technical tables (migrations, jobs, sessions...) + blunx_*Internal blunx_* tables only
When readOn blunx:init / blunx:editOn init / edit (runtime loaded)
config/blunx.php - Laravel PHP
'excluded_tables' => [
    // Already excluded by default: migrations, failed_jobs, cache, jobs, sessions,
    // password_reset_tokens, personal_access_tokens... + the internal blunx_* tables
    'admin_logs', 'notifications', 'settings', 'audit_trail',   // ▶ your tables
],
config/blunx.config.ts - Node.js TypeScript
export default {
  // ...
  // Internal blunx_* tables are already ignored by default.
  // ▶ Add your technical / sensitive tables that must not be scanned:
  excludedTables: ['migrations', 'sessions', 'log_errors', 'admin_logs', 'notifications'],
};
Exclusion ≠ RBAC rule

An excluded table is never scanned: it does not exist for Blunx (neither in the schema nor offered to the assistant). A forbidden table (forbidden_tables, RBAC) stays in the schema but its access is blocked according to the user's role. Change the list before init; afterwards, re-run blunx:edit / npx blunx-ai edit to remove an already-scanned table.

A complete .env ready to copy

.env Env
# --- Blunx server ---
BLUNX_SERVER_URL=https://blunxai.com
# Timeout (s) for each request to the Hub - 600 (10 min) by default; 0 = none
BLUNX_REQUEST_TIMEOUT=600
BLUNX_API_KEY=blunx_app_live_xxxxxxxxxxxx

# --- LLM provider (OpenAI in this example) ---
BLUNX_LLM_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
BLUNX_LLM_DRIVER=openai
BLUNX_LLM_ENDPOINT=https://api.openai.com/v1
BLUNX_LLM_MODEL=gpt-4o
BLUNX_LLM_SUPPORTS_JSON_FORMAT=true

# --- Database ---
BLUNX_DB_CONNECTION=mysql
# BLUNX_DB_URL=postgres://user:pass@host:5432/db?sslmode=require   # alternative: connection URL (DSN)
# BLUNX_READONLY_DB_USERNAME=blunx_readonly   # optional (see connection modes)
# BLUNX_READONLY_DB_PASSWORD=secret
# BLUNX_PGSQL_SCHEMA=public                    # PostgreSQL only

# --- Display & language ---
BLUNX_CURRENCY_CODE=EUR
BLUNX_CURRENCY_SYMBOL=€
BLUNX_CURRENCY_LOCALE=en_US
BLUNX_LOCALE=en

The 3 database connection modes

Blunx runs SQL against your database. Three complementary notions - do not confuse them:

ModeVariableWhat it isWhen to use it
1 · App default connection BLUNX_DB_CONNECTION The connection your application already uses (Laravel default connection, or the knex instance/config passed to Blunx.init()). Most common case: Blunx reuses your app's database as is.
2 · Connection URL (DSN) BLUNX_DB_URL A complete URL (host, port, database, user, password, SSL) that replaces the default connection. Cloud databases (Neon, Railway, Heroku...) or targeting another database without touching the app config.
3 · Read-only role BLUNX_READONLY_DB_* A credentials override applied on top of mode 1 or 2 to force read-only execution. Safety lock: the connection carries the main role but Blunx must never write.
Priority rule

BLUNX_DB_URL takes precedence over the default connection (mode 2 > mode 1). The read-only role (mode 3) is then applied on top, with SSL preserved. If the URL already contains a read-only role, mode 3 is unnecessary: leave it empty.

API contract

Two families of endpoints: the Web SDK only knows the local endpoints /api/blunx/* served by your backend SDK; the latter proxies to the Blunx server /api/v1/*.

Local endpoints (/api/blunx/*)

Session + auth, no CSRF, throttle 60:1. Responses wrapped in { "data": ... }, except /user (direct object).

MethodPathPurpose
GET/api/blunx/userLogged-in user (locale field)
GET · POST/api/blunx/conversationsList / create a conversation
DELETE/api/blunx/conversations/{uuid}Delete a conversation
GET/api/blunx/conversations/{uuid}/messagesMessages of a conversation
POST/api/blunx/messagesSave a message (creates the conversation if conversation_uuid is null)
DELETE/api/blunx/messages/{uuid}Delete a message
POST/api/blunx/feedbackUser feedback
GET · POST/api/blunx/dashboardsList / create a dashboard
GET · DELETE/api/blunx/dashboards/{uuid}Show / delete a dashboard
POST/api/blunx/widgetsCreate a widget
PATCH · DELETE/api/blunx/widgets/{uuid}Update / delete a widget
POST/api/blunx/widgets/{uuid}/executeExecute a widget
GET/api/blunx/insights/{dashUuid}Insights of a dashboard
GET/api/blunx/insights/{dashUuid}/settingsInsight settings
POST/api/blunx/insights/{uuid}/mark-readMark an insight as read
DELETE/api/blunx/insights/{uuid}Delete an insight
GET/api/blunx/insights/{uuid}/downloadDownload the PDF report
POST/api/blunx/insight-settingsCreate / update an insight setting
DELETE/api/blunx/insights/jobs/{widgetUuid}Cancel a scheduled insight job
POST/api/blunx/execute-sqlRun a query stored on a message
POST/api/blunx/streamSSE chat: multi-agent pipeline with streaming

Blunx server endpoints (/api/v1/*)

Middlewares: ValidateBlunxKey (X-Blunx-Key) then RequireLLMConfig (X-Blunx-LLM-Key + the llm block in the body).

EndpointMethodResponse format
/api/v1/chat/queriesPOSTSSE
/api/v1/chat/validatePOSTSSE
/api/v1/chat/synthesizePOSTSSE
/api/v1/insights/analyzePOSTJSON
/api/v1/insights/reportPOSTJSON
/api/v1/schema/enrichPOSTJSON
/api/v1/reference/generatePOSTJSON

Common headers

Headers HTTP
X-Blunx-Key: blunx_app_live_xxxxxxx      # application key (required)
X-Blunx-LLM-Key: sk-xxxxxxxxxxxxxxxx     # LLM provider key (required)
Content-Type: application/json
Accept: text/event-stream | application/json

Base payload

Every call to the Blunx server carries the database connection and the LLM configuration. Keys travel in the headers, never in the body (no leakage into logs).

JSON JSON
{
  "db_connection": "mysql",
  "locale": "fr",
  "currency": { "code": "XOF", "symbol": "FCFA", "locale": "fr_FR" },
  "llm": {
    "driver": "openai",
    "endpoint": "https://api.openai.com/v1",
    "model": "gpt-4o",
    "supports_json_format": true
  }
}

Chat - POST /api/v1/chat/queries

Request:

JSON JSON
{
  "question": "What is the revenue per region?",
  "conversation_history": [ { "role": "user", "content": "..." } ],
  "access_rules": { "forbidden_tables": [], "forbidden_columns": [], "row_level": [] },
  "schema": "... full schema as a JSON string (or schema_id) ...",
  "schema_id": "optional-uuid",
  "db_connection": "mysql",
  "locale": "en",
  "currency": { "code": "USD", "symbol": "$", "locale": "en_US" },
  "llm": { "driver": "openai", "endpoint": "...", "model": "...", "supports_json_format": true }
}

SSE response - result event:

JSON JSON
{
  "needs_data": true,
  "needs_clarification": false,
  "normalized_question": "...",
  "intent": "...",
  "queries": [ { "sql": "SELECT ...", "ui_hint": "chart", "chart_type": "bar" } ],
  "warnings": [],
  "filtered_schema_id": "uuid"
}

Terminal cases: needs_clarification (+ clarification_question), needs_data=false, access_denied (+ message, forbidden_info).

Chat - validate & synthesize

Validate: {normalized_question, intent, execution_results, filtered_schema_id, access_rules, locale, db_connection, llm} → {queries[], warnings[]} (corrected SQL).

Synthesize: {normalized_question, execution_results, conversation_history, needs_data, locale, currency, llm} → structured answer:

JSON JSON
{
  "title": "Revenue per region",
  "layout": [
    { "type": "paragraph", "content": "... Bootstrap 5 HTML ..." },
    { "type": "chart", "chart_type": "bar", "title": "...", "label_column": "...",
      "data_columns": ["..."], "comment": "...", "sql_query": "..." },
    { "type": "table", "title": "...", "comment": "...", "sql_query": "..." }
  ],
  "Resume": "... markdown summary ..."
}

If needs_data=false: { "Resume": "...", "is_conversational": true }.

Insights, schema & references

  • Analyze : {widget_title, sql_query, current_meta, previous_meta?, metric_weights?, locale, currency, llm} → {type, severity, message, meta}.
  • Report : {insight_result, current_meta, previous_meta?, last_insight?, widget_title, dashboard_name, report_frequency, locale, currency, llm} → {html}.
  • Schema enrich: {columns, locale, llm} → {columns} with descriptions.
  • Reference generate: {widget_sql, widget_title?, numeric_columns[], access_rules?, db_connection, locale, llm} → {queries}.

SSE stream - events

SSE stream SSE
event: step
data: {"step":1,"status":"running","duration":1}

event: result
data: { ...result... }

event: done
data: {"result":{ "title":"...", "Resume":"...", "layout":[...] }, "message_uuid":"..."}

event: error
data: {"message":"..."}
EventMeaning
stepProgress (steps 1 to 6: Analysis, Planning, Collection, Execution, Validation, Synthesis)
resultIntermediate result (e.g. generated queries)
doneEnd: final answer + message_uuid
errorError ({message, code, fields, plan, limit, used})
SSE headers to respect

Content-Type: text/event-stream; charset=UTF-8, Cache-Control: no-cache, no-store, X-Accel-Buffering: no. Do not buffer the response.

Security & access

Blunx applies defense in depth across several layers to guarantee that your data is never modified or exposed beyond what you authorize.

1. Strictly read-only (SqlGuard)

Every SQL query is validated locally before execution. The rules are multiple:

  • The query must start with SELECT or WITH (CTE).
  • Forbidden dangerous keywords: INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, GRANT, REVOKE, EXEC, VACUUM, LOCK, COPY, etc.
  • Forbidden dangerous functions: pg_sleep, pg_read_file, lo_import, dblink, pg_execute_server_program, set_config, etc.
  • SELECT ... INTO is forbidden.
  • A sanitize() step strips string literals and comments before analysis (avoiding false positives on values).
Rejection example Text
invalid statement type: INSERT (only SELECT allowed)
forbidden keyword found: DROP
forbidden function call found: pg_sleep
SELECT ... INTO is forbidden ...

Even if a query were tampered with between generation and execution, the execution agent (DataExecutorAgent) re-validates it and skips any non-SELECT query.

2. Dedicated read-only database connection

This is the main security lock. If BLUNX_READONLY_DB_USERNAME is set, the package creates a blunx_readonly connection (same driver/host/database, dedicated credentials) used for all executions. In the database, grant that role only GRANT SELECT, USAGE:

SQL (once, on the DBA side) SQL
-- MySQL
CREATE USER 'blunx_readonly'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON your_database.* TO 'blunx_readonly'@'%';

-- PostgreSQL
CREATE ROLE blunx_readonly LOGIN PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE your_database TO blunx_readonly;
GRANT USAGE ON SCHEMA public TO blunx_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO blunx_readonly;
Recommended in production

Always create a read-only database role. Without it, the package uses Laravel's main connection - the SqlGuard safeguards remain active, but a dedicated role adds protection at the database level itself.

3. Per-role access rules (RBAC)

The config/blunx_access.php file defines, per role, the forbidden tables and columns and the row-level filtering. See the Configuration section for the full syntax. The role comes from getUserRoleName() on your User model.

  • Forbidden tables: never scanned nor exposed to the LLM.
  • Forbidden columns: masked ('*' pattern = all tables).
  • Row-level filtering: the USER_ID placeholder is replaced by the logged-in user's ID (e.g. a sales rep only sees orders.user_id = themselves).

4. Authentication

ContextMechanism
Web SDK → Laravel packageLaravel session cookie (auth middleware of the blunx group)
Web SDK → Node.js packageAuthenticated user of the host app (req.user / ctx.state.user) - the router returns 401 if absent
Backend SDK → Blunx serverHeaders X-Blunx-Key (app key) + X-Blunx-LLM-Key (LLM key)

In cross-origin setups, the backend must accept credentials: include (Access-Control-Allow-Credentials + an explicit origin, not *) and the Accept, Content-Type, X-Requested-With headers (CORS preflight).

5. Other protections

  • Rate limiting: 60 requests/min/IP on the local /api/blunx/* group (Laravel throttle; in Node, add your own rate limiter, e.g. express-rate-limit), except the Stripe webhook (not concerned here).
  • SQL validation: double check (client + server), a non-SELECT query is never executed.
  • PostgreSQL schema: the scan only touches the configured schema (BLUNX_PGSQL_SCHEMA), never the others.
  • CSRF: the blunx group mirrors web without CSRF (session auth is enough for this AJAX API).

LLM providers

Choose your provider through the driver and the endpoint. The openai adapter is "OpenAI-compatible": it works with almost every provider, proprietary or open-source.

The openai driver is not reserved for OpenAI

The openai adapter makes no assumption about the host: it POSTs exactly to the URL provided in llm.endpoint, in the standard chat/completions format. It is an adapter "OpenAI-compatible", not "OpenAI only". And almost every current provider - proprietary AND open-source - exposes that compatible API.

Provider table

ProviderDriverEndpoint (to put in BLUNX_LLM_ENDPOINT)
OpenAIopenaihttps://api.openai.com/v1
Azure OpenAIopenaihttps://{resource}.openai.azure.com/openai/v1
Ollama (local)openaihttp://localhost:11434/v1
Groqopenaihttps://api.groq.com/openai/v1
DeepSeekopenaihttps://api.deepseek.com/v1
Mistralopenaihttps://api.mistral.ai/v1
Together, Fireworks, OpenRouter...openaiProvider's OpenAI-compatible URL
vLLM, LM Studio, llama.cppopenaiLocal / self-hosted URL (e.g. http://localhost:8000/v1)
Anthropic Claudeanthropichttps://api.anthropic.com/v1/messages (native format)
Google Geminigeminihttps://generativelanguage.googleapis.com (native generateContent format)

Configuring a provider

Example: DeepSeek (OpenAI-compatible API)

.env Env
BLUNX_LLM_DRIVER=openai
BLUNX_LLM_ENDPOINT=https://api.deepseek.com/v1
BLUNX_LLM_MODEL=deepseek-chat
BLUNX_LLM_API_KEY=sk-xxxxxxxxxxxx
BLUNX_LLM_SUPPORTS_JSON_FORMAT=true

Example: local Ollama (zero cloud dependency)

.env Env
BLUNX_LLM_DRIVER=openai
BLUNX_LLM_ENDPOINT=http://localhost:11434/v1
BLUNX_LLM_MODEL=llama3.1
BLUNX_LLM_API_KEY=ollama          # unused value, may stay empty
BLUNX_LLM_SUPPORTS_JSON_FORMAT=true

Example: Groq

.env Env
BLUNX_LLM_DRIVER=openai
BLUNX_LLM_ENDPOINT=https://api.groq.com/openai/v1
BLUNX_LLM_MODEL=llama-3.3-70b-versatile
BLUNX_LLM_API_KEY=gsk_xxxxxxxxxxxx
BLUNX_LLM_SUPPORTS_JSON_FORMAT=true

Driver details

DriverFormatNotes
openai POST {endpoint}/chat/completions OpenAI-compatible + all the providers listed above. endpoint may or may not include the /chat/completions suffix.
anthropic POST /v1/messages (native format) Anthropic Claude. Native endpoint, Anthropic headers.
gemini generateContent (native format) Google Gemini. Native Google endpoint.

supports_json_format

Indicates whether the model supports the response_format: json_object mode. Modern models (GPT-4o, DeepSeek, Groq...) support it (true); some older local models do not. If the model does not support it, set false so that the pipeline asks for JSON in natural language rather than in structured mode.

Robustness recommendations

For production use, prefer a model that reliably outputs JSON (supports_json_format=true) and a low-latency endpoint. The pipeline handles automatic retries on empty LLM responses or transient failures.

Errors & troubleshooting

Organized by layer: Blunx server (HTTP), backend (Laravel / Node), SSE and Web SDK. Look for the HTTP code first, then the layer concerned.

1 · Blunx server HTTP codes

Shared by both backends - returned by the /api/v1/* hub:

CodeMeaningTrigger / action
400Bad RequestInvalid JSON. Check the request body.
401UnauthorizedMissing/invalid key (missing_key / invalid_key). Check BLUNX_API_KEY.
402Payment RequiredSubscription inactive/expired (billing_inactive). Check your billing.
403ForbiddenApplication suspended/inactive (frozen). Contact support.
404Not FoundSCHEMA_NOT_FOUND → the package clears its cache and retries without schema_id.
413Payload Too LargeBody too large. Reduce the schema being sent.
422Unprocessable EntityValidation / missing LLM config (missing_llm_config, invalid_llm_driver).
429Too Many RequestsQuota exceeded (plan, limit, used). Wait or upgrade your plan.
500Internal Server ErrorLLM error on the Blunx server side. Retry and check your LLM key.

2 · Backend

Both SDKs throw an exception with a human-readable message (the server message takes precedence when present):

Mapping in BlunxApiClient (PHP):

Client mapping PHP
401 => "Invalid or expired API key"
402 => "Billing inactive. Please check your subscription."
403 => "Application suspended. Contact support."
429 => "Quota exceeded. Please wait before retrying."
422 => "LLM configuration missing. Check your settings."
default => "AI service error. Please try again later."

BlunxError (JS) carries a message and a statusCode - same mapping as Laravel:

BlunxError JavaScript
try {
  await blunx.api.someCall();
} catch (e) {
  if (e instanceof BlunxError) {
    console.error(e.statusCode, e.message); // e.g. 401 "Invalid or expired API key"
  }
}

3 · SSE errors

  • error event: {message, code, fields, plan, limit, used}.
  • HTTP ≥ 400: extraction of message / error / detail.
  • Network error: cURL error: ... (PHP) / fetch failed (Node).
  • SCHEMA_NOT_FOUND: automatic retry without schema_id.

4 · Web SDK errors

CaseBehaviour
Container not foundinit() rejects: [Blunx] Container not found: "#blunx"
view:'dashboard' without UUID[Blunx] Dashboard view without UUID.
Non-OK HTTP response (API)Error HTTP {status}: {statusText} (or the body's message)
Stream failureServer error (HTTP {status}) / Unknown error / Connection failed
Failing CDN dependencyNon-blocking console.warn

5 · Common integration errors

SymptomLikely causeSolution
401 on the first callMissing/invalid BLUNX_API_KEYCheck the .env and run config:clear (Laravel).
422 / missing_llm_configIncomplete LLM setup (endpoint, model, key)Fill in the 4 LLM variables (see Configuration).
401 on /api/blunx/* (Node)req.user not populatedYour auth middleware must run before blunxExpress.
Empty lists in the responseUnauthenticated request (Node) or missing user_idAuthenticate the user before the Blunx router.
SCHEMA_NOT_FOUNDSchema not sent to the hubRe-run blunx:init / blunx-ai init.

Versions & updates

Web SDK versioning policy and backend SDK updates.

Versioning policy

The Web SDK follows semantic versioning (MAJOR.MINOR.PATCH):

  • Patch: non-breaking bugfix (no action required).
  • Minor: new backwards-compatible feature.
  • Major: breaking change (public API, REST contract, navigation).

Web SDK updates

Integration modeUpdate behaviour
Auto-update loader (blunx-loader.min.js) Resolves ./blunx.min.js relative to itself: served from jsDelivr "latest", it is always on the same version as the loader. No action required on your side.
Direct bundle (blunx.min.js) You pin the version. To update, change the URL or add ?v=.
npm (@blunx/web-sdk) Pin the version (^0.1.0) and run npm update.
Loader cache-busting

On a CDN, the loader appends ?v={version} (the CDN purges on release). Locally / self-hosted, it appends a timestamp: you always serve the latest build without clearing the browser cache.

Web SDK changelog

VersionDateHighlights
0.1.0 August 7, 2026 Reworked into a standalone project; public API init/destroy/navigate/version; hash-based routing; esbuild build with 3 targets; auto-update loader; auto-injected dependencies; bundled CSS + logo; option-based config; bundled translations; CSS isolation .blunx-root; complete mock PHP demo.

Laravel package

  • Update with composer update blunx/ai.
  • Check the version: composer show blunx/ai.
  • After a major update, re-check the published config/blunx.php (the published file is not overwritten automatically).
Published config vs package config

After a major update, compare config/blunx.php with the package's published version (vendor/blunx/ai/config/blunx.php) and carry over the new keys where needed.

Support

A problem this documentation does not solve? The Blunx team replies within 24 hours.

ChannelDetails
Contact form/contact - the "Technical integration" subject is recommended for implementation questions
Product feedbackThe application's feedback module (thumbs + comment) reports straight to the team
Bug reportDescribe: SDK version (info.version), Laravel/PHP version, reproduction steps, HTTP/SSE error code
Preparing an effective support request

Always include: the exact error message (HTTP code + body), your llm block (without the key), the output of composer show blunx/ai and, if relevant, your queue logs.

Licence: MIT -© 2026 BlunxAI. Laravel package: blunx/ai · Web SDK : @blunx/web-sdk.