Aelio
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 logic

1. Install dependencies

npm install @aelio/sdk @aelio/chat pg ws

2. 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-production

3. 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

  1. SDK registers locally without runtime — tools configure at startup; listen() only when secret is set
  2. Lifecycle sync from HTTP and SDK — both routes and tool handlers call setCustomerState()
  3. WebSocket proxy — widget uses same-origin URL, proxied to Aelio runtime
  4. Config endpoint — backend exposes chat config without secrets
  5. 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

On this page