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.

🔌
Connect any database
MongoDB, PostgreSQL, MySQL, SQLite, SQL Server, Supabase — one SDK for all.
🧠
AI-powered queries
Natural language → real queries. The agent understands your schema automatically.
🔒
Your data stays yours
Connection strings are never stored. Passed at runtime, used once, discarded.

Choose Your Mode

Not sure which mode fits your use case? Use this decision tree to pick the right one.

🗄️
Database Agent Mode
For internal tools, admin dashboards, analytics. Your users query the database directly using natural language.
Best for: Admin panels, internal dashboards, data exploration tools.
💬
Customer Agent Mode
For customer-facing support widgets. Messages go through a support pipeline (persona, knowledge base, flows, escalation).
Best for: Help desks, support chat, customer portals.
Dual Mode
Both modes in one app. Admin staff get database access, customers get the support pipeline.
Best for: SaaS platforms where admins and customers coexist.

Decision Flowchart

Q: Do your users need to query a database?
YesDatabase Agent Mode

Q: Do you need AI-powered customer support?
YesCustomer Agent Mode

Q: Do you need both?
YesDual 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/js

Step 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
mongodb+srv://user:pass@cluster.mongodb.net/mydb

Include the database name at the end of the URL.

PostgreSQL
postgresql://user:pass@host:5432/mydb

Works with Neon, Railway, Render, Supabase direct connection, etc.

MySQL
mysql://user:pass@host:3306/mydb

Works with PlanetScale, Railway, AWS RDS, etc.

SQLite
/absolute/path/to/database.sqlite

Use an absolute path. Relative paths are resolved from the server working directory.

SQL Server
Server=host,1433;Database=mydb;User Id=user;Password=pass;Encrypt=true;

Works with Azure SQL, AWS RDS SQL Server, on-premise.

Supabase
https://xxx.supabase.co|your-anon-key

Use 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

PropTypeDefaultDescription
configSyncAgentConfigrequired*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)
defaultOpenbooleanfalseStart with panel open
titlestring"SyncAgent"Header title
subtitlestring"AI Database Assistant"Header subtitle
accentColorstring"#10b981"Brand color for header, FAB, send button
suggestionsstring[]3 defaultsQuick-start chips shown on empty state
persistKeystringlocalStorage key for conversation persistence
contextRecord<string,any>Extra context injected into every message
filterRecord<string,any>Mandatory query filter for multi-tenancy
onReaction(idx, reaction, content) => voidCalled when user reacts 👍/👎
onData(data: ToolData) => voidCalled 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/db

Tools-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

PropTypeDefaultDescription
createServerConfig(options)SyncAgentConfigserver onlyCreate config — accepts all SyncAgentConfig options
createServerConfigFromEnv(overrides?)SyncAgentConfigserver onlyCreate 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

PropTypeDefaultDescription
messagesRef<Message[]>Conversation history
isLoadingRef<boolean>True while streaming
errorRef<Error|null>Last error
statusRef<{step,label}|null>Live status while agent works
lastDataRef<ToolData|null>Last DB query result
sendMessage(content: string) => Promise<void>Send a message
stop() => voidAbort current stream
reset() => voidClear 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

PropTypeDefaultDescription
configure(config)voidInitialize with API key, connection string, and options
sendMessage(content)Promise<void>Send a message and start streaming
stop()voidAbort current stream
reset()voidClear all messages
messages()Message[]SignalConversation history (Angular Signal)
isLoading()booleanSignalTrue while streaming
error()Error|nullSignalLast error
status(){step,label}|nullSignalLive status
messages$Observable<Message[]>RxJSConversation history stream
message$Observable<Message>RxJSEmits each new assistant message
status$Observable<{step,label}|null>RxJSStatus 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.

PropTypeDefaultDescription
customerModebooleanfalseEnable customer agent mode. Routes messages through the support pipeline instead of the database agent. Automatically enabled when externalUserId is present.
externalUserIdstringUnique 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:

PropTypeDefaultDescription
escalatedbooleanfalseWhether the conversation was escalated to a human agent during this message
autoReplybooleanfalseWhether the response was generated by an automated flow (not the AI fallback)
flowActivebooleanfalseWhether a guided conversation flow is currently active
resolvedbooleanfalseWhether the conversation has been marked as resolved
welcomeMessagestring | undefinedWelcome message returned on the first interaction only
sourcesany[] | undefinedKnowledge base sources used to generate the response
flowSession{ flowId: string; currentNodeId: string } | undefinedActive 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.

