AI Chatbot Backend
This guide builds a production-ready AI chatbot backend that stores conversation history, authenticates users, streams LLM responses, and runs custom tools — using four Reactor capabilities working together.
What you’ll build:
- User authentication with JWT
- Conversation and message storage with RLS
- Streaming chat endpoint via a WASM function
- LLM routing through the Gateway capability
Prerequisites: Running Reactor server, Node.js 18+, basic TypeScript knowledge.
1. Schema and RLS
Section titled “1. Schema and RLS”Create migrations for conversations and messages:
-- migrations/001_chatbot.sql
CREATE TABLE conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, title TEXT NOT NULL DEFAULT 'New conversation', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now());
CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')), content TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now());
CREATE INDEX messages_conversation_idx ON messages(conversation_id, created_at);
-- RLS: users see only their own conversationsALTER TABLE conversations ENABLE ROW LEVEL SECURITY;ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY conversations_owner ON conversations FOR ALL USING (user_id = (current_setting('request.jwt.claims', true)::json->>'sub')::uuid) WITH CHECK (user_id = (current_setting('request.jwt.claims', true)::json->>'sub')::uuid);
CREATE POLICY messages_via_conversation ON messages FOR ALL USING ( conversation_id IN ( SELECT id FROM conversations WHERE user_id = (current_setting('request.jwt.claims', true)::json->>'sub')::uuid ) );Deploy migrations:
reactor deploy2. Configure the Gateway
Section titled “2. Configure the Gateway”Add AI provider credentials to Reactor.toml:
[ai]openrouter_api_key = "vault:ai/openrouter_key"default_alias = "fast"
# Or use Bedrock:# aws_access_key_id = "vault:ai/aws_key"# aws_secret_access_key = "vault:ai/aws_secret"# aws_bedrock_region = "us-east-1"Store the API key in vault or env:
export REACTOR_AI__OPENROUTER_API_KEY="sk-or-v1-..."3. Chat function
Section titled “3. Chat function”Create a streaming chat handler that loads history, calls the gateway, and saves the response:
import { ReactorClient } from "@reactor/client";
interface ChatRequest { conversation_id: string; message: string;}
export default async function handler(req: Request): Promise<Response> { const reactor = new ReactorClient({ baseUrl: Deno.env.get("REACTOR_URL")!, token: req.headers.get("Authorization")?.replace("Bearer ", "")!, });
const { conversation_id, message } = await req.json() as ChatRequest;
// Save user message await reactor.data.from("messages").insert({ conversation_id, role: "user", content: message, });
// Load conversation history const { data: history } = await reactor.data .from("messages") .select("role, content") .eq("conversation_id", conversation_id) .order("created_at", { ascending: true }) .limit(50);
// Stream from gateway const stream = await reactor.ai.chat.completions.create({ model: "fast", messages: [ { role: "system", content: "You are a helpful assistant." }, ...(history ?? []), ], stream: true, });
let fullResponse = ""; const encoder = new TextEncoder();
const readable = new ReadableStream({ async start(controller) { for await (const chunk of stream) { const text = chunk.choices[0]?.delta?.content ?? ""; fullResponse += text; controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`)); }
// Save assistant message await reactor.data.from("messages").insert({ conversation_id, role: "assistant", content: fullResponse, });
controller.enqueue(encoder.encode("data: [DONE]\n\n")); controller.close(); }, });
return new Response(readable, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", }, });}use reactor_function::prelude::*;use serde::{Deserialize, Serialize};
#[derive(Deserialize)]struct ChatRequest { conversation_id: String, message: String,}
#[derive(Serialize)]struct Message { role: String, content: String,}
#[reactor_function]async fn chat(ctx: Context, req: HttpRequest) -> Result<HttpResponse, Error> { let body: ChatRequest = req.json()?; let client = ctx.reactor_client()?;
// Save user message client.data().from("messages").insert(json!({ "conversation_id": body.conversation_id, "role": "user", "content": body.message, })).await?;
// Load history let history: Vec<Message> = client.data() .from("messages") .select("role, content") .eq("conversation_id", &body.conversation_id) .order("created_at", true) .limit(50) .fetch() .await?;
// Call gateway (non-streaming for WASM simplicity) let response = client.ai().chat(json!({ "model": "fast", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, // ...history ], })).await?;
let content = response["choices"][0]["message"]["content"] .as_str().unwrap_or("");
client.data().from("messages").insert(json!({ "conversation_id": body.conversation_id, "role": "assistant", "content": content, })).await?;
Ok(HttpResponse::json(json!({ "content": content })))}Deploy the function:
reactor functions deploy chat4. Frontend client
Section titled “4. Frontend client”import { createClient } from "@reactor/client";
const reactor = createClient({ url: "https://api.myapp.com",});
// Sign inconst { session } = await reactor.auth.signInWithPassword({ email: "user@example.com", password: "secure-password",});
// Create conversationconst { data: conversation } = await reactor.data .from("conversations") .insert({ title: "Help with React" }) .select() .single();
// Stream chatasync function* streamChat(message: string) { const response = await fetch( `${reactor.url}/fn/v1/chat`, { method: "POST", headers: { Authorization: `Bearer ${session.access_token}`, "Content-Type": "application/json", }, body: JSON.stringify({ conversation_id: conversation.id, message, }), } );
const reader = response.body!.getReader(); const decoder = new TextDecoder();
while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); for (const line of text.split("\n")) { if (line.startsWith("data: ") && line !== "data: [DONE]") { yield JSON.parse(line.slice(6)).text; } } }}
// Usage in Reactfor await (const chunk of streamChat("Explain useEffect")) { setResponse((prev) => prev + chunk);}5. Tool calling (optional)
Section titled “5. Tool calling (optional)”Extend the chat function to support tool use — querying your data API mid-conversation:
const tools = [ { type: "function", function: { name: "search_products", description: "Search the product catalog", parameters: { type: "object", properties: { query: { type: "string" }, }, required: ["query"], }, }, },];
// In the gateway call:const response = await reactor.ai.chat.completions.create({ model: "power", messages: history, tools,});
// If the model requests a tool call:if (response.choices[0].message.tool_calls) { const call = response.choices[0].message.tool_calls[0]; const results = await reactor.data .from("products") .select("*") .ilike("name", `%${JSON.parse(call.function.arguments).query}%`);
// Feed results back to the model for final response}6. Rate limiting
Section titled “6. Rate limiting”Protect the chat endpoint with a job that tracks usage:
// functions/chat/index.ts — add at the top of handlerconst MAX_MESSAGES_PER_HOUR = 60;const hourAgo = new Date(Date.now() - 3600_000).toISOString();
const { count } = await reactor.data .from("messages") .select("*", { count: "exact", head: true }) .eq("role", "user") .gte("created_at", hourAgo);
if (count && count >= MAX_MESSAGES_PER_HOUR) { return new Response(JSON.stringify({ error: "Rate limit exceeded" }), { status: 429, });}Summary
Section titled “Summary”| Capability | Role in chatbot |
|---|---|
| Auth | User sign-up, JWT for RLS |
| Data | Conversations, messages, RLS isolation |
| Functions | Streaming chat handler |
| Gateway | LLM routing (OpenRouter, Bedrock, etc.) |
Related
Section titled “Related”- OAuth setup — social login for your chatbot
- Multi-tenant app — org-scoped conversations
- Security — RLS patterns