@syncagent/react
React SDK for SyncAgent — drop-in AI database chat widget and hooks for React apps.
Works with MongoDB, PostgreSQL, MySQL, SQLite, SQL Server, and Supabase.

Get Your API Key
- Sign up for a free account
- Go to your Dashboard → New Project → choose your database type
- Copy your API key (starts with
sa_)
Every new project gets a 14-day trial with 500 free requests — no credit card required. After the trial, you get 100 free requests/month on the Free plan.
Install
npm install @syncagent/react @syncagent/js
Quick Start
import { SyncAgentChat } from "@syncagent/react";
export default function App() {
return (
<SyncAgentChat
config={{
apiKey: "sa_your_api_key",
connectionString: process.env.DATABASE_URL,
}}
/>
);
}
A floating chat button appears in the bottom-right corner. Your users can now query your database in plain English.
<SyncAgentChat> Props
config | SyncAgentConfig | Required* | API key, connection string, tools, filter, operations |
mode | "floating" | "inline" | "floating" | Floating FAB or embedded inline panel |
position | "bottom-right" | "bottom-left" | "bottom-right" | FAB position (floating mode only) |
defaultOpen | boolean | false | Start with the panel open |
title | string | "SyncAgent" | Header title |
subtitle | string | "AI Database Assistant" | Header subtitle |
placeholder | string | "Ask anything..." | Input placeholder |
welcomeMessage | string | "Hi! I can query..." | Empty state message |
accentColor | string | "#10b981" | Brand color for header, FAB, send button |
suggestions | string[] | 3 defaults | Quick-start suggestion chips |
persistKey | string | — | localStorage key for conversation persistence |
context | Record<string, any> | — | Extra context injected into every message |
filter | Record<string, any> | — | Mandatory query filter for multi-tenancy |
operations | ("read"|"create"|"update"|"delete")[] | — | Restrict operations for this session |
onReaction | (idx, reaction, content) => void | — | Called when user reacts 👍/👎 |
onData | (data: ToolData) => void | — | Called when a DB tool returns structured data |
*config is required unless wrapped in <SyncAgentProvider>.
Inline Mode
Embed the chat inside your layout instead of a floating button:
<div style={{ height: 600 }}>
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
mode="inline"
/>
</div>
Custom UI with useSyncAgent
Build your own chat UI with full control:
import { SyncAgentProvider, useSyncAgent } from "@syncagent/react";
export default function App() {
return (
<SyncAgentProvider config={{ apiKey: "...", connectionString: "..." }}>
<MyChat />
</SyncAgentProvider>
);
}
function MyChat() {
const { messages, isLoading, error, status, lastData, sendMessage, stop, reset } = useSyncAgent();
return (
<div>
{status && <div>⏳ {status.label}</div>}
{messages.map((msg, i) => (
<div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
))}
<button onClick={() => sendMessage("Show all users")}>Ask</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Clear</button>
</div>
);
}
useSyncAgent Returns
messages | Message[] | Full conversation history |
isLoading | boolean | true while streaming |
error | Error | null | Last error |
status | { step, label } | null | Live status while agent is working |
lastData | ToolData | null | Last structured data from a DB tool |
sendMessage | (content: string) => void | Send a user message |
stop | () => void | Abort the current stream |
reset | () => void | Clear all messages |
Features
- Auto page detection — detects current page, record ID, and query params from the URL
- Live status — shows
● Querying users... while the agent works
- Markdown rendering — tables, code blocks, bold, italic, lists
- Streaming — blinking cursor while text streams in
- Copy button — on every AI response
- Reactions — 👍/👎 on AI messages
- Conversation persistence — saves history to localStorage
- Suggestion chips — configurable quick-start prompts
- Export CSV — download tables as CSV
- Bar charts — auto-renders aggregation results
- Resize handle — drag to resize the floating panel
- Mobile responsive — full-width on small screens
- Dark mode — respects
prefers-color-scheme
Multi-tenant SaaS
Pass filter to scope every agent operation to the current user's organization. Enforced server-side.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
filter: { organizationId: currentUser.orgId },
operations: currentUser.isAdmin
? ["read", "create", "update", "delete"]
: ["read"],
}}
/>
Custom Tools
Give the agent capabilities beyond your database:
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
tools: {
createInvoice: {
description: "Create a Stripe invoice for a customer",
inputSchema: {
customerId: { type: "string", description: "Stripe customer ID" },
amount: { type: "number", description: "Amount in cents" },
},
execute: async ({ customerId, amount }) => {
const inv = await stripe.invoices.create({ customer: customerId });
return { invoiceId: inv.id };
},
},
},
}}
/>
Tools-only Mode
Use the agent with only your custom tools — no database access:
<SyncAgentChat
config={{
apiKey: "sa_your_key",
toolsOnly: true,
tools: {
searchProducts: {
description: "Search products by name",
inputSchema: { query: { type: "string", description: "Search query" } },
execute: async ({ query }) => {
const res = await fetch(`/api/products?q=${query}`);
return res.json();
},
},
},
}}
/>
Customer Agent Mode
Use the useCustomerChat hook to build customer-facing support interfaces powered by SyncAgent's customer agent pipeline — including persona, flows, knowledge base, escalation, and AI fallback.
import { SyncAgentProvider, useCustomerChat } from "@syncagent/react";
function App() {
return (
<SyncAgentProvider
config={{
apiKey: "sa_your_api_key",
connectionString: process.env.DATABASE_URL,
externalUserId: currentUser.id,
}}
>
<CustomerChat />
</SyncAgentProvider>
);
}
function CustomerChat() {
const {
messages,
conversationId,
isLoading,
isEscalated,
isResolved,
error,
welcomeMessage,
sendMessage,
rateConversation,
reset,
} = useCustomerChat({
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Conversation resolved:", id),
});
return (
<div>
{welcomeMessage && <p>{welcomeMessage}</p>}
{messages.map((msg, i) => (
<div key={i}>
<strong>{msg.role}:</strong> {msg.content}
</div>
))}
{isEscalated && <p>You've been connected to a human agent.</p>}
{isResolved && (
<div>
<p>Conversation resolved!</p>
<button onClick={() => rateConversation(5)}>⭐ Rate 5/5</button>
</div>
)}
{error && <p>Error: {error.message}</p>}
<button onClick={() => sendMessage("I need help with my order")}>
Send
</button>
<button onClick={reset}>New Conversation</button>
</div>
);
}
useCustomerChat Return Values
messages | Message[] | Full conversation history (user and assistant messages) |
conversationId | string | null | Current conversation ID, set after first message |
isLoading | boolean | true while waiting for a response |
isEscalated | boolean | true when conversation has been escalated to a human agent |
isResolved | boolean | true when the conversation has been resolved |
error | Error | null | Last error encountered, or null |
welcomeMessage | string | null | Welcome message returned on first interaction |
sendMessage | (content: string, metadata?: Record<string, any>) => Promise<void> | Send a message to the customer agent |
rateConversation | (rating: number) => Promise<void> | Rate the conversation (1-5). Throws if no active conversation. |
reset | () => void | Clear all state and start a new conversation |
UseCustomerChatOptions
client | SyncAgentClient? | Optional client instance. If omitted, uses the client from SyncAgentProvider context. |
onEscalated | () => void? | Called when the conversation is escalated to a human agent |
onResolved | (conversationId: string) => void? | Called when the conversation is resolved |
Client resolution: useCustomerChat first checks for a client passed directly in options. If none is provided, it falls back to the client from the nearest <SyncAgentProvider>. If neither is available, it throws an error prompting you to provide one.
Guest Identification
When no externalUserId is provided in the config, the customer chat enters guest mode — anonymous visitors must identify themselves before sending messages. The useCustomerChat hook exposes guest identification state and a method to submit guest data programmatically.
Hook Fields
isIdentified | boolean | true if the user has an externalUserId or has completed guest identification |
guestIdentity | GuestIdentity | null | The stored guest identity object, or null if not yet identified |
identifyGuest | (data: { name: string; email: string; phone?: string }) => void | Submit guest form data — validates, generates a guest ID, persists to localStorage, and transitions to identified state |
Using GuestIdentificationForm
The GuestIdentificationForm component provides a ready-made form with built-in validation, accessibility attributes, and customizable text:
import { useCustomerChat, GuestIdentificationForm } from "@syncagent/react";
function CustomerChat() {
const {
messages,
isIdentified,
identifyGuest,
sendMessage,
} = useCustomerChat({
onGuestIdentified: (identity) => {
console.log("Guest identified:", identity.guestId);
},
});
if (!isIdentified) {
return (
<GuestIdentificationForm
onSubmit={(identity) => identifyGuest(identity)}
config={{
title: "Welcome!",
subtitle: "Tell us a bit about yourself",
submitButtonText: "Start Chat",
namePlaceholder: "Your name",
emailPlaceholder: "you@example.com",
phonePlaceholder: "Phone (optional)",
}}
className="my-guest-form"
/>
);
}
return (
<div>
{messages.map((msg, i) => (
<div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
))}
<button onClick={() => sendMessage("Hello!")}>Send</button>
</div>
);
}
GuestIdentificationFormProps
onSubmit | (identity: GuestIdentity) => void | Yes | Called with the complete GuestIdentity (including generated guestId) on valid submission |
config | GuestFormConfig | No | Customize title, subtitle, button text, and placeholder strings |
className | string | No | Custom CSS class applied to the form container |
Manual Guest Identification
If you prefer to build your own form UI, use the identifyGuest method directly:
import { useState } from "react";
import { useCustomerChat } from "@syncagent/react";
function CustomGuestForm() {
const { isIdentified, identifyGuest, error } = useCustomerChat();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
if (isIdentified) return null;
return (
<form
onSubmit={(e) => {
e.preventDefault();
identifyGuest({ name, email });
}}
>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<button type="submit">Continue</button>
{error && <p>{error.message}</p>}
</form>
);
}
The identifyGuest method validates the input using validateGuestForm from @syncagent/js. If validation fails, it sets the error state with a descriptive message. On success, it generates a deterministic guest identifier from the email, persists the identity to localStorage, and sets isIdentified to true.
Dual Mode (Database + Customer Agent)
⚠️ Deprecated: createDual() will be removed in a future major version. Use useDualChat() instead — pass externalUserId directly to enable both modes on a single instance.
Before (deprecated):
import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentProvider, useCustomerChat, useSyncAgent } from "@syncagent/react";
const { db, support } = SyncAgentClient.createDual({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
externalUserId: currentUser.id,
});
function AdminChat() {
return <SyncAgentProvider config={db}><AdminPanel /></SyncAgentProvider>;
}
function CustomerWidget() {
const chat = useCustomerChat({ client: support });
return <div>{/* customer chat UI */}</div>;
}
After (recommended):
import { SyncAgentProvider, useDualChat } from "@syncagent/react";
function App() {
return (
<SyncAgentProvider
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
externalUserId: currentUser.id,
}}
>
<DualChatUI />
</SyncAgentProvider>
);
}
function DualChatUI() {
const { db, support } = useDualChat();
}
Legacy pattern (deprecated):
import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentProvider, useCustomerChat, useSyncAgent } from "@syncagent/react";
const { db, support } = SyncAgentClient.createDual({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
externalUserId: currentUser.id,
});
function AdminChat() {
return <SyncAgentProvider config={db}><AdminPanel /></SyncAgentProvider>;
}
function CustomerWidget() {
const chat = useCustomerChat({ client: support });
return <div>{/* customer chat UI */}</div>;
}
Unified Dual Mode
The useDualChat() hook provides a single interface for managing both database agent and customer support agent conversations from within a <SyncAgentProvider>. When your config includes externalUserId, both modes are available through namespaced db and support objects.
import { SyncAgentProvider, useDualChat } from "@syncagent/react";
function App() {
return (
<SyncAgentProvider
config={{
apiKey: "sa_your_api_key",
connectionString: process.env.DATABASE_URL,
externalUserId: currentUser.id,
}}
>
<DualChatUI />
</SyncAgentProvider>
);
}
function DualChatUI() {
const { db, support } = useDualChat({
context: { tenant: currentUser.orgId },
onData: (data) => console.log("DB tool result:", data),
onEscalated: () => console.log("Escalated to human"),
onResolved: (id) => console.log("Resolved:", id),
});
return (
<div>
{/* Database agent panel */}
<section>
<h2>Admin Database Chat</h2>
{db.messages.map((msg, i) => (
<div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
))}
{db.isLoading && <p>Loading...</p>}
{db.error && <p>Error: {db.error.message}</p>}
<button onClick={() => db.sendMessage("Show all orders")}>
Query DB
</button>
<button onClick={db.stop}>Stop</button>
<button onClick={db.reset}>Clear</button>
</section>
{/* Customer support panel */}
<section>
<h2>Customer Support</h2>
{support.welcomeMessage && <p>{support.welcomeMessage}</p>}
{support.messages.map((msg, i) => (
<div key={i}><strong>{msg.role}:</strong> {msg.content}</div>
))}
{support.isLoading && <p>Loading...</p>}
{support.isEscalated && <p>Connected to a human agent.</p>}
{support.isResolved && (
<button onClick={() => support.rateConversation(5)}>⭐ Rate</button>
)}
{support.error && <p>Error: {support.error.message}</p>}
<button onClick={() => support.sendMessage("I need help")}>
Send
</button>
<button onClick={support.reset}>New Conversation</button>
</section>
</div>
);
}
Error Handling
If useDualChat() is called outside of a <SyncAgentProvider>, it throws:
useDualChat must be used within a <SyncAgentProvider>
Ensure your component is wrapped in a <SyncAgentProvider> with a valid config that includes externalUserId.
DualChatReturn
The useDualChat() hook returns an object with two namespaces: db for database agent state and support for customer agent state.
db Properties
messages | Message[] | Full database agent conversation history |
isLoading | boolean | true while the database agent is streaming |
error | Error | null | Last error from the database agent, or null |
status | { step: string; label: string } | null | Live status while the agent is working (e.g., querying) |
lastData | ToolData | null | Last structured data returned by a DB tool |
sendMessage | (content: string) => void | Send a message to the database agent |
stop | () => void | Abort the current database agent stream |
reset | () => void | Clear all database agent messages and state |
support Properties
messages | Message[] | Full customer support conversation history |
conversationId | string | null | Current conversation ID, set after first message |
isLoading | boolean | true while waiting for a support response |
isEscalated | boolean | true when conversation has been escalated to a human agent |
isResolved | boolean | true when the conversation has been resolved |
error | Error | null | Last error from the support agent, or null |
welcomeMessage | string | null | Welcome message returned on first interaction |
sendMessage | (content: string, metadata?: Record<string, any>) => Promise<void> | Send a message to the customer support agent |
rateConversation | (rating: number) => Promise<void> | Rate the conversation (1-5). Throws if no active conversation. |
reset | () => void | Clear all support state and start a new conversation |
UseDualChatOptions
context | Record<string, any> | No | Extra context injected into every database agent message |
onData | (data: ToolData) => void | No | Called when a DB tool returns structured data |
onEscalated | () => void | No | Called when the customer conversation is escalated to a human agent |
onResolved | (conversationId: string) => void | No | Called when the customer conversation is resolved |
Customization Options
All config options from @syncagent/js work here too:
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
systemInstruction: "You are a friendly sales assistant for Acme Corp.",
language: "French",
confirmWrites: true,
maxResults: 10,
sensitiveFields: ["ssn", "salary", "creditCard"],
onBeforeToolCall: (name, args) => { console.log(`[Audit] ${name}`, args); return true; },
onAfterToolCall: (name, args, result) => { analytics.track("tool_call", { tool: name }); },
}}
/>
Conversation Persistence
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
persistKey={currentUser.id}
/>
History saves to localStorage under sa_chat_{persistKey}. The "New" button clears it.
Context & Auto Page Detection
The SDK automatically detects the current page from window.location — zero config needed:
URL: /dashboard/orders/ord_123?tab=details
Auto-detected:
currentPage: "orders"
currentPath: "/dashboard/orders/ord_123"
currentRecordId: "ord_123"
param_tab: "details"
Pass additional context:
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
context={{ userId: currentUser.id, userRole: "admin" }}
/>
Vanilla JS Widget
No npm required — drop a script tag into any HTML page:
<script src="https://syncagentdev.vercel.app/api/v1/widget"></script>
<script>
SyncAgent.init({
apiKey: "sa_your_key",
connectionString: "your_database_url",
position: "right",
accentColor: "#10b981",
title: "AI Assistant",
persistKey: "my-app",
});
</script>
Security
- Your database connection string is never stored on SyncAgent servers
- Passed at runtime, used once, immediately discarded
- API keys are hashed with bcrypt
- Never expose your connection string in client-side code — use server components or API routes
Plans & Pricing
| Free (+ 14-day trial) | 100 (500 during trial) | 5 | GH₵0 |
| Starter | 5,000 | 20 | GH₵150/mo |
| Pro | 50,000 | Unlimited | GH₵500/mo |
| Enterprise | Unlimited | Unlimited | Custom |
View full pricing →
Resources
TypeScript Types
The following types are exported from @syncagent/react for use in your TypeScript projects:
DualChatReturn | Return type of the useDualChat() hook containing namespaced db and support objects | import { DualChatReturn } from "@syncagent/react" |
UseDualChatOptions | Options interface for configuring the useDualChat() hook (context, callbacks) | import { UseDualChatOptions } from "@syncagent/react" |
import { DualChatReturn, UseDualChatOptions } from "@syncagent/react";
License
MIT