PropTypeDefaultDescription
onEscalated() => voidCalled 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) => voidCalled 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/js

2. 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 scale

Conversation Lifecycle

Every customer conversation follows this lifecycle:

  1. New conversation — User sends their first message. A conversationId is created and a welcomeMessage may be returned.
  2. Messages — The AI responds using the configured persona, knowledge base, and flows. Each reply may include sources from the knowledge base.
  3. 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).
  4. 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);
PropTypeDefaultDescription
dbSyncAgentClientDatabase agent instance (customerMode: false). Use db.chat() for direct DB queries.
supportSyncAgentClientCustomer 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.

🎨
Themeable
Pass an accent color and dark mode flag — the theme engine generates a full WCAG-compliant palette automatically.
Accessible
ARIA roles, keyboard navigation, focus management, and 4.5:1 text contrast built in.
🔌
Real-time
Pusher integration for live human agent messages during escalation, with polling fallback.
🌐
Cross-framework
Same API surface across React, Next.js, Vue 3, and Angular 17+.

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.

PropTypeDefaultDescription
configSyncAgentConfigServer config object. When provided, apiKey/connectionString are ignored.
apiKeystringAPI key for authentication (ignored if config provided)
connectionStringstringOptional — database connection string. When provided, the agent can also query your DB. Without it, uses only persona, KB, flows, and escalation.
externalUserIdstringAuthenticated 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)
defaultOpenbooleanfalseWhether the floating panel starts open (floating mode only)
titlestring"Customer Support"Header title text
subtitlestring"How can we help you?"Header subtitle text
placeholderstring"Type your message..."Input placeholder text
welcomeMessagestringInitial welcome message displayed before any interaction
accentColorstring"#6366f1"Primary accent color (hex, rgb, or hsl)
darkModebooleanfalseEnable dark mode color scheme
classNamestringAdditional CSS class on the root container
guestFormGuestFormConfigCustom guest form configuration (title, subtitle, placeholders, button text)
pusherKeystringPusher app key for real-time human agent messages
pusherClusterstring"us2"Pusher cluster
metadataRecord<string, any>Custom metadata attached to conversations

With or Without a Database

The connectionString prop is optional. This gives you two deployment modes:

Without Database

Just pass apiKey — the agent uses persona, knowledge base, flows, and escalation only.

<SyncAgentCustomerChat
  apiKey="sa_your_key"
  accentColor="#6366f1"
/>
With Database

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

PropTypeDefaultDescription
onEscalated / (escalated)() => voidFired when the conversation is escalated to a human agent
onResolved / (resolved)(conversationId: string) => voidFired when the conversation is resolved
onGuestIdentified / (guestIdentified)(identity: GuestIdentity) => voidFired 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 uses role="log" with aria-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-motion is 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 / MethodJS CoreReactVueAngularNext.js
isIdentifiedboolean stateRef<boolean>Signal<boolean>boolean state
guestIdentitygetGuestIdentity()GuestIdentity | nullRef<GuestIdentity | null>Signal<GuestIdentity | null>GuestIdentity | null
identifyGuest(data)setGuestIdentity()hook methodcomposable methodservice methodhook method
isIdentified$BehaviorSubject<boolean>
guestIdentified$Subject<GuestIdentity>
GuestIdentificationFormReact componentVue SFCRe-export from React
validateGuestForm(data)re-exportre-exportre-exportre-export
generateGuestIdentifier(email)re-exportre-exportre-exportre-export
onGuestIdentifiedCustomerChatOptionshook optioncomposable optionconfig callbackhook 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:

decision

Presents the customer with labeled options. Each option routes to a different node. The customer's next message selects one of the options.

response

Sends a message to the customer and automatically continues to the next node via a single branch. No customer input is required to advance.

terminal

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:

RuleDescription
At least one trigger phraseThe flow must have at least one non-empty string in triggerPhrases
At least one decision nodeThe flow must contain at least one node with type "decision"
At least one terminal nodeThe flow must contain at least one node with type "terminal"
No dead-end non-terminal nodesEvery decision and response node must have at least one outgoing branch (options with a valid nextNodeId)
No circular referencesThe 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:

