Aelio
PlatformsNext.js

Next.js

Full-stack Aelio integration with Next.js App Router — SDK, API routes, and chat widget.

Integrate Aelio into a Next.js App Router application with the SDK running server-side and the chat widget in a client component.

Install

npm install @aelio/sdk @aelio/chat

Project structure

my-next-app/
├── app/
│   ├── layout.tsx              # Root layout + chat widget
│   ├── api/
│   │   └── chat-token/
│   │       └── route.ts        # Issue session tokens
│   └── page.tsx
├── components/
│   └── aelio-chat.tsx          # Client component for widget
├── lib/
│   └── aelio.ts                # SDK initialization (server-only)
├── instrumentation.ts          # Boot SDK on server start
└── .env.local

Step 1: SDK initialization (server-only)

// lib/aelio.ts
import { aelio } from '@aelio/sdk';

let initialized = false;

export async function initAelio() {
  if (initialized) return;

  aelio.persona('You are a helpful assistant for My SaaS.');

  aelio.expose('getOrderStatus', async ({ orderId }, ctx) => {
    return await db.orders.findOne({ id: orderId, userId: ctx.customerId });
  }, {
    description: 'Get the status of a customer order',
    params: { orderId: 'string' },
    safety: 'read',
  });

  aelio.expose('cancelOrder', async ({ orderId }, ctx) => {
    return await db.orders.cancel(orderId, ctx.customerId);
  }, {
    description: 'Cancel a pending order',
    params: { orderId: 'string' },
    safety: 'write',
  });

  if (process.env.AELIO_SDK_SECRET) {
    await aelio.listen({
      secret: process.env.AELIO_SDK_SECRET,
      url: process.env.AELIO_SERVER_URL ?? 'ws://127.0.0.1:3010',
    });
    console.log('Aelio SDK connected');
  }

  initialized = true;
}

Step 2: Boot SDK on server start

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { initAelio } = await import('./lib/aelio');
    await initAelio();
  }
}

Step 3: Chat token API route

// app/api/chat-token/route.ts
import { NextResponse } from 'next/server';
import { getServerSession } from '@/lib/auth';

export async function POST() {
  const session = await getServerSession();
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const res = await fetch(`${process.env.AELIO_HTTP_URL}/auth/magic-link`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.AELIO_SDK_SECRET}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: session.user.email,
      externalId: session.user.id,
    }),
  });

  const data = await res.json();

  const verifyRes = await fetch(
    `${process.env.AELIO_HTTP_URL}/auth/verify?token=${extractToken(data.url)}`
  );
  const { sessionToken } = await verifyRes.json();

  return NextResponse.json({ sessionToken });
}

Step 4: Chat widget client component

// components/aelio-chat.tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import { mountAelioChat, unmountAelioChat } from '@aelio/chat';

interface Props {
  userId: string;
  email: string;
}

export function AelioChat({ userId, email }: Props) {
  const handleRef = useRef<ReturnType<typeof mountAelioChat>>(null);
  const [token, setToken] = useState<string>();

  useEffect(() => {
    fetch('/api/chat-token', { method: 'POST' })
      .then(r => r.json())
      .then(data => setToken(data.sessionToken))
      .catch(() => {/* anonymous mode fallback */});
  }, []);

  useEffect(() => {
    if (!userId) return;

    handleRef.current = mountAelioChat({
      serverUrl: process.env.NEXT_PUBLIC_AELIO_SERVER_URL!,
      customerId: userId,
      authToken: token,
      email,
      title: 'Chat with us',
    });

    return () => {
      if (handleRef.current) unmountAelioChat(handleRef.current);
    };
  }, [userId, email, token]);

  return null;
}

Step 5: Add to layout

// app/layout.tsx
import { AelioChat } from '@/components/aelio-chat';
import { getServerSession } from '@/lib/auth';

export default async function RootLayout({ children }) {
  const session = await getServerSession();

  return (
    <html lang="en">
      <body>
        {children}
        {session && (
          <AelioChat userId={session.user.id} email={session.user.email} />
        )}
      </body>
    </html>
  );
}

Step 6: Environment variables

# .env.local — server-only (never exposed to browser)
AELIO_SDK_SECRET=change-me-in-production
AELIO_SERVER_URL=ws://127.0.0.1:3010
AELIO_HTTP_URL=http://127.0.0.1:3010

# .env.local — client-safe
NEXT_PUBLIC_AELIO_SERVER_URL=http://127.0.0.1:3010

Step 7: Server origin allowlist

Add your Next.js dev origin to Aelio server config:

channels:
  web:
    enabled: true
    allowed_origins:
      - "http://localhost:3000"
      - "https://your-app.vercel.app"

Alternative: script tag approach

For simpler setups without npm bundling:

// components/aelio-chat-script.tsx
'use client';

import Script from 'next/script';

export function AelioChatScript({ userId, email }: { userId: string; email: string }) {
  return (
    <Script
      src={`${process.env.NEXT_PUBLIC_AELIO_SERVER_URL}/widget.js`}
      data-server-url={process.env.NEXT_PUBLIC_AELIO_SERVER_URL}
      data-customer-id={userId}
      data-email={email}
      strategy="lazyOnload"
    />
  );
}

Lifecycle sync from API routes

// app/api/orders/route.ts
import { aelio } from '@/lib/aelio';

export async function POST(req: Request) {
  const order = await createOrder(await req.json());
  await aelio.setCustomerState(order.userId, 'post_purchase', 'Order placed');
  return Response.json(order);
}

Deploy to Vercel

  1. Set env vars in Vercel dashboard (AELIO_SDK_SECRET, AELIO_SERVER_URL, NEXT_PUBLIC_AELIO_SERVER_URL)
  2. Deploy Aelio server separately (Docker on Railway/Render/Fly)
  3. Add production domain to allowed_origins
  4. Use wss:// for production WebSocket URL

Key considerations

TopicRecommendation
SDK locationServer-only (lib/aelio.ts), never import in client components
WidgetClient component with 'use client' directive
Auth tokensIssue via API route, never expose AELIO_SDK_SECRET
SDK bootUse instrumentation.ts for server-side initialization
Cold startsSDK reconnects automatically on serverless wake

On this page