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
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).
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
| Requirement | Details |
|---|---|
| Application | Laravel 9-12 (PHP 8.1+) or Node.js 18+ (Express, Fastify, Koa, NestJS, plain HTTP) |
| Database | MySQL, MariaDB, PostgreSQL, SQLite or SQL Server (your app's database) |
| Blunx account | An application created in the dashboard → your application key |
| LLM provider | An 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 retrieve | Variable | Example |
|---|---|---|
| Application key | BLUNX_API_KEY | blunx_app_live_xxxx |
| LLM key | BLUNX_LLM_API_KEY | sk-xxxxxxxxxxxx |
| Endpoint LLM | BLUNX_LLM_ENDPOINT | https://api.openai.com/v1 |
| Model | BLUNX_LLM_MODEL | gpt-4o |
Ollama runs locally, without an account: endpoint http://localhost:11434/v1, model llama3.1. See LLM providers.
Install the package
composer require blunx/ai
php artisan vendor:publish --tag=blunx-config
Provider in auto-discovery, no migration to publish.
Fill in your .env
Add your 4 keys from step 1:
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.
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):
// 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.
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.
Generate and lock the schema
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.
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.
public function getUserRoleName(): ?string
{
return $this->role->name ?? null;
}
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.
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.
<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.
Install the package
npm install blunx-ai
The .env is loaded automatically (dotenv). No migration to publish.
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):
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://....
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:
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));
Typical implementation to adapt (here a users table):
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.
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:
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
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.
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():
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'],
};
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.
Generate and lock the schema
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.
Integrate the Web SDK
<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.
It is almost always the BLUNX_API_KEY / BLUNX_LLM_API_KEY keys or an incomplete LLM configuration. See Errors & troubleshooting.
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:
| Database | db_connection value | Notes |
|---|---|---|
| MySQL | mysql | Default |
| MariaDB | mariadb | Handled as MySQL on the SDK side |
| PostgreSQL | pgsql | Scan limited to the BLUNX_PGSQL_SCHEMA schema (default public) |
| SQLite | sqlite | Local file |
| SQL Server | sqlsrv | Driver sqlsrv / sqlserver |
Backend (server SDK)
| Technology | Status | Notes |
|---|---|---|
| Laravel (package blunx/ai) | ● Available | Laravel 9 to 12 (tested on 12), PHP 8.1+ |
| Node.js (package blunx-ai) | ● Available | Node.js 18+, npm, same /api/blunx/* contract |
| Django (Python) | ● Coming soon | Same /api/blunx/* contract |
| Go | ● Coming soon | Same /api/blunx/* contract |
| Spring Boot (Java) | ● Coming soon | Same /api/blunx/* contract |
Frontend (Web SDK @blunx/web-sdk)
| Technology | Method | Constraint |
|---|---|---|
| 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 |
| Angular | npm install + init()/destroy() | Call init() inside ngAfterViewInit |
| Svelte / SvelteKit | npm install + init()/destroy() | Client-side loading only |
| Isolated widget (iframe) | Host page embedding the SDK | Maximum 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.
| Driver | Providers |
|---|---|
| openai | OpenAI, Azure OpenAI, Ollama (local), Groq, DeepSeek, Mistral, Together, Fireworks, OpenRouter, vLLM, LM Studio, llama.cpp... |
| anthropic | Anthropic Claude (native format /v1/messages) |
| gemini | Google Gemini (native generateContent format) |
Browsers & environments
| Item | Support |
|---|---|
| Browsers | Chrome, Edge, Firefox, Safari (ES2019+, no IE) |
| Bundlers | Vite, webpack, esbuild, Rollup (ESM module) |
| SSR | Not supported natively - must run client side only |
| CDN | jsDelivr (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
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 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:
public function getUserRoleName(): ?string
{
return $this->role->name ?? null;
}
Artisan commands
| Command | Purpose |
|---|---|
| blunx:init | Scans the database and generates storage/app/blunx_schema.json (enriched through the Blunx server). |
| blunx:edit | Manual editing of descriptions or re-indexing after the database evolves. |
| blunx:setup | Locks the schema and creates the 8 Blunx tables (idempotent). |
| blunx:feedback | Reviews unresolved user feedback. |
| blunx:run-insights | Immediately 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):
| Table | Purpose |
|---|---|
| blunx_conversations | Chat conversations |
| blunx_messages | Messages (user|assistant) |
| blunx_feedbacks | User feedback |
| blunx_dashboards | Dashboards |
| blunx_dashboard_widgets | Widgets (chart|table) + reference_queries |
| blunx_insights | Insights + report_pdf |
| blunx_widget_insight_settings | Per-widget insight settings |
| blunx_cache | Cache (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).
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).
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
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):
| Method | How | When to use it |
|---|---|---|
| BLUNX_DB_URL in the .env (recommended) | A connection URL, read by the CLI and the runtime | Most cases - a single config for everything. |
| database: { client, connection } | Connection config passed to Blunx.init() | When the CLI is not used (runtime only). |
| knex: knexInstance | Your existing knex instance | Your app already uses knex - we reuse it. |
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():
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
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)
| Command | Purpose |
|---|---|
| npx blunx-ai init | Scans the database and generates storage/app/blunx_schema.json (enriched through the Blunx server). |
| npx blunx-ai edit | Manual editing of descriptions or re-indexing after a database change. |
| npx blunx-ai setup | Locks the schema and creates the 8 Blunx tables (idempotent). |
| npx blunx-ai feedback | Reviews unresolved user feedback. |
| npx blunx-ai run-insights | Runs 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.
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.
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).
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.
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'));
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'));
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'));
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 {}
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'));
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
| File | Format | Usage |
|---|---|---|
| dist/blunx-loader.min.js | IIFE | Plain <script>, auto-update (always the latest version) |
| dist/blunx.min.js | IIFE | Direct <script> → window.Blunx |
| dist/blunx.esm.js | ESM | npm install @blunx/web-sdk → import { init } |
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):
<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):
<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.
npm install @blunx/web-sdk
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).
'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>
);
}
npm install @blunx/web-sdk
<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>
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
}
}
<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
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"
| Option | Type | Default | Description |
|---|---|---|---|
| container | string | HTMLElement | '#blunx' | Selector / element the application mounts into |
| apiBaseUrl | string | '' | API base (empty = same origin, recommended) |
| view | 'auto' | 'cockpit' | 'dashboard' | 'auto' | Initial view ('auto' follows the URL hash) |
| dashboardUuid | string | null | null | Dashboard UUID when view: 'dashboard' |
| onReady | function | null | Called after the first mount: (info) => {} |
| onError | function | null | Called 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
The selector must exist when init() runs, and the container must have a height (height:100vh or 70vh), otherwise the rendering may collapse.
The module accesses window/document at evaluation time: import it on the client side only ('use client' or ssr: false).
The SDK reacts to every hashchange. If your app already uses the hash for its own routing, a conflict may occur.
In cross-origin mode (apiBaseUrl on another domain), the /api/blunx/stream fetch does not send credentials. Prefer a same-origin setup.
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
| Method | Laravel | Node.js | When 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. |
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).
| Variable | Default | Purpose | Alternative (Laravel → Node) |
|---|---|---|---|
| BLUNX_SERVER_URL | https://blunxai.com | Blunx server URL (/api/v1/*) | server_url → serverUrl |
| BLUNX_REQUEST_TIMEOUT | 600 | Timeout (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-Key | api_key → apiKey |
| BLUNX_LLM_API_KEY | — | LLM key → X-Blunx-LLM-Key | llm_api_key → llmApiKey |
| BLUNX_LLM_DRIVER | openai | openai (compatible: Ollama, Groq, DeepSeek...) | gemini | anthropic | llm.driver → llm.driver |
| BLUNX_LLM_ENDPOINT | — | Your provider's endpoint | llm.endpoint → llm.endpoint |
| BLUNX_LLM_MODEL | — | Model used by the AI | llm.model → llm.model |
| BLUNX_LLM_SUPPORTS_JSON_FORMAT | true | Does the model support response_format: json_object? (false on older local models) | llm.supports_json_format → llm.supportsJsonFormat |
| BLUNX_DB_CONNECTION | mysql | SQL 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 possible | db_url → databaseUrl |
| BLUNX_READONLY_DB_USERNAME / PASSWORD | — | Read-only database role (optional - see connection modes) | readonly_db → readonlyDb |
| BLUNX_PGSQL_SCHEMA | public | PostgreSQL schema scanned | pgsql_schema → pgsqlSchema |
| BLUNX_CURRENCY_CODE / SYMBOL / LOCALE | EUR / € / en_US | Amount display format | currency.* → currency.* |
| BLUNX_LOCALE | en | Language of prompts, messages and views | locale → locale |
| BLUNX_SCHEMA_PATH | storage/app/blunx_schema.json | Path 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).
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).
| Option | Type | Default | Env | Description |
|---|---|---|---|---|
| enabled | boolean | true | — | Enables / disables the SDK |
| serverUrl | string | https://blunxai.com | BLUNX_SERVER_URL | Blunx Hub URL (/api/v1/*) |
| requestTimeout | number | 600 | BLUNX_REQUEST_TIMEOUT | Timeout (s) for each request to the Hub (SSE + JSON) - 0 = no timeout |
| apiKey | string | null | null | BLUNX_API_KEY | Application key → header X-Blunx-Key |
| llmApiKey | string | null | null | BLUNX_LLM_API_KEY | LLM key → header X-Blunx-LLM-Key |
| dbConnection | string | null | null | BLUNX_DB_CONNECTION | SQL dialect sent to the Hub (mysql, pgsql...) - not the local connection |
| databaseUrl | string | null | null | BLUNX_DB_URL | Database connection URL (DSN) - takes precedence (mysql://, postgres://, sqlite://...) |
| readonlyDb | { username, password } | { null, null } | BLUNX_READONLY_DB_* | Read-only database role (optional) |
| pgsqlSchema | string | public | BLUNX_PGSQL_SCHEMA | PostgreSQL schema scanned by init/edit |
| llm | { driver, endpoint, model, supportsJsonFormat } | openai / null / null / true | BLUNX_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_US | BLUNX_CURRENCY_* | Amount display format |
| excludedTables | string[] | blunx_* (8) | — | Tables ignored during the schema scan (see the dedicated section below) |
| locale | string | en | BLUNX_LOCALE | Language of messages and views |
| tables | { conversations, messages, feedbacks, dashboards, widgets, insights, insightSettings, cache } | blunx_* | — | Custom Blunx table names |
| schemaPath | string | storage/app/blunx_schema.json | BLUNX_SCHEMA_PATH | Path of the schema file |
| access | AccessConfig | default | — | RBAC rules (see below) |
| knex | instance | — | — | Host's knex instance (main connection) |
| readonlyKnex | instance | — | — | 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) |
| MailOptions | — | — | Email SMTP: host, port, secure, user, pass, from, transport (nodemailer) | |
| enableScheduler | boolean | true | — | Enables the hourly insight scheduler |
- 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:
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:
| Level | Type | Purpose |
|---|---|---|
| forbidden_tables | string[] | 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):
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.
| Laravel | Node.js | |
|---|---|---|
| Where | config/blunx.php → 'excluded_tables' | config/blunx.config.ts → excludedTables |
| Default | Laravel technical tables (migrations, jobs, sessions...) + blunx_* | Internal blunx_* tables only |
| When read | On blunx:init / blunx:edit | On init / edit (runtime loaded) |
'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
],
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'],
};
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
# --- 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:
| Mode | Variable | What it is | When 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. |
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).
| Method | Path | Purpose |
|---|---|---|
| GET | /api/blunx/user | Logged-in user (locale field) |
| GET · POST | /api/blunx/conversations | List / create a conversation |
| DELETE | /api/blunx/conversations/{uuid} | Delete a conversation |
| GET | /api/blunx/conversations/{uuid}/messages | Messages of a conversation |
| POST | /api/blunx/messages | Save a message (creates the conversation if conversation_uuid is null) |
| DELETE | /api/blunx/messages/{uuid} | Delete a message |
| POST | /api/blunx/feedback | User feedback |
| GET · POST | /api/blunx/dashboards | List / create a dashboard |
| GET · DELETE | /api/blunx/dashboards/{uuid} | Show / delete a dashboard |
| POST | /api/blunx/widgets | Create a widget |
| PATCH · DELETE | /api/blunx/widgets/{uuid} | Update / delete a widget |
| POST | /api/blunx/widgets/{uuid}/execute | Execute a widget |
| GET | /api/blunx/insights/{dashUuid} | Insights of a dashboard |
| GET | /api/blunx/insights/{dashUuid}/settings | Insight settings |
| POST | /api/blunx/insights/{uuid}/mark-read | Mark an insight as read |
| DELETE | /api/blunx/insights/{uuid} | Delete an insight |
| GET | /api/blunx/insights/{uuid}/download | Download the PDF report |
| POST | /api/blunx/insight-settings | Create / update an insight setting |
| DELETE | /api/blunx/insights/jobs/{widgetUuid} | Cancel a scheduled insight job |
| POST | /api/blunx/execute-sql | Run a query stored on a message |
| POST | /api/blunx/stream | SSE 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).
| Endpoint | Method | Response format |
|---|---|---|
| /api/v1/chat/queries | POST | SSE |
| /api/v1/chat/validate | POST | SSE |
| /api/v1/chat/synthesize | POST | SSE |
| /api/v1/insights/analyze | POST | JSON |
| /api/v1/insights/report | POST | JSON |
| /api/v1/schema/enrich | POST | JSON |
| /api/v1/reference/generate | POST | JSON |
Common headers
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).
{
"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:
{
"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:
{
"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:
{
"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
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":"..."}
| Event | Meaning |
|---|---|
| step | Progress (steps 1 to 6: Analysis, Planning, Collection, Execution, Validation, Synthesis) |
| result | Intermediate result (e.g. generated queries) |
| done | End: final answer + message_uuid |
| error | Error ({message, code, fields, plan, limit, used}) |
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).
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:
-- 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;
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
| Context | Mechanism |
|---|---|
| Web SDK → Laravel package | Laravel session cookie (auth middleware of the blunx group) |
| Web SDK → Node.js package | Authenticated user of the host app (req.user / ctx.state.user) - the router returns 401 if absent |
| Backend SDK → Blunx server | Headers 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 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
| Provider | Driver | Endpoint (to put in BLUNX_LLM_ENDPOINT) |
|---|---|---|
| OpenAI | openai | https://api.openai.com/v1 |
| Azure OpenAI | openai | https://{resource}.openai.azure.com/openai/v1 |
| Ollama (local) | openai | http://localhost:11434/v1 |
| Groq | openai | https://api.groq.com/openai/v1 |
| DeepSeek | openai | https://api.deepseek.com/v1 |
| Mistral | openai | https://api.mistral.ai/v1 |
| Together, Fireworks, OpenRouter... | openai | Provider's OpenAI-compatible URL |
| vLLM, LM Studio, llama.cpp | openai | Local / self-hosted URL (e.g. http://localhost:8000/v1) |
| Anthropic Claude | anthropic | https://api.anthropic.com/v1/messages (native format) |
| Google Gemini | gemini | https://generativelanguage.googleapis.com (native generateContent format) |
Configuring a provider
Example: DeepSeek (OpenAI-compatible API)
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)
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
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
| Driver | Format | Notes |
|---|---|---|
| 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.
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:
| Code | Meaning | Trigger / action |
|---|---|---|
| 400 | Bad Request | Invalid JSON. Check the request body. |
| 401 | Unauthorized | Missing/invalid key (missing_key / invalid_key). Check BLUNX_API_KEY. |
| 402 | Payment Required | Subscription inactive/expired (billing_inactive). Check your billing. |
| 403 | Forbidden | Application suspended/inactive (frozen). Contact support. |
| 404 | Not Found | SCHEMA_NOT_FOUND → the package clears its cache and retries without schema_id. |
| 413 | Payload Too Large | Body too large. Reduce the schema being sent. |
| 422 | Unprocessable Entity | Validation / missing LLM config (missing_llm_config, invalid_llm_driver). |
| 429 | Too Many Requests | Quota exceeded (plan, limit, used). Wait or upgrade your plan. |
| 500 | Internal Server Error | LLM 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):
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:
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
| Case | Behaviour |
|---|---|
| Container not found | init() 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 failure | Server error (HTTP {status}) / Unknown error / Connection failed |
| Failing CDN dependency | Non-blocking console.warn |
5 · Common integration errors
| Symptom | Likely cause | Solution |
|---|---|---|
| 401 on the first call | Missing/invalid BLUNX_API_KEY | Check the .env and run config:clear (Laravel). |
| 422 / missing_llm_config | Incomplete LLM setup (endpoint, model, key) | Fill in the 4 LLM variables (see Configuration). |
| 401 on /api/blunx/* (Node) | req.user not populated | Your auth middleware must run before blunxExpress. |
| Empty lists in the response | Unauthenticated request (Node) or missing user_id | Authenticate the user before the Blunx router. |
| SCHEMA_NOT_FOUND | Schema not sent to the hub | Re-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 mode | Update 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. |
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
| Version | Date | Highlights |
|---|---|---|
| 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).
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.
| Channel | Details |
|---|---|
| Contact form | /contact - the "Technical integration" subject is recommended for implementation questions |
| Product feedback | The application's feedback module (thumbs + comment) reports straight to the team |
| Bug report | Describe: SDK version (info.version), Laravel/PHP version, reproduction steps, HTTP/SSE error code |
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.