PropTypeDefaultDescription
flowActivebooleanfalsetrue when the customer is currently inside a conversation flow
flowSession{ flowId: string; currentNodeId: string }undefinedIdentifies 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 option

Complete 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

  1. You define tools with a name, description, parameters, and an execute function
  2. The SDK sends only the tool schema to SyncAgent (execute stays in your code)
  3. The AI decides when to call a tool based on the user's message
  4. The SDK runs your function locally and sends the result back to the AI
  5. 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 one

Multiple 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 back

Tool definition reference

PropTypeDefaultDescription
descriptionstringrequiredWhat the tool does — the AI reads this to decide when to call it
inputSchemaRecord<string, ToolParameter>requiredParameters the tool accepts
inputSchema.*.type"string"|"number"|"boolean"|"object"|"array"requiredParameter type
inputSchema.*.descriptionstringHelps the AI know what value to pass
inputSchema.*.requiredbooleantrueSet false for optional parameters
inputSchema.*.enumstring[]Restrict to specific values
execute(args) => any | Promise<any>requiredYour 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

  1. You pass toolsOnly: true and your custom tools
  2. SyncAgent skips database connection and schema discovery entirely
  3. The AI agent receives a system prompt listing only your custom tools
  4. When the user asks a question, the agent decides which of your tools to call
  5. Your execute function 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.

OperationTools enabledBehavior
readquery_documents, count_documents, aggregate_documentsExecutes immediately, no confirmation needed
createcreate_documentAgent states what it will insert, then executes
updateupdate_documentAgent states what it will change, then executes
deletedelete_documentAlways 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 fragment

The 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

PropTypeDefaultDescription
systemInstructionstringCustom agent instructions — personality, tone, rules
languagestringResponse language (e.g. "French", "Spanish")
confirmWritesbooleanfalseAsk for confirmation before create/update/delete
maxResultsnumber50Default max records per query
sensitiveFieldsstring[]["password","token","secret"]Fields to mask in responses
onBeforeToolCall(name, args) => booleanCalled before each client tool. Return false to block.
onAfterToolCall(name, args, result) => voidCalled 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

PropTypeDefaultDescription
connectionStringstringrequired*Your database connection string. *Optional when toolsOnly is true.
messagesMessage[]requiredArray of messages with id, role, and parts
filterRecord<string,any>Mandatory query filter for multi-tenancy
operationsstring[]Restrict operations for this request
clientToolsRecord<string,ToolDef>Custom tool schemas (execute runs client-side)
toolsOnlybooleanWhen true, disables all DB tools — agent only uses clientTools
contextRecord<string,any>Extra context (page, user, etc.) — injected into system prompt, not message text
systemInstructionstringCustom agent instructions — personality, tone, rules. Prepended to system prompt.
confirmWritesbooleanWhen true, agent asks for user confirmation before create/update/delete
languagestringLanguage the agent responds in (e.g. "French", "Spanish")
maxResultsnumberDefault max records per query (default 50)
sensitiveFieldsstring[]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

PropTypeDefaultDescription
401UnauthorizedMissing or invalid API key
403ForbiddenCollection limit reached for your plan
429Too Many RequestsMonthly request limit reached or rate limit exceeded
504Gateway TimeoutDatabase or AI took too long (90s timeout)
500Internal Server ErrorAgent 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

PropTypeDefaultDescription
messagestringrequiredThe customer's message text
connectionStringstringrequiredDatabase connection string for data-aware responses
conversationIdstringExisting conversation ID to continue a thread
externalUserIdstringCustomer identifier for multi-tenant scoping
filterRecord<string, any>Mandatory query filter for multi-tenant data scoping
metadataRecord<string, any>Additional metadata stored on the conversation

Response body

PropTypeDefaultDescription
conversationIdstringUnique conversation identifier
responsestringAI assistant response text
escalatedbooleanWhether conversation was escalated to a human agent
autoReplybooleanWhether response was an auto-reply (e.g. outside business hours)
flowActivebooleanWhether a conversation flow is currently active
resolvedbooleanWhether the conversation has been resolved
welcomeMessagestring?Welcome message (first interaction only)
sourcesany[]?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

PropTypeDefaultDescription
idstringrequiredThe conversation ID to rate

Request body

PropTypeDefaultDescription
ratinginteger (1-5)requiredSatisfaction rating — 1 (poor) to 5 (excellent)

