Rekhu Assistant
Rekhu Assistant: Node.js
Install @rekhu/assistant-node, create the assistant with your first action, and add its route to Express, Next.js, Nuxt, Hono, Bun or Deno.
What this package does
@rekhu/assistant-node is the back end half of the assistant. It adds a chat route to your Node.js server, holds your secret key, asks your own sign-in who the user is, and runs your actions as that user. The front end half is the web widget or the Flutter package, which talk only to your route.
It runs on Node.js 20, 22 or 24, has no dependencies, and ships its TypeScript types.
Install in five steps
- Get a secret key. In your Rekhu account, open Projects → New project, choose Node.js app (monolith or multi-tenant) and name it. On its Connection tab, add a key for
local. The key starts withagk_and is shown once. - Put the key in the server's environment as
REKHU_SECRET_KEY, never in front-end code. - Install the packages: the server package, and the widget for the front end.
- Create the assistant in a module of its own, with your sign-in and a first action.
- Add the route and the widget. Mount the assistant on a path, and point the widget at its
…/chatroute.
npm install @rekhu/assistant-node @rekhu/assistant-widget
Create the assistant in a module of its own, so tests and the command line can load it without starting your server:
// assistant.js (server side)
import { createAssistant, defineAction, Forbidden } from '@rekhu/assistant-node';
import { orders } from './orders.js';
export const assistant = createAssistant({
appName: 'My shop',
user: (req) => req.user ?? null, // your own sign-in decides who this is
tenant: (user) => ({ id: user.companyId, name: user.companyName }),
help: './help', // markdown files, one topic each
actions: [
defineAction({
name: 'reorder',
description: 'Reorder a past order: the same product as a new order; the quantity can be changed',
input: { orderNumber: 'string', quantity: 'integer?' },
confirm: true,
run: async ({ orderNumber, quantity }, { user }) => {
const order = await orders.find(orderNumber);
if (!order || order.ownerId !== user.id) throw new Forbidden();
const created = await orders.reorder(order, quantity);
return { message: `Order ${created.number} placed.`, page: `/orders/${created.number}` };
},
}),
],
});
Then mount it, here with Express, and add the widget in the front end:
// server.js
import express from 'express';
import { assistant } from './assistant.js';
const app = express();
app.use('/api/rekhu', assistant.express()); // serves /api/rekhu/chat
// in the front end, for a signed-in user
import { mount } from '@rekhu/assistant-widget';
mount({ endpoint: '/api/rekhu/chat' });
Warning: check permissions inside
run. The package givesrunthe signed-in user; whether that user may touch this record is your code's job, as in your own route handlers. ThrowForbiddenwhen they may not.
Frameworks
The package serves its routes under the path you mount it on: …/chat for signed-in users and …/visitor/chat for the public chat, each answering GET, POST and DELETE, and GET …/files/:id for the files actions hand over.
Next.js
Route handlers, on the Node.js runtime:
// app/api/rekhu/chat/route.ts
import { assistant } from '@/lib/assistant';
export const GET = assistant.fetch;
export const POST = assistant.fetch;
export const DELETE = assistant.fetch;
In lib/assistant.ts, user: async () => (await auth())?.user ?? null reads your session. When actions hand over files, add app/api/rekhu/files/[id]/route.ts with export const GET = assistant.fetch;.
Nuxt
A server route that hands over the user from your session:
// server/api/rekhu/chat.ts
import { assistant } from '~/server/assistant';
export default defineEventHandler(async (event) => {
const { user } = await getUserSession(event);
return assistant.fetch(toWebRequest(event), { user: user ?? null });
});
assistant.fetch(request, { user }) uses the user you pass instead of calling user().
Hono, Bun and Deno
assistant.fetch is a standard (Request) => Promise<Response> handler:
// Hono
app.all('/api/rekhu/*', (c) => assistant.fetch(c.req.raw, { user: c.get('user') ?? null }));
// Bun
Bun.serve({ fetch: (request) => assistant.fetch(request) });
Remix and SvelteKit hand it their request the same way.
Who may use the chat
Your sign-in decides who the user is; access decides whether that user may use the assistant at all. Anyone it refuses gets 403 with "You cannot use the assistant. Ask an administrator for access." in their language.
createAssistant({
// …
access: async (user) => user.role === 'admin' || (await plans.includes(user.companyId, 'assistant')),
});
Configuration
| Option | Default | Purpose |
|---|---|---|
user(request) |
required | the signed-in user of a request, or null |
secretKey |
REKHU_SECRET_KEY |
the secret key |
userId(user) |
user.id |
the user's id as Rekhu meters it |
tenant(user) |
the app itself | the customer (company) of the user |
actions, pages, help |
see Actions and the scan | |
instructions |
your own rules, in plain words | |
greeting, quick |
the first message and the questions to click, a text or one per language | |
model |
REKHU_ASSISTANT_MODEL |
the model that answers, by its name in your Rekhu account |
maxTokens, maxSteps |
16000, 8 |
how long one answer may be, and how many action rounds a turn may take |
guests |
off | the public chat for visitors |
store |
in memory | a shared store when you run more than one process |
allowedOrigins |
[] |
front-end origins that may call the routes from another domain |
With more than one process or server, give a shared store with four methods (get, set, delete, increment), for example on Redis. The memory store forgets everything when the process restarts.
Check your setup
assistant.check(user) tests the key and the credit, and lists what that user is offered:
console.log(await assistant.check(someUser));
// { connected: true, credit: '$42.10', problem: null, actions: ['reorder'], pages: [...], help: [...], reports: {...} }
From the command line, point --app at the module that exports the assistant:
npx rekhu-assistant try reorder --app ./src/assistant.js --user=12 --input='{"orderNumber":"1042"}'
npx rekhu-assistant check --app ./src/assistant.js --user=12 --locale=ar
try runs one action as a user, exactly as the chat does, and shows what the assistant reads of the result. check finds unclear actions and results too long to read, and exits with code 1 on problems, so it can run in CI. Give findUser: (id) => db.users.findById(id) so --user=12 finds the user.
Reselling to your customers
onAnswer gets what each answer used: the model, the tokens of all its model calls, the number of calls, the actions, the tenant, user and conversation, also when an answer broke off. model picks the model per plan:
const assistant = createAssistant({
// …
model: (user, request, scope) => (user?.plan === 'pro' ? 'Rekhu Pro' : null),
onAnswer: (u) => db.assistantTurns.insert({ tenant: u.tenant.id, user: u.user?.id, model: u.model, input: u.inputTokens, output: u.outputTokens, calls: u.calls }),
});
A model is named as in your Rekhu account; null uses the default.
Next
- Actions and the scan: inputs, confirmations, files and good descriptions.
- The web widget: every option of the chat in the browser.
- Conversation history: the list of earlier conversations, and a shared store.