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/chatProject 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.localStep 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:3010Step 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
- Set env vars in Vercel dashboard (
AELIO_SDK_SECRET,AELIO_SERVER_URL,NEXT_PUBLIC_AELIO_SERVER_URL) - Deploy Aelio server separately (Docker on Railway/Render/Fly)
- Add production domain to
allowed_origins - Use
wss://for production WebSocket URL
Key considerations
| Topic | Recommendation |
|---|---|
| SDK location | Server-only (lib/aelio.ts), never import in client components |
| Widget | Client component with 'use client' directive |
| Auth tokens | Issue via API route, never expose AELIO_SDK_SECRET |
| SDK boot | Use instrumentation.ts for server-side initialization |
| Cold starts | SDK reconnects automatically on serverless wake |