PlatformsNode.js
Vanilla Node.js
Integrate @aelio/sdk with vanilla node:http — no Express or framework required.
This guide follows the Aelio Shopping Testbed pattern — a single server.mjs file with vanilla node:http, Postgres, SDK integration, and WebSocket proxy for the chat widget.
Project structure
my-app/
├── server.mjs # HTTP server + SDK + Postgres
├── package.json
├── docker-compose.yml # Postgres (optional)
├── .env
└── public/
├── index.html # Storefront
├── chat.html # Chat page
└── chat-page.js # Widget mount logic1. Install dependencies
npm install @aelio/sdk @aelio/chat pg ws2. Configure environment
# .env
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
PORT=4173
AELIO_SERVER_URL=ws://127.0.0.1:3010
AELIO_SECRET=change-me-in-production3. SDK integration in server.mjs
import { createServer } from 'node:http';
import { aelio } from '@aelio/sdk';
// Persona and product context
aelio.persona('You are a helpful assistant for My Store.');
aelio.describe('We sell electronics and home goods.');
// Lifecycle states
aelio.state('anonymous', {
description: 'Unknown visitor. Help them browse.',
allowedTools: ['search_products', 'list_categories'],
blockedTools: ['get_cart', 'checkout_cart'],
});
aelio.state('browsing', {
description: 'Known customer browsing products.',
allowedTools: ['search_products', 'get_cart', 'add_to_cart'],
});
// Register tools
function exposeTool(name, handler, schema) {
aelio.expose(name, async (args, ctx) => {
console.log(`[aelio] ${name}`, { customerId: ctx.customerId });
return handler(args, ctx);
}, schema);
}
exposeTool('search_products', async (args) => {
const products = await db.query(
'SELECT * FROM products WHERE name ILIKE $1 LIMIT 10',
[`%${args.query || ''}%`]
);
return { products: products.rows };
}, {
description: 'Search product catalog',
params: { query: 'string?', category: 'string?' },
safety: 'read',
});
exposeTool('add_to_cart', async (args, ctx) => {
// ... cart logic ...
await aelio.setCustomerState(ctx.customerId, 'cart_building');
return { added: true, productId: args.productId };
}, {
description: 'Add item to cart',
params: { productId: 'string', quantity: 'integer?' },
safety: 'write',
});4. Connect SDK and start server
async function start() {
if (process.env.AELIO_SECRET) {
await aelio.listen({
secret: process.env.AELIO_SECRET,
url: process.env.AELIO_SERVER_URL,
});
console.log('Aelio SDK connected');
}
const server = createServer(handleRequest);
server.listen(process.env.PORT ?? 4173, () => {
console.log(`Server running on :${process.env.PORT ?? 4173}`);
});
}
start();5. Serve widget bundle
import { readFile } from 'node:fs/promises';
const WIDGET_FILE = process.env.AELIO_WIDGET_FILE
?? '../Aelio-Convox/server/public/widget.js';
async function handleRequest(req, res) {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === '/vendor/aelio-chat/widget.js') {
const content = await readFile(WIDGET_FILE);
res.writeHead(200, { 'content-type': 'text/javascript' });
res.end(content);
return;
}
// Chat config (no secrets)
if (url.pathname === '/api/aelio/chat-config') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
serverUrl: `http://${req.headers.host}`,
defaultCustomerId: 'demo-user',
defaultEmail: 'user@example.com',
}));
return;
}
// ... other routes ...
}6. WebSocket proxy for widget
Proxy widget WebSocket so the browser talks to your server, which relays to Aelio:
import { WebSocketServer, WebSocket } from 'ws';
function attachWidgetProxy(server) {
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (request, socket, head) => {
const { pathname } = new URL(request.url, `http://${request.headers.host}`);
if (pathname !== '/widget/ws') return;
wss.handleUpgrade(request, socket, head, (client) => {
const upstream = new WebSocket(
process.env.AELIO_SERVER_URL.replace('ws://', 'ws://').replace(/\/$/, '') + '/widget/ws'
);
upstream.on('message', (data) => client.readyState === 1 && client.send(data));
client.on('message', (data) => upstream.readyState === 1 && upstream.send(data));
upstream.on('close', () => client.close());
client.on('close', () => upstream.close());
});
});
}7. Frontend chat page
<!-- public/chat.html -->
<script src="/vendor/aelio-chat/widget.js" data-auto-mount="false"></script>
<script type="module" src="/chat-page.js"></script>// public/chat-page.js
async function mountChat() {
const config = await fetch('/api/aelio/chat-config').then(r => r.json());
const script = document.createElement('script');
script.src = '/vendor/aelio-chat/widget.js';
script.dataset.customerId = config.defaultCustomerId;
script.dataset.email = config.defaultEmail;
script.dataset.serverUrl = config.serverUrl;
document.body.appendChild(script);
}
mountChat();8. Test inbound messages
curl -X POST http://localhost:4173/api/aelio/ingest \
-H 'content-type: application/json' \
-d '{"channel":"web","from":"user@example.com","text":"Search for headphones"}'Key patterns from Shopping Testbed
- SDK registers locally without runtime — tools configure at startup;
listen()only when secret is set - Lifecycle sync from HTTP and SDK — both routes and tool handlers call
setCustomerState() - WebSocket proxy — widget uses same-origin URL, proxied to Aelio runtime
- Config endpoint — backend exposes chat config without secrets
- Event audit trail — log all SDK tool calls and lifecycle events
Run
docker-compose up -d db # if using Postgres
AELIO_SECRET=change-me-in-production npm start
# Store: http://127.0.0.1:4173
# Chat: http://127.0.0.1:4173/chat