Developers
Documentation
Everything you need to embed an AI database agent into your app — from first install to production multi-tenant SaaS.
Overview
SyncAgent lets you embed an AI assistant into any app that can query, analyze, and manage your database using natural language. Your users type questions like "Show me all orders over $500 from last month" and the agent translates them into real database queries — no SQL or code required.
Choose Your Mode
Not sure which mode fits your use case? Use this decision tree to pick the right one.
Decision Flowchart
Q: Do your users need to query a database?
→ Yes → Database Agent Mode
Q: Do you need AI-powered customer support?
→ Yes → Customer Agent Mode
Q: Do you need both?
→ Yes → Dual Mode
💡You can always switch modes later by updating your SDK configuration. No data migration required.
Quick Start
Step 1 — Create a project
Sign up → Dashboard → New Project → choose your database type → copy your API key.
💡Your API key starts with sa_. Keep it secret — treat it like a password.
Step 2 — Install
# React apps
npm install @syncagent/react @syncagent/js
# Next.js apps (server-safe helpers)
npm install @syncagent/nextjs @syncagent/js @syncagent/react
# Vue 3 apps
npm install @syncagent/vue @syncagent/js
# Angular apps
npm install @syncagent/angular @syncagent/js
# Node.js / any JS runtime
npm install @syncagent/jsStep 3 — Add the widget (React)
import { SyncAgentChat } from "@syncagent/react";
export default function App() {
return (
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY,
connectionString: process.env.DATABASE_URL,
}}
/>
);
}🔒Never expose your database connection string in client-side code. In Next.js, use a server action or API route to pass it securely. See the Security section below.
Step 4 — That's it
A floating chat button appears in the bottom-right corner. Your users can now query your database in plain English. The agent auto-discovers your schema on the first request and caches it for 5 minutes. If the schema changes mid-conversation, the agent automatically detects stale schema errors and refreshes it.
Supported Databases
Pass your connection string in the connectionString config. The agent auto-detects the format.
mongodb+srv://user:pass@cluster.mongodb.net/mydbInclude the database name at the end of the URL.
postgresql://user:pass@host:5432/mydbWorks with Neon, Railway, Render, Supabase direct connection, etc.
mysql://user:pass@host:3306/mydbWorks with PlanetScale, Railway, AWS RDS, etc.
/absolute/path/to/database.sqliteUse an absolute path. Relative paths are resolved from the server working directory.
Server=host,1433;Database=mydb;User Id=user;Password=pass;Encrypt=true;Works with Azure SQL, AWS RDS SQL Server, on-premise.
https://xxx.supabase.co|your-anon-keyUse the project URL and anon/service key separated by |. Uses the REST API, not direct PostgreSQL.
React SDK — @syncagent/react
Drop-in floating widget
The simplest integration. Renders a floating chat button that opens a full-featured chat panel.
import { SyncAgentChat } from "@syncagent/react";
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
}}
// Appearance
mode="floating" // "floating" | "inline"
position="bottom-right" // "bottom-right" | "bottom-left"
accentColor="#10b981" // any hex color
title="AI Assistant"
subtitle="Ask about your data"
welcomeMessage="Hi! Ask me anything about your data."
placeholder="Ask anything..."
// Behavior
defaultOpen={false}
persistKey="my-app" // saves chat history to localStorage
suggestions={[ // quick-start chips
"Show all records",
"Count total entries",
"Show recent activity",
]}
/>Inline mode — embed in your layout
// Embed inside a fixed-height container
<div style={{ height: 600 }}>
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
mode="inline"
/>
</div>Custom UI with useSyncAgent hook
Build your own chat UI. Wrap your component in SyncAgentProvider and use the hook.
import { SyncAgentProvider, useSyncAgent } from "@syncagent/react";
// 1. Wrap your app (or just the chat component)
export default function App() {
return (
<SyncAgentProvider
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
}}
>
<MyChat />
</SyncAgentProvider>
);
}
// 2. Use the hook inside
function MyChat() {
const {
messages, // Message[] — full conversation history
isLoading, // boolean — true while streaming
error, // Error | null
status, // { step, label } | null — live status
lastData, // ToolData | null — last DB query result
sendMessage, // (content: string) => void
stop, // () => void — abort current stream
reset, // () => void — clear all messages
} = useSyncAgent();
return (
<div>
{status && <div>⏳ {status.label}</div>}
{messages.map((msg, i) => (
<div key={i} className={msg.role === "user" ? "user" : "ai"}>
{msg.content}
</div>
))}
<button onClick={() => sendMessage("Show all users")}>Ask</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Clear</button>
</div>
);
}All SyncAgentChat props
| Prop | Type | Default | Description |
|---|---|---|---|
| config | SyncAgentConfig | required* | API key, connection string, tools, filter, operations, toolsOnly |
| mode | "floating" | "inline" | "floating" | Floating FAB or embedded panel |
| position | "bottom-right" | "bottom-left" | "bottom-right" | FAB position (floating only) |
| defaultOpen | boolean | false | Start with panel open |
| title | string | "SyncAgent" | Header title |
| subtitle | string | "AI Database Assistant" | Header subtitle |
| accentColor | string | "#10b981" | Brand color for header, FAB, send button |
| suggestions | string[] | 3 defaults | Quick-start chips shown on empty state |
| persistKey | string | — | localStorage key for conversation persistence |
| context | Record<string,any> | — | Extra context injected into every message |
| filter | Record<string,any> | — | Mandatory query filter for multi-tenancy |
| onReaction | (idx, reaction, content) => void | — | Called when user reacts 👍/👎 |
| onData | (data: ToolData) => void | — | Called when a DB tool returns data |
Customer Chat — Pre-built Widget
Drop in a complete customer support chat widget with zero custom UI. Handles guest identification, AI messaging, escalation to human agents, real-time Pusher messages, satisfaction rating, and WCAG AA theming.
import { SyncAgentCustomerChat } from "@syncagent/react";
// Minimal setup — floating widget in bottom-right corner
export default function App() {
return (
<SyncAgentCustomerChat
apiKey="sa_your_api_key"
connectionString={process.env.NEXT_PUBLIC_DATABASE_URL!}
/>
);
}Full-featured example with theming, events, and guest form customization:
import { SyncAgentCustomerChat } from "@syncagent/react";
import type { GuestIdentity } from "@syncagent/react";
export function SupportWidget() {
return (
<SyncAgentCustomerChat
apiKey="sa_your_api_key"
connectionString={process.env.NEXT_PUBLIC_DATABASE_URL!}
// Display
mode="floating"
position="bottom-right"
defaultOpen={false}
title="Support"
subtitle="We're here to help"
placeholder="Type your message..."
welcomeMessage="Hi! How can we help you today?"
// Theming
accentColor="#6366f1"
darkMode={false}
// Guest form (shown when no externalUserId)
guestForm={{
title: "Welcome!",
subtitle: "Tell us about yourself to get started",
submitButtonText: "Start Chat",
namePlaceholder: "Your name",
emailPlaceholder: "you@company.com",
phonePlaceholder: "+1 (555) 000-0000",
}}
// Real-time human agent messages
pusherKey={process.env.NEXT_PUBLIC_PUSHER_KEY}
pusherCluster="us2"
// Events
onEscalated={() => console.log("Escalated to human agent")}
onResolved={(id) => console.log("Resolved:", id)}
onGuestIdentified={(identity: GuestIdentity) => {
console.log("Guest:", identity.name, identity.guestId);
}}
// Skip guest form for authenticated users
externalUserId={currentUser?.id}
// Extra metadata sent with every message
metadata={{ page: window.location.pathname }}
/>
);
}Inline mode — embed inside your layout instead of a floating button:
<div style={{ height: 600, width: 400 }}>
<SyncAgentCustomerChat
apiKey="sa_your_api_key"
connectionString={process.env.NEXT_PUBLIC_DATABASE_URL!}
mode="inline"
darkMode
accentColor="#8b5cf6"
/>
</div>💡For custom UI, use the useCustomerChat hook instead — see the Customer Agent Mode section for the hook API.
JS SDK — @syncagent/js
Use in Node.js, Express, Fastify, or any server-side JS runtime. Also works in the browser.
Basic usage
import { SyncAgentClient } from "@syncagent/js";
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_U
});
// Non-streaming — await full response
const result = await agent.chat([
{ role: "user", content: "How many users signed up this month?" }
]);
console.log(result.text);
// Streaming — token by token
await agent.chat(
[{ role: "user", content: "Show me the top 10 customers by revenue" }],
{
onToken: (token) => process.stdout.write(token),
onComplete: (text) => console.log("\nDone"),
onError: (err) => console.error(err),
}
);
// Schema discovery
const schema = await agent.getSchema();
console.log(schema); // CollectionSchema[]Multi-turn conversations
const history = [];
// Turn 1
history.push({ role: "user", content: "Show top 5 customers" });
const r1 = await agent.chat(history);
history.push({ role: "assistant", content: r1.text });
// Turn 2 — agent remembers context
history.push({ role: "user", content: "Now show their total orders" });
const r2 = await agent.chat(history);Live status events
await agent.chat(messages, {
onStatus: (step, label) => {
// step: "connecting" | "schema" | "thinking" | "querying" | "done"
console.log(`[${step}] ${label}`);
// [connecting] Connecting to database...
// [querying] Querying users...
// [thinking] Thinking...
},
onToken: (token) => process.stdout.write(token),
});onData — react to query results
await agent.chat(messages, {
onData: (data) => {
// Called whenever a DB tool returns data
console.log(data.collection); // "orders"
console.log(data.data); // array of result rows
console.log(data.count); // number of results
// Update your own UI
if (data.collection === "orders") {
setOrders(data.data);
}
},
});Abort / cancel
const controller = new AbortController();
// Start streaming
agent.chat(messages, {
signal: controller.signal,
onToken: (t) => process.stdout.write(t),
});
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);Express.js integration
import express from "express";
import { SyncAgentClient } from "@syncagent/js";
const app = express();
app.use(express.json());
const agent = new SyncAgentClient({
apiKey: process.env.SYNCAGENT_KEY,
connectionString: process.env.DATABASE_URL,
});
// Streaming endpoint
app.post("/chat", async (req, res) => {
res.setHeader("Content-Type", "text/plain");
res.setHeader("Transfer-Encoding", "chunked");
await agent.chat(req.body.messages, {
onToken: (token) => res.write(token),
onComplete: () => res.end(),
onError: (err) => { res.status(500).end(err.message); },
});
});
app.listen(3000);Customer Chat — customerChat() API
The JS SDK provides the core customerChat() method for sending messages through the customer support pipeline. Use this when building custom server-side integrations or non-framework UIs.
import { SyncAgentClient } from "@syncagent/js";
// 1. Initialize client in customer mode
const client = new SyncAgentClient({
apiKey: "sa_your_api_key",
externalUserId: "customer_123", // from your auth session
});
// 2. Send a message through the support pipeline
const result = await client.customerChat("How do I reset my password?");
console.log(result.response); // AI-generated answer
console.log(result.conversationId); // "conv_abc123"
console.log(result.escalated); // false — handled by AI
console.log(result.resolved); // false — conversation ongoing
// 3. Continue the conversation
const followUp = await client.customerChat("That didn't work", {
conversationId: result.conversationId,
onEscalated: () => {
console.log("Escalated to human agent");
},
onResolved: (conversationId) => {
console.log("Resolved:", conversationId);
},
});
// 4. Rate after resolution
if (followUp.resolved) {
await client.rateConversation(followUp.conversationId, 5);
}Guest mode — for anonymous visitors without an authenticated user ID:
import {
SyncAgentClient,
validateGuestForm,
generateGuestIdentifier,
} from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_api_key",
customerMode: true, // no externalUserId — guest mode
});
// Validate guest form data
const validation = validateGuestForm({
name: "Jane Doe",
email: "jane@example.com",
});
if (validation.valid) {
// Set guest identity — persists to localStorage
client.setGuestIdentity({
name: "Jane Doe",
email: "jane@example.com",
phone: null,
guestId: generateGuestIdentifier("jane@example.com"),
});
// Now customerChat() works
const result = await client.customerChat("I need help with my order");
console.log(result.response);
}Theme engine — generate WCAG-compliant color palettes for custom UIs:
import { computeTheme, type ThemeColors } from "@syncagent/js";
// Generate a full color palette from an accent color
const theme: ThemeColors = computeTheme("#6366f1", false); // light mode
const darkTheme: ThemeColors = computeTheme("#6366f1", true); // dark mode
// Apply to your custom UI
console.log(theme.background); // "#ffffff"
console.log(theme.text); // "#111827" (4.5:1 contrast guaranteed)
console.log(theme.accent); // "#6366f1" (3:1 contrast guaranteed)
console.log(theme.userBubble); // accent-derived bubble color
console.log(theme.userBubbleText); // white or black (4.5:1 guaranteed)💡The theme engine is used internally by the pre-built <SyncAgentCustomerChat> widget. Use it directly when building custom chat UIs that need accessible color tokens.
Next.js SDK — @syncagent/nextjs
The Next.js SDK provides server-side helpers that let you safely pass your database connection string from Server Components — it never reaches the browser bundle. All config options from @syncagent/js are supported.
💡Install: npm install @syncagent/nextjs @syncagent/js @syncagent/react
Server Component — the recommended pattern
// app/dashboard/page.tsx — Server Component (no "use client")
import { createServerConfig } from "@syncagent/nextjs/server";
import { SyncAgentChat } from "@syncagent/nextjs";
import { getServerSession } from "next-auth";
export default async function DashboardPage() {
const session = await getServerSession();
const config = createServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
connectionString: process.env.DATABASE_URL!,
filter: { organizationId: session.user.orgId },
operations: session.user.isAdmin
? ["read", "create", "update", "delete"]
: ["read"],
// All new options work here too:
systemInstruction: "You are a helpful assistant for our platform.",
language: "English",
confirmWrites: true,
maxResults: 25,
sensitiveFields: ["ssn", "salary"],
});
return (
<SyncAgentChat
config={config}
persistKey={session.user.id}
accentColor="#6366f1"
context={{ userId: session.user.id, page: "dashboard" }}
/>
);
}From environment variables
import { createServerConfigFromEnv } from "@syncagent/nextjs/server";
// Reads SYNCAGENT_KEY and DATABASE_URL automatically
const config = createServerConfigFromEnv({
filter: { orgId: session.user.orgId },
language: "French",
});# .env.local
SYNCAGENT_KEY=sa_your_api_key
DATABASE_URL=mongodb+srv://user:pass@cluster/dbTools-only mode in Next.js
Create the config on the server, but define tool execute functions on the client (functions can't be serialized across the server/client boundary).
// app/dashboard/page.tsx — Server Component
import { createServerConfig } from "@syncagent/nextjs/server";
import { ChatWithTools } from "./chat-with-tools";
export default async function Page() {
const config = createServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
toolsOnly: true,
systemInstruction: "You are a product search assistant.",
});
return <ChatWithTools serverConfig={config} />;
}// app/dashboard/chat-with-tools.tsx — "use client"
import { SyncAgentChat } from "@syncagent/nextjs";
export function ChatWithTools({ serverConfig }) {
return (
<SyncAgentChat
config={{
...serverConfig,
tools: {
searchProducts: {
description: "Search products",
inputSchema: { query: { type: "string" } },
execute: async ({ query }) => {
const res = await fetch(`/api/products?q=${query}`);
return res.json();
},
},
},
}}
/>
);
}Middleware hooks (client-side only)
Functions like onBeforeToolCall can't be serialized from Server Components. Define them in a Client Component and spread the server config:
// app/components/chat.tsx — "use client"
import { SyncAgentChat } from "@syncagent/nextjs";
export function Chat({ serverConfig }) {
return (
<SyncAgentChat
config={{
...serverConfig,
onBeforeToolCall: (name, args) => {
console.log(`[Audit] ${name}`, args);
return true;
},
}}
/>
);
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| createServerConfig(options) | SyncAgentConfig | server only | Create config — accepts all SyncAgentConfig options |
| createServerConfigFromEnv(overrides?) | SyncAgentConfig | server only | Create config from SYNCAGENT_KEY + DATABASE_URL env vars |
All components and hooks from @syncagent/react are re-exported from @syncagent/nextjs.
Customer Chat — Server-safe Widget
The recommended pattern for customer chat in Next.js: create the config in a Server Component (keeping secrets out of the browser) and pass it to a Client Component that renders the pre-built widget.
// app/support/page.tsx — Server Component
import { createCustomerServerConfig } from "@syncagent/nextjs/server";
import { getServerSession } from "next-auth";
import { CustomerChat } from "./customer-chat";
export default async function SupportPage() {
const session = await getServerSession();
// Create config on the server — secrets never reach the browser
const config = createCustomerServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
connectionString: process.env.SYNCAGENT_CONNECTION_STRING!,
externalUserId: session.user.id,
filter: { organizationId: session.user.orgId },
});
return <CustomerChat config={config} />;
}// app/support/customer-chat.tsx — Client Component
"use client";
import { SyncAgentCustomerChat } from "@syncagent/nextjs";
import type { SyncAgentConfig, GuestIdentity } from "@syncagent/nextjs";
interface Props {
config: SyncAgentConfig;
}
export function CustomerChat({ config }: Props) {
return (
<SyncAgentCustomerChat
config={config}
// Display
mode="floating"
position="bottom-right"
title="Support"
subtitle="We're here to help"
placeholder="Ask us anything..."
welcomeMessage="Hi! How can we help you today?"
// Theming
accentColor="#6366f1"
darkMode={false}
// Real-time (optional)
pusherKey={process.env.NEXT_PUBLIC_PUSHER_KEY}
pusherCluster="us2"
// Events
onEscalated={() => console.log("Escalated")}
onResolved={(id) => console.log("Resolved:", id)}
onGuestIdentified={(identity: GuestIdentity) => {
console.log("Guest:", identity.guestId);
}}
/>
);
}Using createServerConfigFromEnv() for simpler setups:
// app/support/page.tsx — Server Component
import { createServerConfigFromEnv } from "@syncagent/nextjs/server";
import { CustomerChat } from "./customer-chat";
export default async function SupportPage() {
// Reads SYNCAGENT_KEY and DATABASE_URL from env automatically
const config = createServerConfigFromEnv({
customerMode: true,
externalUserId: "user_123",
});
return <CustomerChat config={config} />;
}# .env.local
SYNCAGENT_KEY=sa_your_api_key
SYNCAGENT_CONNECTION_STRING=mongodb+srv://user:pass@cluster/db
NEXT_PUBLIC_PUSHER_KEY=your_pusher_key # optional — for real-time agent messages🔒Never use the NEXT_PUBLIC_ prefix for SYNCAGENT_KEY or SYNCAGENT_CONNECTION_STRING — they contain secrets that must stay on the server. Only NEXT_PUBLIC_PUSHER_KEY is safe for the client.
Vue SDK — @syncagent/vue
Vue 3 composables for SyncAgent chat. Works with Nuxt, Vite, and any Vue 3 app.
💡Install: npm install @syncagent/vue @syncagent/js
Basic usage
<script setup lang="ts">
import { ref } from "vue";
import { SyncAgentClient } from "@syncagent/js";
import { useSyncAgent } from "@syncagent/vue";
const client = new SyncAgentClient({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
connectionString: import.meta.env.VITE_DATABASE_URL,
});
const { messages, isLoading, error, status, sendMessage, stop, reset } = useSyncAgent({ client });
const input = ref("");
function send() {
if (!input.value.trim()) return;
sendMessage(input.value);
input.value = "";
}
</script>
<template>
<div>
<div v-if="status">⏳ {{ status.label }}</div>
<div v-for="(msg, i) in messages" :key="i" :class="msg.role">
{{ msg.content }}
</div>
<div v-if="error">⚠️ {{ error.message }}</div>
<input v-model="input" @keydown.enter="send" :disabled="isLoading" />
<button @click="send" :disabled="isLoading">Send</button>
<button v-if="isLoading" @click="stop">Stop</button>
<button @click="reset">Clear</button>
</div>
</template>Multi-tenant SaaS
<script setup lang="ts">
import { computed } from "vue";
import { SyncAgentClient } from "@syncagent/js";
import { useSyncAgent } from "@syncagent/vue";
import { useAuth } from "@/composables/auth";
const { user } = useAuth();
const client = computed(() => new SyncAgentClient({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
connectionString: import.meta.env.VITE_DATABASE_URL,
filter: { organizationId: user.value.orgId },
operations: user.value.isAdmin ? ["read","create","update","delete"] : ["read"],
}));
const { messages, sendMessage } = useSyncAgent({
client: client.value,
context: { userId: user.value.id },
});
</script>useSyncAgent return values
| Prop | Type | Default | Description |
|---|---|---|---|
| messages | Ref<Message[]> | — | Conversation history |
| isLoading | Ref<boolean> | — | True while streaming |
| error | Ref<Error|null> | — | Last error |
| status | Ref<{step,label}|null> | — | Live status while agent works |
| lastData | Ref<ToolData|null> | — | Last DB query result |
| sendMessage | (content: string) => Promise<void> | — | Send a message |
| stop | () => void | — | Abort current stream |
| reset | () => void | — | Clear all messages |
Customer Chat — Pre-built Widget
Drop in a complete customer support chat widget as a Vue component. Handles guest identification, AI messaging, escalation, real-time Pusher messages, satisfaction rating, and WCAG AA theming.
<script setup lang="ts">
import { SyncAgentCustomerChat } from "@syncagent/vue";
import type { GuestIdentity } from "@syncagent/vue";
function onEscalated() {
console.log("Conversation escalated to a human agent");
}
function onResolved(conversationId: string) {
console.log("Conversation resolved:", conversationId);
}
function onGuestIdentified(identity: GuestIdentity) {
console.log("Guest identified:", identity.name, identity.guestId);
}
</script>
<template>
<SyncAgentCustomerChat
api-key="sa_your_api_key"
connection-string="your_connection_string"
mode="floating"
position="bottom-right"
:default-open="false"
title="Support"
subtitle="We're here to help"
placeholder="Type your message..."
welcome-message="Hi! How can we help you today?"
accent-color="#6366f1"
:dark-mode="false"
pusher-key="your_pusher_key"
pusher-cluster="us2"
@escalated="onEscalated"
@resolved="onResolved"
@guest-identified="onGuestIdentified"
/>
</template>Inline mode — embed inside your layout:
<template>
<div style="height: 600px; width: 400px;">
<SyncAgentCustomerChat
api-key="sa_your_api_key"
connection-string="your_connection_string"
mode="inline"
:dark-mode="true"
accent-color="#8b5cf6"
/>
</div>
</template>Skip the guest form for authenticated users:
<template>
<SyncAgentCustomerChat
api-key="sa_your_api_key"
connection-string="your_connection_string"
:external-user-id="currentUser.id"
/>
</template>Custom guest form text:
<script setup lang="ts">
import { SyncAgentCustomerChat } from "@syncagent/vue";
const guestFormConfig = {
title: "Welcome!",
subtitle: "Tell us about yourself to get started",
submitButtonText: "Start Chat",
namePlaceholder: "Your name",
emailPlaceholder: "you@company.com",
phonePlaceholder: "+1 (555) 000-0000",
};
</script>
<template>
<SyncAgentCustomerChat
api-key="sa_your_api_key"
connection-string="your_connection_string"
:guest-form="guestFormConfig"
/>
</template>💡For custom UI, use the useCustomerChat composable instead — see the Customer Agent Mode section for the composable API.
Angular SDK — @syncagent/angular
Injectable Angular service with both Angular Signals and RxJS observables.
💡Install: npm install @syncagent/angular @syncagent/js
Basic usage
// app.component.ts
import { Component, OnInit } from "@angular/core";
import { SyncAgentService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-root",
providers: [SyncAgentService],
template: `
<div *ngIf="agent.status() as s">⏳ {{ s.label }}</div>
<div *ngFor="let msg of agent.messages()" [class]="msg.role">
{{ msg.content }}
</div>
<div *ngIf="agent.error() as err">
⚠️ {{ err.message }}
</div>
<input [(ngModel)]="input" (keydown.enter)="send()" [disabled]="agent.isLoading()" />
<button (click)="send()" [disabled]="agent.isLoading()">Send</button>
<button *ngIf="agent.isLoading()" (click)="agent.stop()">Stop</button>
<button (click)="agent.reset()">Clear</button>
`,
})
export class AppComponent implements OnInit {
input = "";
constructor(public agent: SyncAgentService) {}
ngOnInit() {
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
});
}
send() {
if (!this.input.trim()) return;
this.agent.sendMessage(this.input);
this.input = "";
}
}Multi-tenant SaaS
ngOnInit() {
const user = this.authService.currentUser;
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
filter: { organizationId: user.orgId },
operations: user.isAdmin
? ["read", "create", "update", "delete"]
: ["read"],
context: { userId: user.id, userRole: user.role },
});
}Using RxJS observables
// Subscribe to messages stream
this.agent.messages$
.pipe(takeUntil(this.destroy$))
.subscribe(messages => console.log(messages.length));
// Subscribe to each new assistant message
this.agent.message$
.pipe(takeUntil(this.destroy$))
.subscribe(msg => console.log(msg.content));
// Subscribe to status
this.agent.status$
.pipe(takeUntil(this.destroy$), filter(Boolean))
.subscribe(({ step, label }) => console.log(`[${step}] ${label}`));SyncAgentService API
| Prop | Type | Default | Description |
|---|---|---|---|
| configure(config) | void | — | Initialize with API key, connection string, and options |
| sendMessage(content) | Promise<void> | — | Send a message and start streaming |
| stop() | void | — | Abort current stream |
| reset() | void | — | Clear all messages |
| messages() | Message[] | Signal | Conversation history (Angular Signal) |
| isLoading() | boolean | Signal | True while streaming |
| error() | Error|null | Signal | Last error |
| status() | {step,label}|null | Signal | Live status |
| messages$ | Observable<Message[]> | RxJS | Conversation history stream |
| message$ | Observable<Message> | RxJS | Emits each new assistant message |
| status$ | Observable<{step,label}|null> | RxJS | Status stream |
Customer Chat — Pre-built Widget
Drop in a complete customer support chat widget as an Angular standalone component. Handles guest identification, AI messaging, escalation, real-time Pusher messages, satisfaction rating, and WCAG AA theming.
import { Component } from "@angular/core";
import { SyncAgentCustomerChatComponent } from "@syncagent/angular";
import type { GuestIdentity } from "@syncagent/angular";
@Component({
selector: "app-support",
standalone: true,
imports: [SyncAgentCustomerChatComponent],
template: `
<syncagent-customer-chat
[apiKey]="apiKey"
[connectionString]="connectionString"
mode="floating"
position="bottom-right"
[defaultOpen]="false"
title="Support"
subtitle="We're here to help"
placeholder="Type your message..."
welcomeMessage="Hi! How can we help you today?"
accentColor="#6366f1"
[darkMode]="false"
[pusherKey]="pusherKey"
pusherCluster="us2"
[externalUserId]="currentUser?.id"
[metadata]="{ page: currentRoute }"
[guestForm]="guestFormConfig"
(escalated)="onEscalated()"
(resolved)="onResolved($event)"
(guestIdentified)="onGuestIdentified($event)"
/>
`,
})
export class SupportComponent {
apiKey = "sa_your_api_key";
connectionString = "your_connection_string";
pusherKey = "your_pusher_key"; // optional
currentUser = { id: "user_123" }; // from your auth service
currentRoute = "/support";
guestFormConfig = {
title: "Welcome!",
subtitle: "Tell us about yourself to get started",
submitButtonText: "Start Chat",
namePlaceholder: "Your name",
emailPlaceholder: "you@company.com",
phonePlaceholder: "+1 (555) 000-0000",
};
onEscalated() {
console.log("Conversation escalated to a human agent");
}
onResolved(conversationId: string) {
console.log("Conversation resolved:", conversationId);
}
onGuestIdentified(identity: GuestIdentity) {
console.log("Guest identified:", identity.name, identity.guestId);
}
}Inline mode — embed inside your layout:
@Component({
imports: [SyncAgentCustomerChatComponent],
template: `
<div style="height: 600px; width: 400px;">
<syncagent-customer-chat
[apiKey]="apiKey"
[connectionString]="connectionString"
mode="inline"
[darkMode]="true"
accentColor="#8b5cf6"
/>
</div>
`,
})
export class InlineChatComponent {
apiKey = "sa_your_api_key";
connectionString = "your_connection_string";
}The component is standalone — just import it directly in your component's imports array. No module registration needed.
💡For custom UI, use the CustomerChatService directly — see the Customer Agent Mode section for the service API with signals and observables.
Customer Agent Mode
Overview
Customer Agent Mode transforms SyncAgent into a full customer support AI pipeline. Instead of querying your database directly, messages are routed through a configurable support system that includes:
- Persona — A customizable AI personality that matches your brand voice and tone
- Flows — Guided conversation flows for common support scenarios (returns, billing, onboarding)
- Knowledge Base — AI searches your docs, FAQs, and help articles to answer questions accurately
- Escalation — Automatic handoff to human agents when the AI cannot resolve an issue
- AI Fallback Pipeline — When no flow matches, the AI uses the knowledge base and persona to generate helpful responses
Conversations are scoped per customer using externalUserId, enabling persistent history, ratings, and escalation tracking across sessions.
Configuration
Enable customer agent mode by providing an externalUserId in your client config.
| Prop | Type | Default | Description |
|---|---|---|---|
| customerMode | boolean | false | Enable customer agent mode. Routes messages through the support pipeline instead of the database agent. Automatically enabled when externalUserId is present. |
| externalUserId | string | — | Unique identifier for the customer. Scopes conversations, history, and ratings to this user. When provided, customerMode is automatically enabled and both chat() and customerChat() become available. |
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_key",
externalUserId: "customer_123", // from your auth session
});
// Send a customer support message
const result = await client.customerChat("How do I reset my password?");
console.log(result.response); // AI-generated answer
console.log(result.escalated); // false — handled by AI
console.log(result.conversationId); // "conv_abc123"
ℹ️customerMode is automatically enabled when externalUserId is present in the configuration. You no longer need to set customerMode: true explicitly.
🔒The externalUserId should come from an authenticated session. It scopes all conversations to a specific customer — never let end users set this value directly.
React — useCustomerChat Hook
The React SDK provides a useCustomerChat hook that wraps the JS SDK with React state management. Use it inside a SyncAgentProvider or pass a client directly.
import { SyncAgentProvider, useCustomerChat } from "@syncagent/react";
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_key",
externalUserId: "customer_123",
});
function SupportChat() {
const {
messages,
conversationId,
isLoading,
isEscalated,
isResolved,
error,
welcomeMessage,
sendMessage,
rateConversation,
reset,
} = useCustomerChat({
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Resolved:", id),
});
return (
<div>
{welcomeMessage && <p>{welcomeMessage}</p>}
{messages.map((msg, i) => (
<div key={i} className={msg.role}>{msg.content}</div>
))}
{isEscalated && <p>A human agent will be with you shortly.</p>}
{isResolved && (
<button onClick={() => rateConversation(5)}>Rate ⭐</button>
)}
<button onClick={() => sendMessage("Help me!")} disabled={isLoading}>
Send
</button>
</div>
);
}
export default function App() {
return (
<SyncAgentProvider config={{ apiKey: "sa_your_key", externalUserId: "customer_123" }}>
<SupportChat />
</SyncAgentProvider>
);
}💡The hook resolves the client from SyncAgentProvider context automatically. You can also pass a client directly via useCustomerChat({ client }).
Vue — useCustomerChat Composable
The Vue SDK provides a useCustomerChat composable that returns Vue Ref values for reactive state.
<script setup lang="ts">
import { ref } from "vue";
import { SyncAgentClient } from "@syncagent/js";
import { useCustomerChat } from "@syncagent/vue";
const client = new SyncAgentClient({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
externalUserId: "customer_123",
});
const {
messages, // Ref<Message[]>
conversationId, // Ref<string | null>
isLoading, // Ref<boolean>
isEscalated, // Ref<boolean>
isResolved, // Ref<boolean>
error, // Ref<Error | null>
welcomeMessage, // Ref<string | null>
sendMessage,
rateConversation,
reset,
} = useCustomerChat({
client,
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Resolved:", id),
});
const input = ref("");
function send() {
if (!input.value.trim()) return;
sendMessage(input.value);
input.value = "";
}
</script>
<template>
<div>
<p v-if="welcomeMessage">{{ welcomeMessage }}</p>
<div v-for="(msg, i) in messages" :key="i" :class="msg.role">
{{ msg.content }}
</div>
<p v-if="isEscalated">A human agent will be with you shortly.</p>
<div v-if="isResolved">
<button @click="rateConversation(5)">Rate ⭐</button>
</div>
<input v-model="input" @keydown.enter="send" :disabled="isLoading" />
<button @click="send" :disabled="isLoading">Send</button>
<button @click="reset">Clear</button>
</div>
</template>Angular — CustomerChatService
The Angular SDK provides an injectable CustomerChatService with both Angular signals and RxJS observables.
import { Component, OnInit } from "@angular/core";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-support-chat",
providers: [CustomerChatService],
template: `
<p *ngIf="chat.welcomeMessage()">{{ chat.welcomeMessage() }}</p>
<div *ngFor="let msg of chat.messages()" [class]="msg.role">
{{ msg.content }}
</div>
<p *ngIf="chat.isEscalated()">A human agent will be with you shortly.</p>
<div *ngIf="chat.isResolved()">
<button (click)="rate(5)">Rate ⭐</button>
</div>
<input [(ngModel)]="input" (keydown.enter)="send()" [disabled]="chat.isLoading()" />
<button (click)="send()" [disabled]="chat.isLoading()">Send</button>
`,
})
export class SupportChatComponent implements OnInit {
input = "";
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123", // from your auth service
onEscalated: () => console.log("Escalated"),
onResolved: (id) => console.log("Resolved:", id),
});
}
send() {
if (!this.input.trim()) return;
this.chat.sendMessage(this.input);
this.input = "";
}
rate(value: number) {
this.chat.rateConversation(value);
}
}💡Use Angular signals (chat.messages(), chat.isEscalated()) in templates, or subscribe to RxJS observables (chat.messages$, chat.escalated$) for imperative logic.
Next.js — createCustomerServerConfig
The Next.js SDK provides a createCustomerServerConfig server helper that sets up customer mode securely from a Server Component. The externalUserId is required — omitting it throws an error.
// app/support/page.tsx — Server Component
import { createCustomerServerConfig } from "@syncagent/nextjs/server";
import { getServerSession } from "next-auth";
import { CustomerWidget } from "./customer-widget";
export default async function SupportPage() {
const session = await getServerSession();
const config = createCustomerServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
connectionString: process.env.DATABASE_URL!,
externalUserId: session.user.id, // required — throws if omitted
filter: { organizationId: session.user.orgId },
});
return <CustomerWidget config={config} />;
}// app/support/customer-widget.tsx — "use client"
"use client";
import { useCustomerChat } from "@syncagent/nextjs"; // re-exported from @syncagent/react
import { SyncAgentClient } from "@syncagent/js";
export function CustomerWidget({ config }: { config: any }) {
const client = new SyncAgentClient(config);
const { messages, sendMessage, isEscalated, rateConversation } = useCustomerChat({ client });
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={msg.role}>{msg.content}</div>
))}
{isEscalated && <p>Connecting you to a human agent...</p>}
<button onClick={() => sendMessage("I need help")}>Send</button>
</div>
);
}ℹ️The useCustomerChat hook is re-exported from @syncagent/nextjs for convenience — no need to install @syncagent/react separately.
Response Shape
Every call to customerChat() returns a CustomerChatResult object with the following fields:
| Prop | Type | Default | Description |
|---|---|---|---|
| escalated | boolean | false | Whether the conversation was escalated to a human agent during this message |
| autoReply | boolean | false | Whether the response was generated by an automated flow (not the AI fallback) |
| flowActive | boolean | false | Whether a guided conversation flow is currently active |
| resolved | boolean | false | Whether the conversation has been marked as resolved |
| welcomeMessage | string | undefined | — | Welcome message returned on the first interaction only |
| sources | any[] | undefined | — | Knowledge base sources used to generate the response |
| flowSession | { flowId: string; currentNodeId: string } | undefined | — | Active flow session state when a flow is in progress |
💡The result also includes conversationId and response — use conversationId to continue the conversation and response for the AI-generated text.
Rating
After a conversation is resolved, you can submit a satisfaction rating (integer 1–5) using rateConversation(). Ratings are tied to the conversation and visible in your dashboard analytics.
// Rate a resolved conversation (1 = poor, 5 = excellent)
await client.rateConversation(result.conversationId, 5);
// Validation rules:
// - conversationId is required (throws if empty)
// - rating must be an integer between 1 and 5 (throws otherwise)
ℹ️Ratings are typically collected after the conversation ends. Show a rating prompt when result.resolved is true or when the onResolved callback fires.
Callbacks
Register callbacks to react to escalation and resolution events in real time. These fire automatically after each customerChat() call when the corresponding condition is met.
| Prop | Type | Default | Description |
|---|---|---|---|
| onEscalated | () => void | — | Called when the conversation is escalated to a human agent. Use this to update your UI (e.g., show a 'connecting to agent' banner). |
| onResolved | (conversationId: string) => void | — | Called when the conversation is resolved. Receives the conversation ID — use it to prompt for a rating. |
const result = await client.customerChat("I need to cancel my subscription", {
conversationId: "conv_abc123",
onEscalated: () => {
// Show a banner: "Connecting you to a human agent..."
showBanner("A support agent will be with you shortly.");
},
onResolved: (conversationId) => {
// Prompt the user to rate the conversation
showRatingDialog(conversationId);
},
});💡In React, use the onEscalated and onResolved options in useCustomerChat for the same behavior with automatic state management.
Integration Guide
A step-by-step guide to embedding a customer support chat widget in your application — from project setup to production.
1. Project Setup & Installation
Start with a React project (Vite, Next.js, or Create React App) and install the SyncAgent packages:
npm install @syncagent/react @syncagent/js2. Configure the Client in Customer Mode
Create a SyncAgentClient with an authenticated externalUserId:
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_key",
externalUserId: currentUser.id, // from your auth session
});3. Build the Chat UI
Use the useCustomerChat hook to manage conversation state. Here is a complete minimal customer support chat component:
import { useState } from "react";
import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentProvider, useCustomerChat } from "@syncagent/react";
// Configure the client
const client = new SyncAgentClient({
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY!,
externalUserId: "user_123", // replace with authenticated user ID
});
function CustomerSupportChat() {
const {
messages,
isLoading,
isEscalated,
isResolved,
welcomeMessage,
sendMessage,
rateConversation,
reset,
} = useCustomerChat({
onEscalated: () => {
// Notify the user that a human agent is taking over
},
onResolved: (conversationId) => {
// Prompt the user to rate the conversation
},
});
const [input, setInput] = useState("");
const handleSend = () => {
if (!input.trim()) return;
sendMessage(input);
setInput("");
};
return (
<div className="chat-container">
{/* Escalation banner */}
{isEscalated && (
<div className="escalation-banner">
🙋 A human agent is reviewing your conversation.
</div>
)}
{/* Welcome message */}
{welcomeMessage && (
<div className="welcome">{welcomeMessage}</div>
)}
{/* Messages */}
{messages.map((msg, i) => (
<div key={i} className={msg.role === "user" ? "user-msg" : "agent-msg"}>
{msg.content}
</div>
))}
{isLoading && <div className="typing-indicator">Agent is typing...</div>}
{/* Rating prompt after resolution */}
{isResolved && (
<div className="rating-prompt">
<p>How was your experience?</p>
{[1, 2, 3, 4, 5].map((star) => (
<button key={star} onClick={() => rateConversation(star)}>
{"⭐".repeat(star)}
</button>
))}
</div>
)}
{/* Input */}
<div className="input-row">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
placeholder="Type your message..."
disabled={isLoading}
/>
<button onClick={handleSend} disabled={isLoading}>Send</button>
</div>
<button onClick={reset}>New Conversation</button>
</div>
);
}
// Wrap with provider
export default function App() {
return (
<SyncAgentProvider config={{ apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY!, externalUserId: "user_123" }}>
<CustomerSupportChat />
</SyncAgentProvider>
);
}4. Handle Escalation
When the AI cannot resolve an issue, it escalates to a human agent. The isEscalated flag becomes true and the onEscalated callback fires. Recommended UI patterns:
- Show a prominent banner or toast: "A human agent will be with you shortly"
- Disable the input or show a waiting state while the handoff occurs
- Display estimated wait time if available from your support system
- Keep the conversation history visible so the user sees context is preserved
5. Add Conversation Rating
After a conversation is resolved (isResolved === true), prompt the user to rate their experience. Ratings are integers from 1 to 5:
// Rate after resolution
await rateConversation(5); // 1-5 scaleConversation Lifecycle
Every customer conversation follows this lifecycle:
- New conversation — User sends their first message. A
conversationIdis created and awelcomeMessagemay be returned. - Messages — The AI responds using the configured persona, knowledge base, and flows. Each reply may include
sourcesfrom the knowledge base. - Resolution or Escalation — The conversation ends in one of two ways: the AI resolves the issue (
resolved: true) or escalates to a human agent (escalated: true). - Rating — After resolution, the user rates the conversation (1-5). Call
rateConversation(rating)to submit.
🔒The externalUserId scopes all conversations to a specific customer. Each user only sees their own conversation history. Always derive this value from an authenticated session (e.g., JWT claims, server session) — never from client-side input that can be spoofed. In Next.js, use createCustomerServerConfig to set it securely from a Server Component.
Dual Mode (Database + Customer Agent)
⚠️Deprecated: The createDual() pattern shown below is deprecated and will be removed in a future major version. Use the Unified Dual Mode section instead — pass externalUserId directly to the constructor to enable both modes on a single instance.
Use SyncAgentClient.createDual() when your app needs both a database agent (for admins/internal users) and a customer agent (for end-users) from a single configuration.
import { SyncAgentClient } from "@syncagent/js";
const { db, support } = SyncAgentClient.createDual({
apiKey: "sa_your_key",
connectionString: "postgresql://user:pass@host:5432/mydb",
externalUserId: currentUser.id, // from your auth session
});
// Admin: direct database queries
const result = await db.chat([
{ role: "user", content: "Show all overdue invoices" }
]);
// Customer: support pipeline (persona, flows, KB, escalation)
const reply = await support.customerChat("I need help with my order");
await support.rateConversation(reply.conversationId, 5);| Prop | Type | Default | Description |
|---|---|---|---|
| db | SyncAgentClient | — | Database agent instance (customerMode: false). Use db.chat() for direct DB queries. |
| support | SyncAgentClient | — | Customer agent instance (customerMode: true). Use support.customerChat() for the support pipeline. |
💡Both clients share the same API key and connection string. The externalUserId is required and applied only to the customer agent. The database agent ignores it.
React — Full Dual Mode Setup
Create both clients once and use them in separate components — an admin panel with database access and a customer support widget.
import { useState } from "react";
import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentProvider, useSyncAgent, useCustomerChat } from "@syncagent/react";
// Create both clients from a single config
const { db, support } = SyncAgentClient.createDual({
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY!,
connectionString: process.env.NEXT_PUBLIC_DATABASE_URL!,
externalUserId: "user_123", // from your auth session
filter: { organizationId: "org_456" }, // multi-tenant scoping
});
// --- Admin Panel (Database Agent) ---
function AdminPanel() {
const { messages, sendMessage, isLoading } = useSyncAgent({ client: db });
const [input, setInput] = useState("");
return (
<div className="admin-panel">
<h2>Admin Dashboard</h2>
{messages.map((msg, i) => (
<div key={i} className={msg.role}>{msg.content}</div>
))}
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && input.trim()) {
sendMessage(input);
setInput("");
}
}}
placeholder="Query your database..."
disabled={isLoading}
/>
</div>
);
}
// --- Customer Support Widget (Customer Agent) ---
function CustomerWidget() {
const {
messages,
sendMessage,
isLoading,
isEscalated,
isResolved,
welcomeMessage,
rateConversation,
} = useCustomerChat({
client: support,
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Resolved:", id),
});
const [input, setInput] = useState("");
return (
<div className="customer-widget">
<h2>Need Help?</h2>
{welcomeMessage && <p className="welcome">{welcomeMessage}</p>}
{messages.map((msg, i) => (
<div key={i} className={msg.role}>{msg.content}</div>
))}
{isEscalated && <p>🙋 A human agent will be with you shortly.</p>}
{isResolved && (
<div>
<p>✅ Resolved! How was your experience?</p>
{[1, 2, 3, 4, 5].map((r) => (
<button key={r} onClick={() => rateConversation(r)}>{"⭐".repeat(r)}</button>
))}
</div>
)}
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && input.trim()) {
sendMessage(input);
setInput("");
}
}}
placeholder="Ask a question..."
disabled={isLoading}
/>
</div>
);
}
// --- App Layout ---
export default function App() {
return (
<div className="app-layout">
<AdminPanel />
<CustomerWidget />
</div>
);
}Next.js — Full Dual Mode Setup
In Next.js, create both clients in a Server Component (keeping secrets server-side) and pass configs to Client Components.
// app/dashboard/page.tsx — Server Component
import { SyncAgentClient } from "@syncagent/js";
import { getServerSession } from "next-auth";
import { AdminPanel } from "./admin-panel";
import { CustomerWidget } from "./customer-widget";
export default async function DashboardPage() {
const session = await getServerSession();
const { db, support } = SyncAgentClient.createDual({
apiKey: process.env.SYNCAGENT_KEY!,
connectionString: process.env.DATABASE_URL!,
externalUserId: session.user.id,
filter: { organizationId: session.user.orgId },
});
return (
<div className="grid grid-cols-2 gap-4">
{/* Admin: database queries for internal staff */}
<AdminPanel />
{/* Customer: support widget for end-users */}
<CustomerWidget userId={session.user.id} />
</div>
);
}// app/dashboard/admin-panel.tsx — "use client"
"use client";
import { SyncAgentChat } from "@syncagent/nextjs";
export function AdminPanel() {
return (
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY!,
connectionString: process.env.NEXT_PUBLIC_DATABASE_URL!,
systemInstruction: "You are an admin assistant. Help staff query orders, users, and invoices.",
}}
/>
);
}// app/dashboard/customer-widget.tsx — "use client"
"use client";
import { useState } from "react";
import { SyncAgentClient } from "@syncagent/js";
import { useCustomerChat } from "@syncagent/nextjs";
export function CustomerWidget({ userId }: { userId: string }) {
const client = new SyncAgentClient({
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY!,
connectionString: process.env.NEXT_PUBLIC_DATABASE_URL!,
externalUserId: userId,
});
const {
messages, sendMessage, isLoading,
isEscalated, isResolved, rateConversation,
} = useCustomerChat({ client });
const [input, setInput] = useState("");
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.role}: {msg.content}</div>
))}
{isEscalated && <p>Connecting to a human agent...</p>}
{isResolved && <button onClick={() => rateConversation(5)}>Rate ⭐⭐⭐⭐⭐</button>}
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") { sendMessage(input); setInput(""); } }}
disabled={isLoading}
placeholder="How can we help?"
/>
</div>
);
}Vue — Full Dual Mode Setup
Use both composables in a single component or split them across views.
<script setup lang="ts">
import { ref } from "vue";
import { SyncAgentClient } from "@syncagent/js";
import { useSyncAgent, useCustomerChat } from "@syncagent/vue";
const { db, support } = SyncAgentClient.createDual({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
connectionString: import.meta.env.VITE_DATABASE_URL,
externalUserId: "customer_123",
filter: { organizationId: "org_456" },
});
// Database agent (admin)
const {
messages: adminMessages,
sendMessage: adminSend,
isLoading: adminLoading,
} = useSyncAgent({ client: db });
// Customer agent (support)
const {
messages: supportMessages,
sendMessage: supportSend,
isLoading: supportLoading,
isEscalated,
isResolved,
welcomeMessage,
rateConversation,
} = useCustomerChat({
client: support,
onEscalated: () => console.log("Escalated"),
onResolved: (id) => console.log("Resolved:", id),
});
const adminInput = ref("");
const supportInput = ref("");
function sendAdmin() {
if (!adminInput.value.trim()) return;
adminSend(adminInput.value);
adminInput.value = "";
}
function sendSupport() {
if (!supportInput.value.trim()) return;
supportSend(supportInput.value);
supportInput.value = "";
}
</script>
<template>
<div class="dual-layout">
<!-- Admin Panel -->
<div class="admin-panel">
<h2>Admin Dashboard</h2>
<div v-for="(msg, i) in adminMessages" :key="'a'+i" :class="msg.role">
{{ msg.content }}
</div>
<input v-model="adminInput" @keydown.enter="sendAdmin" :disabled="adminLoading" placeholder="Query database..." />
<button @click="sendAdmin" :disabled="adminLoading">Send</button>
</div>
<!-- Customer Support -->
<div class="customer-widget">
<h2>Customer Support</h2>
<p v-if="welcomeMessage">{{ welcomeMessage }}</p>
<div v-for="(msg, i) in supportMessages" :key="'s'+i" :class="msg.role">
{{ msg.content }}
</div>
<p v-if="isEscalated">🙋 A human agent will be with you shortly.</p>
<div v-if="isResolved">
<p>✅ Resolved!</p>
<button v-for="r in 5" :key="r" @click="rateConversation(r)">{{ '⭐'.repeat(r) }}</button>
</div>
<input v-model="supportInput" @keydown.enter="sendSupport" :disabled="supportLoading" placeholder="Ask a question..." />
<button @click="sendSupport" :disabled="supportLoading">Send</button>
</div>
</div>
</template>Angular — Full Dual Mode Setup
Provide both SyncAgentService and CustomerChatService at the component level and configure each independently.
import { Component, OnInit } from "@angular/core";
import { SyncAgentService, CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-dual-dashboard",
providers: [SyncAgentService, CustomerChatService],
template: `
<div class="dual-layout">
<!-- Admin Panel (Database Agent) -->
<div class="admin-panel">
<h2>Admin Dashboard</h2>
<div *ngFor="let msg of admin.messages()" [class]="msg.role">
{{ msg.content }}
</div>
<input
[(ngModel)]="adminInput"
(keydown.enter)="sendAdmin()"
[disabled]="admin.isLoading()"
placeholder="Query database..."
/>
<button (click)="sendAdmin()" [disabled]="admin.isLoading()">Send</button>
</div>
<!-- Customer Support Widget -->
<div class="customer-widget">
<h2>Customer Support</h2>
<p *ngIf="support.welcomeMessage()">{{ support.welcomeMessage() }}</p>
<div *ngFor="let msg of support.messages()" [class]="msg.role">
{{ msg.content }}
</div>
<p *ngIf="support.isEscalated()">🙋 A human agent will be with you shortly.</p>
<div *ngIf="support.isResolved()">
<p>✅ Resolved! Rate your experience:</p>
<button *ngFor="let r of [1,2,3,4,5]" (click)="rate(r)">{{ r }}⭐</button>
</div>
<input
[(ngModel)]="supportInput"
(keydown.enter)="sendSupport()"
[disabled]="support.isLoading()"
placeholder="Ask a question..."
/>
<button (click)="sendSupport()" [disabled]="support.isLoading()">Send</button>
</div>
</div>
`,
})
export class DualDashboardComponent implements OnInit {
adminInput = "";
supportInput = "";
constructor(
public admin: SyncAgentService,
public support: CustomerChatService,
) {}
ngOnInit() {
// Configure database agent (admin)
this.admin.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
filter: { organizationId: "org_456" },
systemInstruction: "You are an admin assistant for internal staff.",
});
// Configure customer agent (support)
this.support.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123", // from your auth service
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Conversation resolved:", id),
});
}
sendAdmin() {
if (!this.adminInput.trim()) return;
this.admin.sendMessage(this.adminInput);
this.adminInput = "";
}
sendSupport() {
if (!this.supportInput.trim()) return;
this.support.sendMessage(this.supportInput);
this.supportInput = "";
}
rate(value: number) {
this.support.rateConversation(value);
}
}ℹ️Each service is provided at the component level, so they maintain independent state. You can also split them into separate components with their own providers for better separation of concerns.
Customer Chat Widget
Overview
The <SyncAgentCustomerChat> component is a pre-built, drop-in customer support widget available for React, Next.js, Vue, and Angular. It handles the full conversation lifecycle — guest identification, AI messaging, escalation to human agents via Pusher, satisfaction rating, and WCAG AA-compliant theming — all with zero custom UI required.
React
import { SyncAgentCustomerChat } from "@syncagent/react";
export default function App() {
return (
<SyncAgentCustomerChat
apiKey="sa_your_api_key"
connectionString={process.env.NEXT_PUBLIC_DATABASE_URL!}
accentColor="#6366f1"
onEscalated={() => console.log("Escalated")}
onResolved={(id) => console.log("Resolved:", id)}
/>
);
}Next.js (Server Config)
Keep secrets on the server — create the config in a Server Component and pass it to the client widget.
// app/support/page.tsx — Server Component
import { createCustomerServerConfig } from "@syncagent/nextjs/server";
import { CustomerChat } from "./customer-chat";
export default async function SupportPage() {
const config = createCustomerServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
connectionString: process.env.DATABASE_URL!,
externalUserId: session.user.id,
});
return <CustomerChat config={config} />;
}
// app/support/customer-chat.tsx — "use client"
"use client";
import { SyncAgentCustomerChat } from "@syncagent/nextjs";
export function CustomerChat({ config }) {
return (
<SyncAgentCustomerChat
config={config}
title="Support"
accentColor="#6366f1"
darkMode={false}
/>
);
}Vue 3
<script setup lang="ts">
import { SyncAgentCustomerChat } from "@syncagent/vue";
</script>
<template>
<SyncAgentCustomerChat
api-key="sa_your_api_key"
connection-string="your_connection_string"
accent-color="#6366f1"
@escalated="() => console.log('Escalated')"
@resolved="(id) => console.log('Resolved:', id)"
/>
</template>Angular
import { SyncAgentCustomerChatComponent } from "@syncagent/angular";
@Component({
imports: [SyncAgentCustomerChatComponent],
template: `
<syncagent-customer-chat
[apiKey]="'sa_your_api_key'"
[connectionString]="'your_connection_string'"
accentColor="#6366f1"
(escalated)="onEscalated()"
(resolved)="onResolved($event)"
/>
`,
})
export class SupportComponent {
onEscalated() { console.log("Escalated"); }
onResolved(id: string) { console.log("Resolved:", id); }
}Props / Inputs
All frameworks share the same configuration surface. React uses props, Vue uses kebab-case props, Angular uses inputs.
| Prop | Type | Default | Description |
|---|---|---|---|
| config | SyncAgentConfig | — | Server config object. When provided, apiKey/connectionString are ignored. |
| apiKey | string | — | API key for authentication (ignored if config provided) |
| connectionString | string | — | Optional — database connection string. When provided, the agent can also query your DB. Without it, uses only persona, KB, flows, and escalation. |
| externalUserId | string | — | Authenticated user ID — skips guest form when provided |
| mode | "floating" | "inline" | "floating" | Floating toggle button or embedded inline panel |
| position | "bottom-right" | "bottom-left" | "bottom-right" | Position of the floating widget (floating mode only) |
| defaultOpen | boolean | false | Whether the floating panel starts open (floating mode only) |
| title | string | "Customer Support" | Header title text |
| subtitle | string | "How can we help you?" | Header subtitle text |
| placeholder | string | "Type your message..." | Input placeholder text |
| welcomeMessage | string | — | Initial welcome message displayed before any interaction |
| accentColor | string | "#6366f1" | Primary accent color (hex, rgb, or hsl) |
| darkMode | boolean | false | Enable dark mode color scheme |
| className | string | — | Additional CSS class on the root container |
| guestForm | GuestFormConfig | — | Custom guest form configuration (title, subtitle, placeholders, button text) |
| pusherKey | string | — | Pusher app key for real-time human agent messages |
| pusherCluster | string | "us2" | Pusher cluster |
| metadata | Record<string, any> | — | Custom metadata attached to conversations |
With or Without a Database
The connectionString prop is optional. This gives you two deployment modes:
Just pass apiKey — the agent uses persona, knowledge base, flows, and escalation only.
<SyncAgentCustomerChat
apiKey="sa_your_key"
accentColor="#6366f1"
/>Add connectionString — the agent can also query your DB to answer questions like "What's my order status?"
<SyncAgentCustomerChat
apiKey="sa_your_key"
connectionString={process.env.DATABASE_URL}
accentColor="#6366f1"
/>💡Most customer support use cases work great without a database — the agent answers from your knowledge base and follows conversation flows. Add a connection string only if your customers need to look up account-specific data.
Events / Outputs
| Prop | Type | Default | Description |
|---|---|---|---|
| onEscalated / (escalated) | () => void | — | Fired when the conversation is escalated to a human agent |
| onResolved / (resolved) | (conversationId: string) => void | — | Fired when the conversation is resolved |
| onGuestIdentified / (guestIdentified) | (identity: GuestIdentity) => void | — | Fired after guest form submission |
Theming
The widget uses a built-in theme engine (computeTheme() from @syncagent/js) that generates a complete color palette from your accentColor and darkMode settings. All generated colors meet WCAG AA contrast requirements:
- 4.5:1 — text against backgrounds
- 4.5:1 — user/assistant bubble text against bubble backgrounds
- 3:1 — accent color and focus indicators against backgrounds
All styles are applied inline — no external CSS required and no styles leak to or from your application.
// Light mode with custom brand color
<SyncAgentCustomerChat accentColor="#059669" />
// Dark mode
<SyncAgentCustomerChat darkMode />
// Dark mode with custom accent
<SyncAgentCustomerChat darkMode accentColor="#f59e0b" />Accessibility
The widget is built to meet WCAG 2.1 AA standards across all frameworks:
- ARIA roles — Container uses
role="region", message list usesrole="log"witharia-live="polite" - Keyboard navigation — Tab through elements, Enter/Space for buttons, arrow keys for rating stars
- Focus management — Focus moves to message input within 100ms after guest form submission
- Focus indicators — 2px outline with minimum 3:1 contrast ratio
- Reduced motion — Animations disabled when
prefers-reduced-motionis set
ℹ️Full WCAG validation requires manual testing with assistive technologies and expert accessibility review.
Inline Mode
Embed the chat inside your layout instead of a floating button. The panel fills its parent container and is always visible.
<div style={{ height: 600, width: 400 }}>
<SyncAgentCustomerChat
apiKey="sa_your_api_key"
connectionString={process.env.DATABASE_URL!}
mode="inline"
/>
</div>Custom Guest Form
<SyncAgentCustomerChat
apiKey="sa_your_api_key"
connectionString={process.env.DATABASE_URL!}
guestForm={{
title: "Welcome!",
subtitle: "Tell us about yourself to get started",
submitButtonText: "Start Chat",
namePlaceholder: "Your name",
emailPlaceholder: "you@company.com",
phonePlaceholder: "+1 (555) 000-0000",
}}
onGuestIdentified={(identity) => {
console.log("Guest:", identity.guestId);
}}
/>💡When externalUserId is provided, the guest form is skipped entirely and the user is considered pre-authenticated.
Guest Identification
Overview
Guest Identification enables customer support for anonymous visitors who don't have an account in your system. When no externalUserId is configured, the SDK activates the guest identification flow — collecting a name, email, and optional phone number before allowing chat messages.
Once identified, the guest's identity is persisted to localStorage (key: syncagent_guest_identity), so returning visitors are recognized automatically without re-entering their details. A deterministic guest_-prefixed identifier is generated from the email address, ensuring the same email always maps to the same guest ID.
ℹ️Guest identification is only relevant in Customer Agent Mode. If you provide an externalUserId, the guest flow is bypassed entirely and the user is considered pre-authenticated.
JS Core — @syncagent/js
The JS core provides all guest identification primitives: validation, identifier generation, and storage management.
import {
SyncAgentClient,
validateGuestForm,
validateName,
validateEmail,
generateGuestIdentifier,
GuestIdentificationRequiredError,
type GuestIdentity,
type GuestFormConfig,
} from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_key",
// No externalUserId — guest flow is active
guestForm: {
title: "Welcome",
subtitle: "Please introduce yourself to get started",
submitButtonText: "Start Chat",
onSubmit: (identity) => console.log("Guest identified:", identity),
},
});
// Check for a returning visitor
const stored = client.getGuestIdentity();
if (stored) {
console.log("Welcome back,", stored.name);
}
// Validate and identify a new guest
const data = { name: "Jane Doe", email: "jane@example.com", phone: "+1234567890" };
const validation = validateGuestForm(data);
if (validation.valid) {
const guestId = generateGuestIdentifier(data.email); // "guest_a1b2c3d4"
const identity: GuestIdentity = {
name: data.name,
email: data.email,
phone: data.phone || null,
guestId,
};
client.setGuestIdentity(identity); // Persists to localStorage
console.log("Guest ID:", identity.guestId);
} else {
console.log("Validation errors:", validation.errors);
// { name?: "Name is required", email?: "Please enter a valid email address" }
}React — useCustomerChat + GuestIdentificationForm
The React SDK provides a useCustomerChat hook with built-in guest state and a pre-built GuestIdentificationForm component.
import { SyncAgentClient } from "@syncagent/js";
import { useCustomerChat, GuestIdentificationForm } from "@syncagent/react";
const client = new SyncAgentClient({ apiKey: "sa_your_key" });
function SupportChat() {
const {
messages,
isIdentified,
guestIdentity,
identifyGuest,
sendMessage,
isLoading,
} = useCustomerChat({
client,
onGuestIdentified: (identity) => {
console.log("Guest identified:", identity.guestId);
},
});
if (!isIdentified) {
return (
<GuestIdentificationForm
onSubmit={identifyGuest}
config={{
title: "Welcome!",
subtitle: "Tell us a bit about yourself",
submitButtonText: "Start Chat",
}}
/>
);
}
return (
<div>
<p>Hello, {guestIdentity?.name}!</p>
{messages.map((msg, i) => (
<div key={i} className={msg.role}>{msg.content}</div>
))}
<button onClick={() => sendMessage("Help me!")} disabled={isLoading}>
Send
</button>
</div>
);
}Vue — useCustomerChat + GuestIdentificationForm
The Vue SDK provides a useCustomerChat composable with reactive guest state and a GuestIdentificationForm SFC.
<script setup lang="ts">
import { ref } from "vue";
import { SyncAgentClient } from "@syncagent/js";
import { useCustomerChat, GuestIdentificationForm } from "@syncagent/vue";
import type { GuestIdentity } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
});
const {
messages,
isIdentified, // Ref<boolean>
guestIdentity, // Ref<GuestIdentity | null>
identifyGuest,
sendMessage,
isLoading,
} = useCustomerChat({
client,
onGuestIdentified: (identity: GuestIdentity) => {
console.log("Guest identified:", identity.guestId);
},
});
const input = ref("");
function send() {
if (!input.value.trim()) return;
sendMessage(input.value);
input.value = "";
}
</script>
<template>
<div>
<!-- Show form when guest is not yet identified -->
<GuestIdentificationForm
v-if="!isIdentified"
:config="{ title: 'Welcome!', subtitle: 'Tell us about yourself' }"
@submit="identifyGuest"
/>
<!-- Show chat when identified -->
<div v-else>
<p>Hello, {{ guestIdentity?.name }}!</p>
<div v-for="(msg, i) in messages" :key="i" :class="msg.role">
{{ msg.content }}
</div>
<input v-model="input" @keydown.enter="send" :disabled="isLoading" />
<button @click="send" :disabled="isLoading">Send</button>
</div>
</div>
</template>Angular — CustomerChatService
The Angular SDK exposes guest identification via signals and observables on the CustomerChatService.
import { Component, OnInit } from "@angular/core";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-support",
providers: [CustomerChatService],
template: `
<!-- Guest form when not identified -->
<form *ngIf="!chat.isIdentified()" (ngSubmit)="identify()">
<h2>Welcome</h2>
<p>Please introduce yourself to get started</p>
<input [(ngModel)]="name" name="name" placeholder="Your name" required />
<input [(ngModel)]="email" name="email" placeholder="Email address" required />
<input [(ngModel)]="phone" name="phone" placeholder="Phone (optional)" />
<button type="submit">Start Chat</button>
</form>
<!-- Chat when identified -->
<div *ngIf="chat.isIdentified()">
<p>Hello, {{ chat.guestIdentity()?.name }}!</p>
<div *ngFor="let msg of chat.messages()" [class]="msg.role">
{{ msg.content }}
</div>
<input [(ngModel)]="input" (keydown.enter)="send()" />
<button (click)="send()" [disabled]="chat.isLoading()">Send</button>
</div>
`,
})
export class SupportComponent implements OnInit {
name = "";
email = "";
phone = "";
input = "";
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
// No externalUserId — guest flow active
onGuestIdentified: (identity) => {
console.log("Guest identified:", identity.guestId);
},
});
// React to identification via observable
this.chat.guestIdentified$.subscribe((identity) => {
console.log("Guest ready:", identity.email);
});
}
identify() {
this.chat.identifyGuest({
name: this.name,
email: this.email,
phone: this.phone || undefined,
});
}
send() {
if (!this.input.trim()) return;
this.chat.sendMessage(this.input);
this.input = "";
}
}Next.js — @syncagent/nextjs
The Next.js package re-exports the React GuestIdentificationForm and all guest utilities. Use the "use client" directive since the form is a client component.
// app/support/guest-chat.tsx
"use client";
import { SyncAgentClient } from "@syncagent/js";
import {
useCustomerChat,
GuestIdentificationForm,
validateGuestForm,
generateGuestIdentifier,
} from "@syncagent/nextjs";
import type { GuestIdentity } from "@syncagent/nextjs";
export function GuestChat({ apiKey }: { apiKey: string }) {
const client = new SyncAgentClient({ apiKey });
const { isIdentified, identifyGuest, messages, sendMessage } = useCustomerChat({ client });
if (!isIdentified) {
return <GuestIdentificationForm onSubmit={identifyGuest} />;
}
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.content}</div>
))}
<button onClick={() => sendMessage("Hello!")}>Send</button>
</div>
);
}⚠️The GuestIdentificationForm component uses browser APIs (localStorage, DOM). Always mark the file with "use client" in Next.js App Router.
API Reference
Guest-related properties and methods available in each framework package:
| Property / Method | JS Core | React | Vue | Angular | Next.js |
|---|---|---|---|---|---|
| isIdentified | — | boolean state | Ref<boolean> | Signal<boolean> | boolean state |
| guestIdentity | getGuestIdentity() | GuestIdentity | null | Ref<GuestIdentity | null> | Signal<GuestIdentity | null> | GuestIdentity | null |
| identifyGuest(data) | setGuestIdentity() | hook method | composable method | service method | hook method |
| isIdentified$ | — | — | — | BehaviorSubject<boolean> | — |
| guestIdentified$ | — | — | — | Subject<GuestIdentity> | — |
| GuestIdentificationForm | — | React component | Vue SFC | — | Re-export from React |
| validateGuestForm(data) | ✓ | re-export | re-export | re-export | re-export |
| generateGuestIdentifier(email) | ✓ | re-export | re-export | re-export | re-export |
| onGuestIdentified | CustomerChatOptions | hook option | composable option | config callback | hook option |
localStorage Persistence & Returning Visitors
When a guest completes identification, their identity is stored in localStorage under the key syncagent_guest_identity. On subsequent visits, the SDK reads this stored identity and automatically sets isIdentified to true — the guest skips the form and goes straight to chat.
The guest identifier is deterministic: the same email always produces the same guest_-prefixed ID (using FNV-1a hashing of the normalized email). This means conversation history is tied to the guest across sessions.
💡If localStorage is unavailable (private browsing, SSR), the SDK falls back to in-memory storage. The guest will need to re-identify on each page load, but the flow still works correctly.
Conversation Flows
Overview
Conversation Flows are scripted, branching conversation paths that guide customers through common support scenarios (order status checks, returns, billing inquiries, etc.). When a customer message matches one of a flow's trigger phrases, the agent enters the flow instead of using the AI fallback pipeline.
If multiple flows match a message, the one with the highest priority value is selected. Flows are composed of interconnected nodes that form a directed graph — each node either presents options, sends a message, or terminates the conversation.
Node Types
Every flow is built from three node types:
Presents the customer with labeled options. Each option routes to a different node. The customer's next message selects one of the options.
Sends a message to the customer and automatically continues to the next node via a single branch. No customer input is required to advance.
Ends the flow. The terminalAction is either resolve (marks the conversation as done) or escalate (hands off to a human agent).
Data Model — IFlowNode Interface
interface IFlowNode {
id: string; // Unique node identifier
type: "decision" | "response" | "terminal";
content: string; // Message text displayed to the customer
options?: { // Only for "decision" nodes
label: string; // Option button text
nextNodeId: string; // Node to navigate to when selected
}[];
terminalAction?: "resolve" | "escalate"; // Only for "terminal" nodes
isEntry?: boolean; // Marks the starting node of the flow
}
interface ConversationFlow {
id: string;
projectId: string;
name: string;
triggerPhrases: string[]; // Phrases that activate this flow
priority: number; // Higher = selected first on conflict
isActive: boolean;
nodes: IFlowNode[];
createdAt: string;
updatedAt: string;
}REST API Endpoints
Manage flows programmatically via the REST API. All endpoints require your project API key in the x-api-key header.
GET /api/v1/flows
List all flows for the project. Supports pagination via limit and offset query parameters.
curl -X GET "https://your-app.com/api/v1/flows?limit=10&offset=0" \
-H "x-api-key: sa_your_key"{
"flows": [
{
"id": "flow_abc123",
"name": "Order Status Check",
"triggerPhrases": ["order status", "where is my order", "track order"],
"priority": 10,
"isActive": true,
"nodes": [...],
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z"
}
],
"total": 1,
"limit": 10,
"offset": 0
}POST /api/v1/flows
Create a new conversation flow.
curl -X POST "https://your-app.com/api/v1/flows" \
-H "x-api-key: sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Order Status Check",
"triggerPhrases": ["order status", "where is my order", "track order"],
"priority": 10,
"nodes": [
{
"id": "node_1",
"type": "decision",
"content": "I can help you check your order status. What would you like to do?",
"options": [
{ "label": "Track my order", "nextNodeId": "node_2" },
{ "label": "Report a problem", "nextNodeId": "node_3" }
],
"isEntry": true
},
{
"id": "node_2",
"type": "response",
"content": "Please provide your order number and I will look it up for you.",
"options": [{ "label": "", "nextNodeId": "node_4" }]
},
{
"id": "node_3",
"type": "terminal",
"content": "I will connect you with a support specialist who can help resolve your issue.",
"terminalAction": "escalate"
},
{
"id": "node_4",
"type": "terminal",
"content": "Thank you! Your order issue has been noted and we will follow up via email.",
"terminalAction": "resolve"
}
]
}'{
"id": "flow_abc123",
"name": "Order Status Check",
"triggerPhrases": ["order status", "where is my order", "track order"],
"priority": 10,
"isActive": true,
"nodes": [...],
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z"
}PUT /api/v1/flows/:flowId
Update an existing flow. Send only the fields you want to change.
curl -X PUT "https://your-app.com/api/v1/flows/flow_abc123" \
-H "x-api-key: sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"priority": 20,
"triggerPhrases": ["order status", "where is my order", "track order", "check order"]
}'{
"id": "flow_abc123",
"name": "Order Status Check",
"triggerPhrases": ["order status", "where is my order", "track order", "check order"],
"priority": 20,
"isActive": true,
"nodes": [...],
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-20T14:30:00Z"
}DELETE /api/v1/flows/:flowId
Delete a flow permanently.
curl -X DELETE "https://your-app.com/api/v1/flows/flow_abc123" \
-H "x-api-key: sa_your_key"{
"success": true,
"message": "Flow deleted successfully"
}Validation Rules
When creating or updating a flow, the API enforces these validation rules:
| Rule | Description |
|---|---|
| At least one trigger phrase | The flow must have at least one non-empty string in triggerPhrases |
| At least one decision node | The flow must contain at least one node with type "decision" |
| At least one terminal node | The flow must contain at least one node with type "terminal" |
| No dead-end non-terminal nodes | Every decision and response node must have at least one outgoing branch (options with a valid nextNodeId) |
| No circular references | The node graph must be acyclic — following nextNodeId links must eventually reach a terminal node |
⚠️If validation fails, the API returns a 400 response with a descriptive error message indicating which rule was violated.
Runtime Behavior
When a customer sends a message via customerChat(), the agent checks if the message matches any active flow's trigger phrases. If a match is found, the agent enters the flow and begins at the entry node.
While a flow is active, the customerChat response includes two additional fields:
| Prop | Type | Default | Description |
|---|---|---|---|
| flowActive | boolean | false | true when the customer is currently inside a conversation flow |
| flowSession | { flowId: string; currentNodeId: string } | undefined | Identifies which flow and which node the customer is currently on |
For decision nodes, the agent presents the options as the response. The customer's next message is matched against the option labels to determine which branch to follow. For response nodes, the message is sent and the flow automatically advances to the next node. The flow continues until a terminal node is reached, at which point the flow session ends and the specified action (resolve or escalate) is executed.
const result = await client.customerChat("Where is my order?");
if (result.flowActive) {
console.log("Flow in progress:", result.flowSession?.flowId);
console.log("Current node:", result.flowSession?.currentNodeId);
// The response contains the current node's content/options
console.log(result.response);
}
// When the customer selects an option:
const next = await client.customerChat("Track my order", {
conversationId: result.conversationId,
});
// The flow advances to the next node based on the selected optionComplete Example — Order Status Check Flow
Here's a complete example creating a multi-node flow with decision, response, and terminal nodes:
// Create an "Order Status Check" flow via the REST API
const flow = await fetch("https://your-app.com/api/v1/flows", {
method: "POST",
headers: {
"x-api-key": "sa_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Order Status Check",
triggerPhrases: ["order status", "where is my order", "track my order"],
priority: 10,
nodes: [
{
id: "entry",
type: "decision",
content: "I'd be happy to help with your order! What would you like to do?",
options: [
{ label: "Track my order", nextNodeId: "track_info" },
{ label: "Report a missing item", nextNodeId: "missing_item" },
{ label: "Cancel my order", nextNodeId: "cancel_confirm" }
],
isEntry: true
},
{
id: "track_info",
type: "response",
content: "I've found your most recent order #12345. It shipped on Jan 15 and is currently in transit. Expected delivery: Jan 18.",
options: [{ label: "", nextNodeId: "track_done" }]
},
{
id: "track_done",
type: "terminal",
content: "Is there anything else I can help you with? If not, I hope your package arrives soon!",
terminalAction: "resolve"
},
{
id: "missing_item",
type: "decision",
content: "I'm sorry to hear about a missing item. Was the package marked as delivered?",
options: [
{ label: "Yes, marked delivered but not received", nextNodeId: "escalate_missing" },
{ label: "No, it's still in transit", nextNodeId: "track_info" }
]
},
{
id: "escalate_missing",
type: "terminal",
content: "I understand how frustrating that is. Let me connect you with our shipping team who can investigate this further.",
terminalAction: "escalate"
},
{
id: "cancel_confirm",
type: "decision",
content: "Are you sure you want to cancel your order? This cannot be undone if the item has already shipped.",
options: [
{ label: "Yes, cancel it", nextNodeId: "cancel_done" },
{ label: "No, keep my order", nextNodeId: "cancel_abort" }
]
},
{
id: "cancel_done",
type: "terminal",
content: "Your order has been cancelled. A refund will be processed within 3-5 business days.",
terminalAction: "resolve"
},
{
id: "cancel_abort",
type: "terminal",
content: "No problem! Your order remains active. Is there anything else I can help with?",
terminalAction: "resolve"
}
]
}),
});
console.log("Flow created:", await flow.json());💡Use the dashboard UI to visually build and test flows before deploying them via the API. The API is ideal for programmatic management, CI/CD pipelines, or bulk operations.
Unified Dual Mode
Overview
The unified dual mode pattern lets you use both database queries (chat()) and customer support (customerChat()) from a single client instance. Simply pass externalUserId to the constructor — both modes are enabled automatically without needing createDual().
💡This is the recommended approach for apps that serve both admin/internal users (database queries) and end-users (customer support) from a single configuration.
JavaScript
Pass externalUserId to the SyncAgentClient constructor to enable both chat() and customerChat() on the same instance.
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: "postgresql://user:pass@host:5432/mydb",
externalUserId: "customer_123", // enables both modes
});
// Database queries (admin)
const result = await client.chat([
{ role: "user", content: "Show all overdue invoices" }
]);
// Customer support (end-user)
const reply = await client.customerChat("How do I reset my password?");
console.log(reply.response);
console.log(reply.conversationId);ℹ️When externalUserId is provided, customerMode is automatically enabled. No need to set customerMode: true explicitly.
React — useDualChat
The useDualChat() hook provides access to both db and support namespaces from a single hook call within a SyncAgentProvider.
import { SyncAgentProvider, useDualChat } from "@syncagent/react";
function DualChatUI() {
const { db, support } = useDualChat({
context: { page: "dashboard" },
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Resolved:", id),
});
return (
<div>
{/* Database agent */}
<div>
{db.messages.map((msg, i) => (
<div key={i}>{msg.content}</div>
))}
<button onClick={() => db.sendMessage("Show all users")}>
Query DB
</button>
</div>
{/* Customer support */}
<div>
{support.messages.map((msg, i) => (
<div key={i}>{msg.content}</div>
))}
<button onClick={() => support.sendMessage("I need help")}>
Ask Support
</button>
</div>
</div>
);
}
export default function App() {
return (
<SyncAgentProvider config={{
apiKey: "sa_your_key",
connectionString: "postgresql://user:pass@host:5432/mydb",
externalUserId: "customer_123",
}}>
<DualChatUI />
</SyncAgentProvider>
);
}Vue — useDualChat
The useDualChat() composable returns reactive refs for both database and support namespaces.
<script setup lang="ts">
import { SyncAgentClient } from "@syncagent/js";
import { useDualChat } from "@syncagent/vue";
const client = new SyncAgentClient({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
connectionString: import.meta.env.VITE_DATABASE_URL,
externalUserId: "customer_123",
});
const { db, support } = useDualChat({
client,
onEscalated: () => console.log("Escalated"),
onResolved: (id) => console.log("Resolved:", id),
});
</script>
<template>
<div>
<!-- Database queries -->
<div v-for="(msg, i) in db.messages" :key="'db'+i">{{ msg.content }}</div>
<button @click="db.sendMessage('Show all orders')">Query DB</button>
<!-- Customer support -->
<div v-for="(msg, i) in support.messages" :key="'s'+i">{{ msg.content }}</div>
<button @click="support.sendMessage('I need help')">Ask Support</button>
</div>
</template>Angular — DualChatService
The DualChatService provides Angular Signals and RxJS Observables for both database and support chat state from a single injectable service.
import { Component, OnInit } from "@angular/core";
import { DualChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-dual-chat",
providers: [DualChatService],
template: `
<!-- Database agent -->
<div *ngFor="let msg of dual.dbMessages()">{{ msg.content }}</div>
<button (click)="dual.sendDbMessage('Show all users')">Query DB</button>
<!-- Customer support -->
<div *ngFor="let msg of dual.supportMessages()">{{ msg.content }}</div>
<button (click)="dual.sendSupportMessage('I need help')">Ask Support</button>
`,
})
export class DualChatComponent implements OnInit {
constructor(public dual: DualChatService) {}
ngOnInit() {
this.dual.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123",
onEscalated: () => console.log("Escalated"),
onResolved: (id) => console.log("Resolved:", id),
});
}
}Next.js — createDualServerConfig
The createDualServerConfig() helper creates unified dual-mode configs from Server Components, keeping secrets server-side.
// app/dashboard/page.tsx — Server Component
import { createDualServerConfig } from "@syncagent/nextjs/server";
import { getServerSession } from "next-auth";
import { AdminPanel } from "./admin-panel";
import { SupportWidget } from "./support-widget";
export default async function DashboardPage() {
const session = await getServerSession();
const { db, support } = createDualServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
connectionString: process.env.DATABASE_URL!,
externalUserId: session.user.id,
});
return (
<div>
<AdminPanel config={db} />
<SupportWidget config={support} />
</div>
);
}// app/dashboard/support-widget.tsx — "use client"
"use client";
import { useDualChat } from "@syncagent/nextjs";
export function SupportWidget({ config }: { config: any }) {
const { db, support } = useDualChat();
return (
<div>
{support.messages.map((msg, i) => (
<div key={i}>{msg.content}</div>
))}
<button onClick={() => support.sendMessage("Help me")}>Send</button>
</div>
);
}ℹ️useDualChat, DualChatReturn, and UseDualChatOptions are re-exported from @syncagent/nextjs for client-side use.
Custom Tools
Give the agent capabilities beyond your database. Tools run entirely in your app — SyncAgent only sees the schema and the result you return, never your implementation or secrets.
🔒Your execute function runs in your app, not on SyncAgent servers. API keys, secrets, and business logic never leave your environment.
How it works
- You define tools with a name, description, parameters, and an execute function
- The SDK sends only the tool schema to SyncAgent (execute stays in your code)
- The AI decides when to call a tool based on the user's message
- The SDK runs your function locally and sends the result back to the AI
- The AI uses the result to continue the conversation
Basic example — send email
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
tools: {
sendEmail: {
description: "Send an email to a user",
inputSchema: {
to: { type: "string", description: "Recipient email address" },
subject: { type: "string", description: "Email subject line" },
body: { type: "string", description: "Email body (plain text)" },
},
execute: async ({ to, subject, body }) => {
// Your email logic — runs in YOUR app
await resend.emails.send({ from: "noreply@yourapp.com", to, subject, text: body });
return { sent: true, to };
},
},
},
}}
/>
// Now users can say: "Email all users whose subscription expired"
// The agent will query expired users, then call sendEmail for each oneMultiple tools — real-world example
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
tools: {
sendSlackAlert: {
description: "Post an alert to a Slack channel",
inputSchema: {
channel: { type: "string", description: "Channel name e.g. #alerts" },
message: { type: "string", description: "Alert message" },
severity: { type: "string", description: "low | medium | high", enum: ["low","medium","high"] },
},
execute: async ({ channel, message, severity }) => {
await slack.chat.postMessage({ channel, text: `[${severity.toUpperCase()}] ${message}` });
return { posted: true };
},
},
createStripeInvoice: {
description: "Create a Stripe invoice for a customer",
inputSchema: {
customerId: { type: "string", description: "Stripe customer ID" },
amount: { type: "number", description: "Amount in cents" },
description:{ type: "string", description: "Invoice description" },
},
execute: async ({ customerId, amount, description }) => {
const invoice = await stripe.invoices.create({
customer: customerId,
description,
collection_method: "send_invoice",
days_until_due: 30,
});
await stripe.invoiceItems.create({ customer: customerId, amount, invoice: invoice.id });
await stripe.invoices.finalizeInvoice(invoice.id);
return { invoiceId: invoice.id, status: invoice.status };
},
},
generatePdfReport: {
description: "Generate a PDF report and return the download URL",
inputSchema: {
title: { type: "string", description: "Report title" },
content: { type: "string", description: "Report content as markdown" },
},
execute: async ({ title, content }) => {
const url = await pdfService.generate({ title, markdown: content });
return { url, generatedAt: new Date().toISOString() };
},
},
},
});
// "Find all overdue invoices, create Stripe invoices for them, and post a summary to #billing"
// The agent will: query DB → create invoices → post Slack message → report backTool definition reference
| Prop | Type | Default | Description |
|---|---|---|---|
| description | string | required | What the tool does — the AI reads this to decide when to call it |
| inputSchema | Record<string, ToolParameter> | required | Parameters the tool accepts |
| inputSchema.*.type | "string"|"number"|"boolean"|"object"|"array" | required | Parameter type |
| inputSchema.*.description | string | — | Helps the AI know what value to pass |
| inputSchema.*.required | boolean | true | Set false for optional parameters |
| inputSchema.*.enum | string[] | — | Restrict to specific values |
| execute | (args) => any | Promise<any> | required | Your function — runs in your app, not on SyncAgent servers |
Tools-Only Mode
When you want the agent to only use your custom tools — with no database access at all — set toolsOnly: true. This is useful when you want to build an AI assistant powered by your own APIs, webhooks, or business logic.
💡In tools-only mode, connectionString is not required. No database connection is made, no schema is discovered, and no built-in DB tools are available.
React example
import { SyncAgentChat } from "@syncagent/react";
<SyncAgentChat
config={{
apiKey: "sa_your_key",
toolsOnly: true,
tools: {
searchProducts: {
description: "Search products by name or category",
inputSchema: {
query: { type: "string", description: "Search query" },
},
execute: async ({ query }) => {
const res = await fetch(`/api/products?q=${query}`);
return res.json();
},
},
createOrder: {
description: "Place an order for a product",
inputSchema: {
productId: { type: "string", description: "Product ID" },
quantity: { type: "number", description: "Quantity" },
},
execute: async ({ productId, quantity }) => {
const res = await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId, quantity }),
});
return res.json();
},
},
},
}}
/>JS SDK example
import { SyncAgentClient } from "@syncagent/js";
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
toolsOnly: true,
tools: {
getWeather: {
description: "Get current weather for a city",
inputSchema: {
city: { type: "string", description: "City name" },
},
execute: async ({ city }) => {
const res = await fetch(`https://api.weather.com/v1/${city}`);
return res.json();
},
},
},
});
const result = await agent.chat([
{ role: "user", content: "What's the weather in Accra?" }
]);
console.log(result.text);REST API
curl -X POST https://syncagent.dev/api/v1/chat \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"toolsOnly": true,
"clientTools": {
"searchProducts": {
"description": "Search products by name",
"inputSchema": {
"query": { "type": "string", "description": "Search query" }
}
}
},
"messages": [{
"id": "1",
"role": "user",
"parts": [{ "type": "text", "text": "Find running shoes" }]
}]
}'When to use tools-only mode
- You want an AI assistant that calls your own REST APIs instead of querying a database directly
- Your data comes from third-party services (Stripe, Shopify, Salesforce, etc.)
- You need the agent to trigger actions (send emails, create tickets, deploy code) without any DB access
- You want full control over every tool the agent can use
How it works
- You pass
toolsOnly: trueand your customtools - SyncAgent skips database connection and schema discovery entirely
- The AI agent receives a system prompt listing only your custom tools
- When the user asks a question, the agent decides which of your tools to call
- Your
executefunction runs in your app, and the result is sent back to the AI
Multi-tenancy
Building a SaaS app where multiple organizations share the same database? Use the filterprop to scope every agent operation to the current user's organization. Enforced server-side — the agent cannot query outside this scope.
🔒The filter is applied server-side to every query, count, aggregation, insert, update, and delete. The agent is told about the scope in its system prompt and cannot override it.
Basic multi-tenant setup
// In your app, after the user logs in:
import { SyncAgentChat } from "@syncagent/react";
export default function Dashboard({ currentUser }) {
return (
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY,
connectionString: process.env.DATABASE_URL,
// Scope ALL queries to this organization
filter: { organizationId: currentUser.orgId },
}}
/>
);
}
// Every operation is now scoped:
// "Show all orders" → db.orders.find({ organizationId: "org_123" })
// "Count users" → db.users.countDocuments({ organizationId: "org_123" })
// "Add a product" → db.products.insertOne({ ...doc, organizationId: "org_123" })Per-user operation restrictions
Use operations to give different users different access levels. The client can only restrict further — never grant more than the project dashboard allows.
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY,
connectionString: process.env.DATABASE_URL,
filter: { organizationId: currentUser.orgId },
// Admins get full access, regular users read-only
operations: currentUser.role === "admin"
? ["read", "create", "update", "delete"]
: ["read"],
}}
/>Common filter patterns
// By organization ID (most common)
filter: { organizationId: org.id }
// By tenant slug
filter: { tenant: "acme-corp" }
// Personal data — scope to current user
filter: { userId: currentUser.id }
// Multiple conditions
filter: { orgId: org.id, deleted: false }
// SQL databases (same syntax)
filter: { tenant_id: tenant.id }Operations & Permissions
Control what the agent can do. Configure the maximum allowed operations in your project dashboard, then optionally restrict further per-session using the operations prop.
| Operation | Tools enabled | Behavior |
|---|---|---|
| read | query_documents, count_documents, aggregate_documents | Executes immediately, no confirmation needed |
| create | create_document | Agent states what it will insert, then executes |
| update | update_document | Agent states what it will change, then executes |
| delete | delete_document | Always asks for explicit confirmation first. Empty filter blocked. |
⚠️Deletes always require explicit user confirmation and are blocked with an empty filter. The agent will never delete all records in a collection.
Allowed & blocked collections
In your project Settings tab, you can restrict which collections the agent can see:
Allowed Collections: users, orders, products
(blank = all collections)
Blocked Collections: admin_logs, secrets, audit_trail
(always denied, overrides allowed list)Context & Auto Page Detection
The SDK automatically detects the current page from window.locationon every message — zero config needed. The agent knows what page the user is on, what record they're viewing, and any relevant query params.
💡Auto page detection works in all frameworks: React, Next.js, Vue, Angular, and vanilla JS. It's SSR-safe — returns empty context on the server.
What gets auto-detected
URL: /dashboard/orders/ord_123?tab=details&status=active
Auto-detected context:
currentPage: "orders" ← last meaningful path segment
currentPath: "/dashboard/orders/ord_123"
currentRecordId: "ord_123" ← detected as ID (ObjectId/UUID/numeric)
param_tab: "details" ← useful query params
param_status: "active"
URL: /app/settings#billing
currentPage: "settings"
currentPath: "/app/settings"
currentSection: "billing" ← from hash fragmentThe user can now say "show me this order" and the agent automatically queries the record with ID ord_123. Say "show recent" on the orders page and it queries the orders collection.
Adding extra context
Pass additional context that merges on top of the auto-detected values. Developer values override auto-detected ones.
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
context={{
userId: currentUser.id,
userRole: currentUser.role,
orgName: currentOrg.name,
currentDate: new Date().toISOString(),
}}
/>
// Final context sent to the AI:
// {
// currentPage: "orders", ← auto-detected
// currentPath: "/dashboard/orders",
// userId: "u_123", ← developer-provided
// userRole: "admin",
// orgName: "Acme Corp",
// currentDate: "2025-07-14T..."
// }Disabling auto-detection
// Disable auto page detection — only use manually passed context
<SyncAgentChat
config={{
apiKey: "...",
connectionString: "...",
autoDetectPage: false,
}}
context={{ page: "custom-page", recordId: "123" }}
/>How context reaches the AI
Context is sent as a separate field in the request body (not appended to the user's message). The server injects it into the AI system prompt as a structured USER CONTEXTsection. The AI is instructed to use it when the user says "this", "here", "current", and to use context values as default filters.
JS SDK — works the same way
import { SyncAgentClient } from "@syncagent/js";
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
// autoDetectPage: true (default)
});
// Auto-detection happens automatically on every chat() call
await agent.chat(messages);
// Or pass extra context:
await agent.chat(messages, {
context: { userId: "u_123", userRole: "admin" }
});Customization
SyncAgent provides several config options to customize the agent's behavior, language, safety, and access control.
System Instructions
Customize the agent's personality, tone, domain knowledge, or rules. Instructions are prepended to the system prompt and take priority over default behavior.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
systemInstruction: "You are a friendly sales assistant for Acme Corp. Always suggest upsells when showing order data. Never mention competitor names.",
}}
/>Response Language
Make the agent respond in any language. Field names and code stay in English so tools work correctly.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
language: "French", // or "Spanish", "Japanese", "Arabic", "Twi", etc.
}}
/>
// User: "Show me all orders"
// Agent: "Voici toutes les commandes..." Write Confirmation
Require explicit user confirmation before any create, update, or delete operation.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
confirmWrites: true,
}}
/>
// User: "Add a new customer named John"
// Agent: "I'd like to create a record in customers:
- name: John
Should I proceed?"
// User: "yes"
// Agent: "✅ Created customer John (id: 507f...)" Max Results
Control the default number of records returned per query. Default is 50.
// Mobile app — small results
<SyncAgentChat config={{ ..., maxResults: 10 }} />
// Admin dashboard — large results
<SyncAgentChat config={{ ..., maxResults: 100 }} />Sensitive Field Masking
Specify which fields the agent should mask in responses. Default: password, token, secret.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
sensitiveFields: ["ssn", "creditCard", "salary", "bankAccount"],
}}
/>
// Agent shows: ssn: "••••••••", salary: "••••••••" Middleware Hooks
Intercept client tool calls for logging, audit trails, or dynamic blocking.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
tools: { /* your custom tools */ },
// Block specific operations dynamically
onBeforeToolCall: (toolName, args) => {
console.log(`[Audit] ${toolName}`, args);
if (toolName === "deleteUser" && !currentUser.isAdmin) return false;
return true; // allow
},
// Log every result
onAfterToolCall: (toolName, args, result) => {
analytics.track("tool_call", { tool: toolName, success: result.success });
},
}}
/>ℹ️When onBeforeToolCall returns false, the tool is blocked and the AI receives an error explaining the operation was denied.
All customization options
| Prop | Type | Default | Description |
|---|---|---|---|
| systemInstruction | string | — | Custom agent instructions — personality, tone, rules |
| language | string | — | Response language (e.g. "French", "Spanish") |
| confirmWrites | boolean | false | Ask for confirmation before create/update/delete |
| maxResults | number | 50 | Default max records per query |
| sensitiveFields | string[] | ["password","token","secret"] | Fields to mask in responses |
| onBeforeToolCall | (name, args) => boolean | — | Called before each client tool. Return false to block. |
| onAfterToolCall | (name, args, result) => void | — | Called after each client tool executes |
Conversation Persistence
Pass persistKey to save chat history to localStorage. Survives page refresh. The "New" button in the header clears it.
// Use a unique key per user or project
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
persistKey={currentUser.id} // or project.id, or "global"
/>
// History is saved to localStorage under: sa_chat_{persistKey}
// Conversation history sidebar (🕐 button) shows past conversations💡Use the user's ID as the persist key so each user has their own conversation history. Use a project ID if you want all users of a project to share history.
Vanilla JS Widget
No npm required. Drop a script tag into any HTML page — works with plain HTML, PHP, Ruby on Rails, Django, or any server-rendered app.
<script src="https://syncagentdev.vercel.app/api/v1/widget"></script>
<script>
SyncAgent.init({
apiKey: "sa_your_key",
connectionString: "your_database_url",
// Appearance
position: "right", // "right" or "left"
accentColor: "#10b981", // brand color
title: "AI Assistant",
subtitle: "Ask about your data",
// Behavior
persistKey: "my-app", // localStorage persistence
open: false, // start open?
});
// Programmatic control
SyncAgent.open();
SyncAgent.close();
SyncAgent.toggle();
SyncAgent.clearHistory(); // clear persisted conversation
</script>Features included in the widget
- Markdown rendering (tables, code blocks, bold, lists)
- Live status indicator (connecting, querying, thinking)
- 👍/👎 reaction buttons on AI responses
- Copy button on AI responses
- Conversation persistence via localStorage
- New conversation button
- Stop button to abort streaming
- Dark mode support (prefers-color-scheme)
- Mobile responsive
REST API
SyncAgent is a standard REST API. Use it from any language — Python, Go, Ruby, PHP, Java, C#, Rust, or anything that can make HTTP requests.
ℹ️All API endpoints require Authorization: Bearer sa_your_key header.
POST /api/v1/chat — Send a message
Returns a plain text streaming response. Read it line by line.
curl -X POST https://syncagent.dev/api/v1/chat \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"connectionString": "mongodb+srv://user:pass@cluster/db",
"messages": [{
"id": "1",
"role": "user",
"parts": [{ "type": "text", "text": "Show all active users" }]
}]
}'import requests
res = requests.post(
"https://syncagent.dev/api/v1/chat",
headers={"Authorization": "Bearer sa_your_key"},
json={
"connectionString": "mongodb+srv://user:pass@cluster/db",
"messages": [{
"id": "1", "role": "user",
"parts": [{"type": "text", "text": "Show all active users"}]
}]
},
stream=True
)
for chunk in res.iter_content(decode_unicode=True):
print(chunk, end="")body, _ := json.Marshal(map[string]any{
"connectionString": "mongodb+srv://user:pass@cluster/db",
"messages": []map[string]any{{
"id": "1", "role": "user",
"parts": []map[string]string{{"type": "text", "text": "Show all active users"}},
}},
})
req, _ := http.NewRequest("POST", "https://syncagent.dev/api/v1/chat", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer sa_your_key")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
io.Copy(os.Stdout, res.Body)Request body parameters
| Prop | Type | Default | Description |
|---|---|---|---|
| connectionString | string | required* | Your database connection string. *Optional when toolsOnly is true. |
| messages | Message[] | required | Array of messages with id, role, and parts |
| filter | Record<string,any> | — | Mandatory query filter for multi-tenancy |
| operations | string[] | — | Restrict operations for this request |
| clientTools | Record<string,ToolDef> | — | Custom tool schemas (execute runs client-side) |
| toolsOnly | boolean | — | When true, disables all DB tools — agent only uses clientTools |
| context | Record<string,any> | — | Extra context (page, user, etc.) — injected into system prompt, not message text |
| systemInstruction | string | — | Custom agent instructions — personality, tone, rules. Prepended to system prompt. |
| confirmWrites | boolean | — | When true, agent asks for user confirmation before create/update/delete |
| language | string | — | Language the agent responds in (e.g. "French", "Spanish") |
| maxResults | number | — | Default max records per query (default 50) |
| sensitiveFields | string[] | — | Fields to mask in responses (default: password, token, secret) |
POST /api/v1/schema — Discover schema
curl -X POST https://syncagent.dev/api/v1/schema \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{ "connectionString": "mongodb+srv://user:pass@cluster/db" }'
# Response:
# {
# "success": true,
# "dbType": "mongodb",
# "collections": [
# {
# "name": "users",
# "documentCount": 1247,
# "fields": [
# { "name": "_id", "type": "ObjectId" },
# { "name": "email", "type": "string", "sample": "user@example.com" },
# { "name": "createdAt", "type": "Date" }
# ]
# }
# ]
# }Error responses
| Prop | Type | Default | Description |
|---|---|---|---|
| 401 | Unauthorized | — | Missing or invalid API key |
| 403 | Forbidden | — | Collection limit reached for your plan |
| 429 | Too Many Requests | — | Monthly request limit reached or rate limit exceeded |
| 504 | Gateway Timeout | — | Database or AI took too long (90s timeout) |
| 500 | Internal Server Error | — | Agent error — check error.message for details |
POST /api/v1/customer-chat — Customer agent message
Send a message through the customer agent pipeline. Routes through persona, flows, knowledge base, escalation checks, and AI fallback. Requires customer agent mode to be enabled on the project.
ℹ️Authentication: Authorization: Bearer sa_your_api_key — use your project API key.
Request body
| Prop | Type | Default | Description |
|---|---|---|---|
| message | string | required | The customer's message text |
| connectionString | string | required | Database connection string for data-aware responses |
| conversationId | string | — | Existing conversation ID to continue a thread |
| externalUserId | string | — | Customer identifier for multi-tenant scoping |
| filter | Record<string, any> | — | Mandatory query filter for multi-tenant data scoping |
| metadata | Record<string, any> | — | Additional metadata stored on the conversation |
Response body
| Prop | Type | Default | Description |
|---|---|---|---|
| conversationId | string | — | Unique conversation identifier |
| response | string | — | AI assistant response text |
| escalated | boolean | — | Whether conversation was escalated to a human agent |
| autoReply | boolean | — | Whether response was an auto-reply (e.g. outside business hours) |
| flowActive | boolean | — | Whether a conversation flow is currently active |
| resolved | boolean | — | Whether the conversation has been resolved |
| welcomeMessage | string? | — | Welcome message (first interaction only) |
| sources | any[]? | — | Knowledge base sources used for the response |
| flowSession | { flowId, currentNodeId }? | — | Active flow session state |
curl example
curl -X POST https://syncagent.dev/api/v1/customer-chat \
-H "Authorization: Bearer sa_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"message": "Hi, I need help with my order",
"connectionString": "mongodb+srv://user:pass@cluster/db",
"externalUserId": "customer_123",
"metadata": { "page": "support" }
}'
# Response:
# {
# "conversationId": "conv_abc123",
# "response": "Hello! I'd be happy to help with your order. Could you provide your order number?",
# "escalated": false,
# "autoReply": false,
# "flowActive": false,
# "resolved": false,
# "welcomeMessage": "Welcome to support! How can I help you today?"
# }fetch example
const response = await fetch("https://syncagent.dev/api/v1/customer-chat", {
method: "POST",
headers: {
"Authorization": "Bearer sa_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Hi, I need help with my order",
connectionString: "mongodb+srv://user:pass@cluster/db",
externalUserId: "customer_123",
conversationId: "conv_abc123", // optional: continue existing conversation
metadata: { page: "support" },
}),
});
const data = await response.json();
// { conversationId, response, escalated, autoReply, flowActive, resolved, ... }POST /api/v1/conversations/[id]/rate — Rate a conversation
Submit a satisfaction rating for a resolved conversation. Rating must be an integer between 1 and 5. The conversation must be in a resolved or closed state.
ℹ️Authentication: Authorization: Bearer sa_your_api_key — use your project API key.
Path parameters
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | required | The conversation ID to rate |
Request body
| Prop | Type | Default | Description |
|---|---|---|---|
| rating | integer (1-5) | required | Satisfaction rating — 1 (poor) to 5 (excellent) |
Response body
| Prop | Type | Default | Description |
|---|---|---|---|
| success | boolean | — | Whether the rating was saved successfully |
| rating | number | — | The submitted rating value |
curl example
curl -X POST https://syncagent.dev/api/v1/conversations/conv_abc123/rate \
-H "Authorization: Bearer sa_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "rating": 5 }'
# Response:
# { "success": true, "rating": 5 }fetch example
const conversationId = "conv_abc123";
const response = await fetch(
`https://syncagent.dev/api/v1/conversations/${conversationId}/rate`,
{
method: "POST",
headers: {
"Authorization": "Bearer sa_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ rating: 5 }),
}
);
const data = await response.json();
// { success: true, rating: 5 }⚠️Rating is only allowed on conversations with status "resolved" or "closed". Attempting to rate an open conversation returns a 400 error.
Conversations API
Store and retrieve conversation history server-side. Useful for multi-device sync, audit trails, and analytics. All endpoints require Authorization: Bearer sa_your_key.
POST /api/v1/conversations — Save a conversation
curl -X POST https://syncagentdev.vercel.app/api/v1/conversations \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"title": "Order analysis",
"messages": [
{ "role": "user", "content": "Show all orders over $500" },
{ "role": "assistant", "content": "Here are 12 orders..." }
],
"metadata": { "page": "orders" }
}'
# Response:
# { "success": true, "conversation": { "id": "conv_abc", "title": "Order analysis", "messageCount": 2 } }GET /api/v1/conversations — List conversations
curl https://syncagentdev.vercel.app/api/v1/conversations?userId=user_123&limit=20 \
-H "Authorization: Bearer sa_your_key"
# Response:
# { "success": true, "conversations": [...], "total": 42, "limit": 20, "offset": 0 }GET /api/v1/conversations/:id — Get full conversation
curl https://syncagentdev.vercel.app/api/v1/conversations/conv_abc \
-H "Authorization: Bearer sa_your_key"
# Response:
# { "success": true, "conversation": { "id": "conv_abc", "messages": [...], "metadata": {...} } }DELETE /api/v1/conversations — Delete a conversation
curl -X DELETE https://syncagentdev.vercel.app/api/v1/conversations \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{ "id": "conv_abc" }'Update an existing conversation
Pass the id field in the POST body to update instead of create:
curl -X POST https://syncagentdev.vercel.app/api/v1/conversations \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"id": "conv_abc",
"messages": [
{ "role": "user", "content": "Show all orders over $500" },
{ "role": "assistant", "content": "Here are 12 orders..." },
{ "role": "user", "content": "Now filter by this month" },
{ "role": "assistant", "content": "Found 3 orders this month..." }
]
}'💡Conversations auto-expire after 30 days. Use this API to persist important conversations to your own database if needed.
API Key Scoping
Create API keys with restricted permissions. Useful for giving different access levels to different parts of your app (e.g., a read-only key for a public widget vs a full-access key for admin dashboards).
How it works
When generating a new API key in the dashboard, you can optionally restrict which operations that key allows. The key can only restrict further — it can never grant more than the project's configured operations.
// Key with read-only access (for public-facing widget)
// Generated in dashboard with operations: ["read"]
// Key with full access (for admin panel)
// Generated in dashboard with operations: ["read", "create", "update", "delete"]
// The SDK can further restrict at runtime:
const agent = new SyncAgentClient({
apiKey: "sa_readonly_key", // this key only allows "read"
connectionString: process.env.DATABASE_URL,
operations: ["read", "create"], // "create" is ignored — key doesn't allow it
});
// Effective operations: ["read"]Precedence
Operations are intersected at three levels:
- Project level — configured in dashboard Settings tab
- API key level — set when generating the key (optional)
- Request level — passed via SDK
operationsconfig
Each level can only restrict further, never expand access.
Webhooks & Events
Configure webhook endpoints to receive real-time notifications when events occur in your SyncAgent project.
Supported Events
| Event | Trigger | Payload |
|---|---|---|
| conversation.created | New conversation started | { conversationId, externalUserId, createdAt } |
| conversation.escalated | AI escalated to human | { conversationId, externalUserId, reason, messages } |
| conversation.resolved | Conversation marked resolved | { conversationId, externalUserId, resolvedAt, rating } |
| conversation.rated | Customer submitted rating | { conversationId, externalUserId, rating } |
| guest.identified | Guest completed identification | { guestId, name, email, projectId } |
Configuration
- Go to Dashboard → Settings → Webhooks → Add Endpoint
- Provide your HTTPS endpoint URL
- Select which events to subscribe to
- Copy the signing secret for verification
Verifying Webhook Signatures
All webhook payloads are signed with HMAC-SHA256. Verify the signature before processing.
import crypto from "crypto";
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post("/webhooks/syncagent", (req, res) => {
const signature = req.headers["x-syncagent-signature"] as string;
const body = JSON.stringify(req.body);
if (!verifyWebhook(body, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const { event, data } = req.body;
console.log(`Event: ${event}`, data);
res.json({ received: true });
});Retry Policy
- Failed deliveries are retried 3 times with exponential backoff (1min, 5min, 30min)
- After 3 failures, the endpoint is marked as failing
- Endpoints with 10 consecutive failures are automatically disabled
⚠️Make sure your endpoint responds with a 2xx status within 10 seconds. Timeouts count as failures.
Security
Connection string safety
Your database connection string is never stored on SyncAgent servers. It is passed from your app to our API at runtime, used to process the current request, and immediately discarded.
⚠️Never put your connection string in client-side code (browser JavaScript). Use environment variables and pass it from your server.
In Next.js, use a server component or API route:
// ✅ CORRECT — server component passes connection string
// app/dashboard/page.tsx (server component)
import { SyncAgentChat } from "@syncagent/react";
export default function DashboardPage() {
return (
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY, // public key is fine
connectionString: process.env.DATABASE_URL, // server-only env var
}}
/>
);
}
// ❌ WRONG — never do this in a client component
// "use client"
// const conn = process.env.NEXT_PUBLIC_DATABASE_URL; // exposed to browser!API key security
- API keys are hashed with bcrypt — we never store the raw key
- Only the first 12 characters (prefix) are stored for lookup
- Rotate keys anytime from the project dashboard
- You cannot revoke the last key — always keep at least one active
- Keys have no expiry by default — rotate them periodically
Rate limiting
Two layers of rate limiting protect your project:
- Monthly limit — enforced per plan (100 free, 5k starter, 50k pro)
- Per-second limit — max 5 concurrent requests per API key per 10 seconds
When limits are hit, the API returns HTTP 429 with a clear error message and your current usage.
Webhooks for usage alerts
Configure webhooks in your project to receive alerts when usage hits 50%, 80%, or 100% of your monthly limit. Webhooks are signed with HMAC-SHA256.
// Verify webhook signature in your endpoint
import crypto from "crypto";
app.post("/webhooks/syncagent", (req, res) => {
const signature = req.headers["x-syncagent-signature"];
const body = JSON.stringify(req.body);
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(body)
.digest("hex");
if (signature !== expected) {
return res.status(401).send("Invalid signature");
}
const { event, data } = req.body;
// event: "usage.50" | "usage.80" | "usage.100"
console.log(`Usage alert: ${data.percentage}% (${data.used}/${data.limit})`);
res.json({ received: true });
});Plans & Limits
| Plan | Requests/Month | Collections | Price |
|---|---|---|---|
| Free (+ 14-day trial) | 100 (500 during trial) | 5 | GH₵0 |
| Starter | 5,000 | 20 | GH₵150/mo |
| Pro | 50,000 | Unlimited | GH₵500/mo |
| Enterprise | Unlimited | Unlimited | Custom |
💡Every new project gets a 14-day trial with 500 free requests — no credit card required. After the trial, the project moves to the Free plan (100 requests/month).
What counts as a request?
Each message sent to the AI agent counts as one request. Schema discovery and API key generation do not count. A single user message may trigger multiple DB tool calls internally — that still counts as one request.
Collection limits
The collection limit applies to the number of collections/tables the agent can see in your database. If your database has more collections than your plan allows, the agent will only see the first N (sorted alphabetically). Use the Allowed Collections setting to control which ones are visible.
Dashboard Setup Guide
Configure your SyncAgent project from the dashboard before integrating the SDK.
Creating a Project
- Sign up at syncagentdev.vercel.app
- Click "New Project" from the dashboard
- Choose your database type
- Copy your API key (starts with
sa_)
Configuring Customer Agent
- Persona — Set your AI's name, tone, and behavior rules. Example: "You are a friendly support agent for Acme Corp. Always be helpful and concise."
- Knowledge Base — Upload documents, FAQs, or connect URLs. The AI searches these to answer customer questions accurately.
- Conversation Flows — Create guided paths for common scenarios (returns, billing, onboarding). See the Conversation Flows section for API details.
- Escalation Rules — Configure when the AI should hand off to a human agent (e.g., after 3 failed attempts, on specific keywords like "speak to human").
- Welcome Message — Set the first message customers see when they open the chat widget.
Managing API Keys
- Each project has its own API key
- Rotate keys from Settings → API Keys
- Old keys are invalidated immediately on rotation
⚠️Store your API key securely. You won't be able to view it again after creation — only regenerate.
Analytics
- View conversation volume, resolution rates, and average ratings
- Filter by date range, escalation status, or customer
- Export conversation logs for compliance
Error Handling & Troubleshooting
Common Errors
| Error | Cause | Fix |
|---|---|---|
| 401 Unauthorized | Invalid or expired API key | Check your sa_ key in the dashboard |
| 403 Forbidden | API key doesn't have access to this project | Verify the key belongs to the correct project |
| 404 Not Found | Invalid endpoint or project not found | Check your baseUrl configuration |
| 429 Too Many Requests | Rate limit exceeded | Implement exponential backoff or upgrade your plan |
| 500 Internal Server Error | Server-side issue | Retry after a few seconds. If persistent, contact support |
| GuestIdentificationRequiredError | Sending message before guest identification | Call identifyGuest() first or provide externalUserId |
| Schema discovery failed | Cannot connect to database | Verify your connection string and network access |
| Connection string format not recognized | Unsupported or malformed connection string | Check the Databases section for correct formats |
Error handling in code
JS SDK — try/catch pattern:
import { SyncAgentClient } from "@syncagent/js";
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
});
try {
const result = await agent.chat([
{ role: "user", content: "Show all users" }
]);
console.log(result.text);
} catch (error) {
if (error.status === 401) {
console.error("Invalid API key — check your dashboard");
} else if (error.status === 429) {
console.error("Rate limited — retrying in 5s...");
await new Promise(r => setTimeout(r, 5000));
// retry logic
} else {
console.error("Unexpected error:", error.message);
}
}React hook — error state:
import { useSyncAgent } from "@syncagent/react";
function MyChat() {
const { messages, error, sendMessage } = useSyncAgent();
if (error) {
return (
<div className="text-red-500 p-4">
<p>Something went wrong: {error.message}</p>
<button onClick={() => sendMessage("retry")}>Retry</button>
</div>
);
}
return <div>{/* chat UI */}</div>;
}Vue composable — error handling:
import { useSyncAgent } from "@syncagent/vue";
const { messages, error, sendMessage } = useSyncAgent({
apiKey: "sa_your_key",
connectionString: import.meta.env.VITE_DATABASE_URL,
});
watch(error, (err) => {
if (err) {
console.error("SyncAgent error:", err.message);
}
});Angular service — error handling:
import { SyncAgentService } from "@syncagent/angular";
@Component({ /* ... */ })
export class ChatComponent {
constructor(private syncAgent: SyncAgentService) {}
async send(message: string) {
try {
await this.syncAgent.sendMessage(message);
} catch (error) {
this.errorMessage = error.message;
}
}
}Debugging Tips
- Enable verbose logging with
debug: truein config - Check browser console for network errors
- Verify CORS if using from browser
- Test connection string independently first
// Enable debug mode for verbose logging
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
debug: true, // logs all requests, responses, and timing
});Migration Guide (0.3.x → 0.4.0)
What's New in 0.4.0
- Guest Identification flow for anonymous visitors (all frameworks)
- Conversation Flows — scripted branching paths for common support scenarios
GuestIdentificationFormcomponent (React, Vue)createDual()deprecated in favor of unified constructor withexternalUserId
Breaking Changes
💡None — 0.4.0 is fully backward compatible. No code changes required for existing functionality.
New Features
isIdentified,guestIdentity,identifyGuest()added to all customer chat hooks/servicesGuestIdentificationFormcomponent available in React, Vue, and Next.jsflowActiveandflowSessionfields inCustomerChatResultvalidateGuestForm,validateName,validateEmail,generateGuestIdentifierutility exports
Deprecations
SyncAgentClient.createDual() — use the unified constructor with externalUserId instead (see Unified Dual Mode section).
⚠️While createDual() still works in 0.4.0, it will be removed in a future major version. Migrate early.
Upgrade Steps
- Install the latest packages:
npm install @syncagent/js@0.4.0 @syncagent/react@0.4.0
# Also update framework-specific packages if used:
npm install @syncagent/vue@0.4.0 @syncagent/angular@0.4.0 @syncagent/nextjs@0.4.0- No code changes required for existing functionality
- Optionally add guest identification to customer-facing widgets (see Guest Identification section)
Changelog
0.5.0 — May 2026
- Added:
<SyncAgentCustomerChat>pre-built widget for React, Next.js, Vue, and Angular - Added: Theme engine (
computeTheme) migrated to@syncagent/jscore - Added: Angular standalone component with signals and Pusher integration
- Added: Vue SFC component with composables and Pusher integration
- Added: WCAG AA accessibility across all customer chat components
- Added:
relativeLuminanceandcontrastRatioutilities exported from@syncagent/js
0.4.0 — May 2026
- Added: Guest Identification flow for anonymous visitors
- Added:
GuestIdentificationFormcomponent (React, Vue) - Added: Conversation Flows — scripted branching conversation paths
- Added:
flowActiveandflowSessionin CustomerChatResult - Added: Guest identification utilities (
validateGuestForm,generateGuestIdentifier, etc.) - Added: Vue composable and Angular service guest identification support
- Added: Next.js re-exports for all guest identification features
- Deprecated:
SyncAgentClient.createDual()— use unified constructor instead
0.3.4 — April 2026
- Added: Unified Dual Mode — single constructor with
externalUserIdenables both modes - Added:
DualChatServicefor Angular - Added:
useDualChatcomposable for Vue - Improved: Auto-detection of
customerModewhenexternalUserIdis present
0.3.0 — March 2026
- Added: Customer Agent Mode with persona, knowledge base, and escalation
- Added:
useCustomerChathook (React) andCustomerChatService(Angular) - Added: Conversation rating system
- Added:
createCustomerServerConfigfor Next.js
0.2.0 — February 2026
- Added: Vue SDK (
@syncagent/vue) - Added: Angular SDK (
@syncagent/angular) - Added: Custom Tools and Tools-Only mode
- Added: Multi-tenancy with
filteroption
0.1.0 — January 2026
- Initial release
- React SDK with
SyncAgentChatwidget anduseSyncAgenthook - JS SDK with streaming and non-streaming chat
- Next.js SDK with server-side config helpers
- Support for MongoDB, PostgreSQL, MySQL, SQLite, SQL Server, Supabase
Database Connection Strings
mongodb+srv://user:pass@cluster.mongodb.net/mydb
mongodb://localhost:27017/mydbAlways include the database name at the end.
postgresql://user:pass@host:5432/mydb
postgres://user:pass@host/mydb?sslmode=requireWorks with Neon, Railway, Render, Supabase direct, AWS RDS.
mysql://user:pass@host:3306/mydb
mysql://user:pass@host/mydb?ssl=trueWorks with PlanetScale, Railway, AWS RDS, Google Cloud SQL.
/absolute/path/to/database.sqlite
file:./relative/path/db.sqliteUse absolute paths in production. Relative paths resolve from the server working directory.
Server=host,1433;Database=mydb;User Id=user;Password=pass;Encrypt=true;
Server=host;Database=mydb;Trusted_Connection=true;Works with Azure SQL, AWS RDS SQL Server, on-premise SQL Server 2016+.
https://your-project.supabase.co|your-anon-key
https://your-project.supabase.co|your-service-role-keyUse anon key for user-scoped access, service role key for admin access. Separated by |.