Response body

PropTypeDefaultDescription
successbooleanWhether the rating was saved successfully
ratingnumberThe 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:

  1. Project level — configured in dashboard Settings tab
  2. API key level — set when generating the key (optional)
  3. Request level — passed via SDK operations config

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

EventTriggerPayload
conversation.createdNew conversation started{ conversationId, externalUserId, createdAt }
conversation.escalatedAI escalated to human{ conversationId, externalUserId, reason, messages }
conversation.resolvedConversation marked resolved{ conversationId, externalUserId, resolvedAt, rating }
conversation.ratedCustomer submitted rating{ conversationId, externalUserId, rating }
guest.identifiedGuest completed identification{ guestId, name, email, projectId }

Configuration

  1. Go to Dashboard → Settings → Webhooks → Add Endpoint
  2. Provide your HTTPS endpoint URL
  3. Select which events to subscribe to
  4. 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

PlanRequests/MonthCollectionsPrice
Free (+ 14-day trial)100 (500 during trial)5GH₵0
Starter5,00020GH₵150/mo
Pro50,000UnlimitedGH₵500/mo
EnterpriseUnlimitedUnlimitedCustom

💡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

  1. Sign up at syncagentdev.vercel.app
  2. Click "New Project" from the dashboard
  3. Choose your database type
  4. 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

ErrorCauseFix
401 UnauthorizedInvalid or expired API keyCheck your sa_ key in the dashboard
403 ForbiddenAPI key doesn't have access to this projectVerify the key belongs to the correct project
404 Not FoundInvalid endpoint or project not foundCheck your baseUrl configuration
429 Too Many RequestsRate limit exceededImplement exponential backoff or upgrade your plan
500 Internal Server ErrorServer-side issueRetry after a few seconds. If persistent, contact support
GuestIdentificationRequiredErrorSending message before guest identificationCall identifyGuest() first or provide externalUserId
Schema discovery failedCannot connect to databaseVerify your connection string and network access
Connection string format not recognizedUnsupported or malformed connection stringCheck 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: true in 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
  • GuestIdentificationForm component (React, Vue)
  • createDual() deprecated in favor of unified constructor with externalUserId

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/services
  • GuestIdentificationForm component available in React, Vue, and Next.js
  • flowActive and flowSession fields in CustomerChatResult
  • validateGuestForm, validateName, validateEmail, generateGuestIdentifier utility 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

  1. 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
  1. No code changes required for existing functionality
  2. 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/js core
  • 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: relativeLuminance and contrastRatio utilities exported from @syncagent/js

0.4.0 — May 2026

  • Added: Guest Identification flow for anonymous visitors
  • Added: GuestIdentificationForm component (React, Vue)
  • Added: Conversation Flows — scripted branching conversation paths
  • Added: flowActive and flowSession in 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 externalUserId enables both modes
  • Added: DualChatService for Angular
  • Added: useDualChat composable for Vue
  • Improved: Auto-detection of customerMode when externalUserId is present

0.3.0 — March 2026

  • Added: Customer Agent Mode with persona, knowledge base, and escalation
  • Added: useCustomerChat hook (React) and CustomerChatService (Angular)
  • Added: Conversation rating system
  • Added: createCustomerServerConfig for 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 filter option

0.1.0 — January 2026

  • Initial release
  • React SDK with SyncAgentChat widget and useSyncAgent hook
  • 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
mongodb+srv://user:pass@cluster.mongodb.net/mydb
mongodb://localhost:27017/mydb

Always include the database name at the end.

PostgreSQL
postgresql://user:pass@host:5432/mydb
postgres://user:pass@host/mydb?sslmode=require

Works with Neon, Railway, Render, Supabase direct, AWS RDS.

MySQL
mysql://user:pass@host:3306/mydb
mysql://user:pass@host/mydb?ssl=true

Works with PlanetScale, Railway, AWS RDS, Google Cloud SQL.

SQLite
/absolute/path/to/database.sqlite
file:./relative/path/db.sqlite

Use absolute paths in production. Relative paths resolve from the server working directory.

SQL Server
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+.

Supabase
https://your-project.supabase.co|your-anon-key
https://your-project.supabase.co|your-service-role-key

Use anon key for user-scoped access, service role key for admin access. Separated by |.