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.
Quick Start
Step 1 — Create a project
Sign up → Dashboard → New Project → choose your database type → copy your API key.
💡Your API key starts with sa_. Keep it secret — treat it like a password.
Step 2 — Install
# React apps
npm install @syncagent/react @syncagent/js
# Next.js apps (server-safe helpers)
npm install @syncagent/nextjs @syncagent/js @syncagent/react
# Vue 3 apps
npm install @syncagent/vue @syncagent/js
# Angular apps
npm install @syncagent/angular @syncagent/js
# Node.js / any JS runtime
npm install @syncagent/jsStep 3 — Add the widget (React)
import { SyncAgentChat } from "@syncagent/react";
export default function App() {
return (
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY,
connectionString: process.env.DATABASE_URL,
// baseUrl: "http://localhost:3100", // dev only
}}
/>
);
}🔒Never expose your database connection string in client-side code. In Next.js, use a server action or API route to pass it securely. See the Security section below.
Step 4 — That's it
A floating chat button appears in the bottom-right corner. Your users can now query your database in plain English. The agent auto-discovers your schema on the first request and caches it for 5 minutes. If the schema changes mid-conversation, the agent automatically detects stale schema errors and refreshes it.
Supported Databases
Pass your connection string in the connectionString config. The agent auto-detects the format.
mongodb+srv://user:pass@cluster.mongodb.net/mydbInclude the database name at the end of the URL.
postgresql://user:pass@host:5432/mydbWorks with Neon, Railway, Render, Supabase direct connection, etc.
mysql://user:pass@host:3306/mydbWorks with PlanetScale, Railway, AWS RDS, etc.
/absolute/path/to/database.sqliteUse an absolute path. Relative paths are resolved from the server working directory.
Server=host,1433;Database=mydb;User Id=user;Password=pass;Encrypt=true;Works with Azure SQL, AWS RDS SQL Server, on-premise.
https://xxx.supabase.co|your-anon-keyUse the project URL and anon/service key separated by |. Uses the REST API, not direct PostgreSQL.
React SDK — @syncagent/react
Drop-in floating widget
The simplest integration. Renders a floating chat button that opens a full-featured chat panel.
import { SyncAgentChat } from "@syncagent/react";
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
}}
// Appearance
mode="floating" // "floating" | "inline"
position="bottom-right" // "bottom-right" | "bottom-left"
accentColor="#10b981" // any hex color
title="AI Assistant"
subtitle="Ask about your data"
welcomeMessage="Hi! Ask me anything about your data."
placeholder="Ask anything..."
// Behavior
defaultOpen={false}
persistKey="my-app" // saves chat history to localStorage
suggestions={[ // quick-start chips
"Show all records",
"Count total entries",
"Show recent activity",
]}
/>Inline mode — embed in your layout
// Embed inside a fixed-height container
<div style={{ height: 600 }}>
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
mode="inline"
/>
</div>Custom UI with useSyncAgent hook
Build your own chat UI. Wrap your component in SyncAgentProvider and use the hook.
import { SyncAgentProvider, useSyncAgent } from "@syncagent/react";
// 1. Wrap your app (or just the chat component)
export default function App() {
return (
<SyncAgentProvider
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
}}
>
<MyChat />
</SyncAgentProvider>
);
}
// 2. Use the hook inside
function MyChat() {
const {
messages, // Message[] — full conversation history
isLoading, // boolean — true while streaming
error, // Error | null
status, // { step, label } | null — live status
lastData, // ToolData | null — last DB query result
sendMessage, // (content: string) => void
stop, // () => void — abort current stream
reset, // () => void — clear all messages
} = useSyncAgent();
return (
<div>
{status && <div>⏳ {status.label}</div>}
{messages.map((msg, i) => (
<div key={i} className={msg.role === "user" ? "user" : "ai"}>
{msg.content}
</div>
))}
<button onClick={() => sendMessage("Show all users")}>Ask</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Clear</button>
</div>
);
}All SyncAgentChat props
| Prop | Type | Default | Description |
|---|---|---|---|
| config | SyncAgentConfig | required* | API key, connection string, tools, filter, operations, toolsOnly |
| mode | "floating" | "inline" | "floating" | Floating FAB or embedded panel |
| position | "bottom-right" | "bottom-left" | "bottom-right" | FAB position (floating only) |
| defaultOpen | boolean | false | Start with panel open |
| title | string | "SyncAgent" | Header title |
| subtitle | string | "AI Database Assistant" | Header subtitle |
| accentColor | string | "#10b981" | Brand color for header, FAB, send button |
| suggestions | string[] | 3 defaults | Quick-start chips shown on empty state |
| persistKey | string | — | localStorage key for conversation persistence |
| context | Record<string,any> | — | Extra context injected into every message |
| filter | Record<string,any> | — | Mandatory query filter for multi-tenancy |
| onReaction | (idx, reaction, content) => void | — | Called when user reacts 👍/👎 |
| onData | (data: ToolData) => void | — | Called when a DB tool returns data |
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_URL,
// baseUrl: "http://localhost:3100", // dev only
});
// 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);Next.js SDK — @syncagent/nextjs
The Next.js SDK provides server-side helpers that let you safely pass your database connection string from Server Components — it never reaches the browser bundle. All config options from @syncagent/js are supported.
💡Install: npm install @syncagent/nextjs @syncagent/js @syncagent/react
Server Component — the recommended pattern
// app/dashboard/page.tsx — Server Component (no "use client")
import { createServerConfig } from "@syncagent/nextjs/server";
import { SyncAgentChat } from "@syncagent/nextjs";
import { getServerSession } from "next-auth";
export default async function DashboardPage() {
const session = await getServerSession();
const config = createServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
connectionString: process.env.DATABASE_URL!,
filter: { organizationId: session.user.orgId },
operations: session.user.isAdmin
? ["read", "create", "update", "delete"]
: ["read"],
// All new options work here too:
systemInstruction: "You are a helpful assistant for our platform.",
language: "English",
confirmWrites: true,
maxResults: 25,
sensitiveFields: ["ssn", "salary"],
});
return (
<SyncAgentChat
config={config}
persistKey={session.user.id}
accentColor="#6366f1"
context={{ userId: session.user.id, page: "dashboard" }}
/>
);
}From environment variables
import { createServerConfigFromEnv } from "@syncagent/nextjs/server";
// Reads SYNCAGENT_KEY and DATABASE_URL automatically
const config = createServerConfigFromEnv({
filter: { orgId: session.user.orgId },
language: "French",
});# .env.local
SYNCAGENT_KEY=sa_your_api_key
DATABASE_URL=mongodb+srv://user:pass@cluster/dbTools-only mode in Next.js
Create the config on the server, but define tool execute functions on the client (functions can't be serialized across the server/client boundary).
// app/dashboard/page.tsx — Server Component
import { createServerConfig } from "@syncagent/nextjs/server";
import { ChatWithTools } from "./chat-with-tools";
export default async function Page() {
const config = createServerConfig({
apiKey: process.env.SYNCAGENT_KEY!,
toolsOnly: true,
systemInstruction: "You are a product search assistant.",
});
return <ChatWithTools serverConfig={config} />;
}// app/dashboard/chat-with-tools.tsx — "use client"
import { SyncAgentChat } from "@syncagent/nextjs";
export function ChatWithTools({ serverConfig }) {
return (
<SyncAgentChat
config={{
...serverConfig,
tools: {
searchProducts: {
description: "Search products",
inputSchema: { query: { type: "string" } },
execute: async ({ query }) => {
const res = await fetch(`/api/products?q=${query}`);
return res.json();
},
},
},
}}
/>
);
}Middleware hooks (client-side only)
Functions like onBeforeToolCall can't be serialized from Server Components. Define them in a Client Component and spread the server config:
// app/components/chat.tsx — "use client"
import { SyncAgentChat } from "@syncagent/nextjs";
export function Chat({ serverConfig }) {
return (
<SyncAgentChat
config={{
...serverConfig,
onBeforeToolCall: (name, args) => {
console.log(`[Audit] ${name}`, args);
return true;
},
}}
/>
);
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| createServerConfig(options) | SyncAgentConfig | server only | Create config — accepts all SyncAgentConfig options |
| createServerConfigFromEnv(overrides?) | SyncAgentConfig | server only | Create config from SYNCAGENT_KEY + DATABASE_URL env vars |
All components and hooks from @syncagent/react are re-exported from @syncagent/nextjs.
Vue SDK — @syncagent/vue
Vue 3 composables for SyncAgent chat. Works with Nuxt, Vite, and any Vue 3 app.
💡Install: npm install @syncagent/vue @syncagent/js
Basic usage
<script setup lang="ts">
import { ref } from "vue";
import { SyncAgentClient } from "@syncagent/js";
import { useSyncAgent } from "@syncagent/vue";
const client = new SyncAgentClient({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
connectionString: import.meta.env.VITE_DATABASE_URL,
});
const { messages, isLoading, error, status, sendMessage, stop, reset } = useSyncAgent({ client });
const input = ref("");
function send() {
if (!input.value.trim()) return;
sendMessage(input.value);
input.value = "";
}
</script>
<template>
<div>
<div v-if="status">⏳ {{ status.label }}</div>
<div v-for="(msg, i) in messages" :key="i" :class="msg.role">
{{ msg.content }}
</div>
<div v-if="error">⚠️ {{ error.message }}</div>
<input v-model="input" @keydown.enter="send" :disabled="isLoading" />
<button @click="send" :disabled="isLoading">Send</button>
<button v-if="isLoading" @click="stop">Stop</button>
<button @click="reset">Clear</button>
</div>
</template>Multi-tenant SaaS
<script setup lang="ts">
import { computed } from "vue";
import { SyncAgentClient } from "@syncagent/js";
import { useSyncAgent } from "@syncagent/vue";
import { useAuth } from "@/composables/auth";
const { user } = useAuth();
const client = computed(() => new SyncAgentClient({
apiKey: import.meta.env.VITE_SYNCAGENT_KEY,
connectionString: import.meta.env.VITE_DATABASE_URL,
filter: { organizationId: user.value.orgId },
operations: user.value.isAdmin ? ["read","create","update","delete"] : ["read"],
}));
const { messages, sendMessage } = useSyncAgent({
client: client.value,
context: { userId: user.value.id },
});
</script>useSyncAgent return values
| Prop | Type | Default | Description |
|---|---|---|---|
| messages | Ref<Message[]> | — | Conversation history |
| isLoading | Ref<boolean> | — | True while streaming |
| error | Ref<Error|null> | — | Last error |
| status | Ref<{step,label}|null> | — | Live status while agent works |
| lastData | Ref<ToolData|null> | — | Last DB query result |
| sendMessage | (content: string) => Promise<void> | — | Send a message |
| stop | () => void | — | Abort current stream |
| reset | () => void | — | Clear all messages |
Angular SDK — @syncagent/angular
Injectable Angular service with both Angular Signals and RxJS observables.
💡Install: npm install @syncagent/angular @syncagent/js
Basic usage
// app.component.ts
import { Component, OnInit } from "@angular/core";
import { SyncAgentService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-root",
providers: [SyncAgentService],
template: `
<div *ngIf="agent.status() as s">⏳ {{ s.label }}</div>
<div *ngFor="let msg of agent.messages()" [class]="msg.role">
{{ msg.content }}
</div>
<div *ngIf="agent.error() as err">
⚠️ {{ err.message }}
</div>
<input [(ngModel)]="input" (keydown.enter)="send()" [disabled]="agent.isLoading()" />
<button (click)="send()" [disabled]="agent.isLoading()">Send</button>
<button *ngIf="agent.isLoading()" (click)="agent.stop()">Stop</button>
<button (click)="agent.reset()">Clear</button>
`,
})
export class AppComponent implements OnInit {
input = "";
constructor(public agent: SyncAgentService) {}
ngOnInit() {
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
});
}
send() {
if (!this.input.trim()) return;
this.agent.sendMessage(this.input);
this.input = "";
}
}Multi-tenant SaaS
ngOnInit() {
const user = this.authService.currentUser;
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
filter: { organizationId: user.orgId },
operations: user.isAdmin
? ["read", "create", "update", "delete"]
: ["read"],
context: { userId: user.id, userRole: user.role },
});
}Using RxJS observables
// Subscribe to messages stream
this.agent.messages$
.pipe(takeUntil(this.destroy$))
.subscribe(messages => console.log(messages.length));
// Subscribe to each new assistant message
this.agent.message$
.pipe(takeUntil(this.destroy$))
.subscribe(msg => console.log(msg.content));
// Subscribe to status
this.agent.status$
.pipe(takeUntil(this.destroy$), filter(Boolean))
.subscribe(({ step, label }) => console.log(`[${step}] ${label}`));SyncAgentService API
| Prop | Type | Default | Description |
|---|---|---|---|
| configure(config) | void | — | Initialize with API key, connection string, and options |
| sendMessage(content) | Promise<void> | — | Send a message and start streaming |
| stop() | void | — | Abort current stream |
| reset() | void | — | Clear all messages |
| messages() | Message[] | Signal | Conversation history (Angular Signal) |
| isLoading() | boolean | Signal | True while streaming |
| error() | Error|null | Signal | Last error |
| status() | {step,label}|null | Signal | Live status |
| messages$ | Observable<Message[]> | RxJS | Conversation history stream |
| message$ | Observable<Message> | RxJS | Emits each new assistant message |
| status$ | Observable<{step,label}|null> | RxJS | Status stream |
Custom Tools
Give the agent capabilities beyond your database. Tools run entirely in your app — SyncAgent only sees the schema and the result you return, never your implementation or secrets.
🔒Your execute function runs in your app, not on SyncAgent servers. API keys, secrets, and business logic never leave your environment.
How it works
- You define tools with a name, description, parameters, and an execute function
- The SDK sends only the tool schema to SyncAgent (execute stays in your code)
- The AI decides when to call a tool based on the user's message
- The SDK runs your function locally and sends the result back to the AI
- The AI uses the result to continue the conversation
Basic example — send email
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
tools: {
sendEmail: {
description: "Send an email to a user",
inputSchema: {
to: { type: "string", description: "Recipient email address" },
subject: { type: "string", description: "Email subject line" },
body: { type: "string", description: "Email body (plain text)" },
},
execute: async ({ to, subject, body }) => {
// Your email logic — runs in YOUR app
await resend.emails.send({ from: "noreply@yourapp.com", to, subject, text: body });
return { sent: true, to };
},
},
},
}}
/>
// Now users can say: "Email all users whose subscription expired"
// The agent will query expired users, then call sendEmail for each oneMultiple tools — real-world example
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
tools: {
sendSlackAlert: {
description: "Post an alert to a Slack channel",
inputSchema: {
channel: { type: "string", description: "Channel name e.g. #alerts" },
message: { type: "string", description: "Alert message" },
severity: { type: "string", description: "low | medium | high", enum: ["low","medium","high"] },
},
execute: async ({ channel, message, severity }) => {
await slack.chat.postMessage({ channel, text: `[${severity.toUpperCase()}] ${message}` });
return { posted: true };
},
},
createStripeInvoice: {
description: "Create a Stripe invoice for a customer",
inputSchema: {
customerId: { type: "string", description: "Stripe customer ID" },
amount: { type: "number", description: "Amount in cents" },
description:{ type: "string", description: "Invoice description" },
},
execute: async ({ customerId, amount, description }) => {
const invoice = await stripe.invoices.create({
customer: customerId,
description,
collection_method: "send_invoice",
days_until_due: 30,
});
await stripe.invoiceItems.create({ customer: customerId, amount, invoice: invoice.id });
await stripe.invoices.finalizeInvoice(invoice.id);
return { invoiceId: invoice.id, status: invoice.status };
},
},
generatePdfReport: {
description: "Generate a PDF report and return the download URL",
inputSchema: {
title: { type: "string", description: "Report title" },
content: { type: "string", description: "Report content as markdown" },
},
execute: async ({ title, content }) => {
const url = await pdfService.generate({ title, markdown: content });
return { url, generatedAt: new Date().toISOString() };
},
},
},
});
// "Find all overdue invoices, create Stripe invoices for them, and post a summary to #billing"
// The agent will: query DB → create invoices → post Slack message → report backTool definition reference
| Prop | Type | Default | Description |
|---|---|---|---|
| description | string | required | What the tool does — the AI reads this to decide when to call it |
| inputSchema | Record<string, ToolParameter> | required | Parameters the tool accepts |
| inputSchema.*.type | "string"|"number"|"boolean"|"object"|"array" | required | Parameter type |
| inputSchema.*.description | string | — | Helps the AI know what value to pass |
| inputSchema.*.required | boolean | true | Set false for optional parameters |
| inputSchema.*.enum | string[] | — | Restrict to specific values |
| execute | (args) => any | Promise<any> | required | Your function — runs in your app, not on SyncAgent servers |
Tools-Only Mode
When you want the agent to only use your custom tools — with no database access at all — set toolsOnly: true. This is useful when you want to build an AI assistant powered by your own APIs, webhooks, or business logic.
💡In tools-only mode, connectionString is not required. No database connection is made, no schema is discovered, and no built-in DB tools are available.
React example
import { SyncAgentChat } from "@syncagent/react";
<SyncAgentChat
config={{
apiKey: "sa_your_key",
toolsOnly: true,
tools: {
searchProducts: {
description: "Search products by name or category",
inputSchema: {
query: { type: "string", description: "Search query" },
},
execute: async ({ query }) => {
const res = await fetch(`/api/products?q=${query}`);
return res.json();
},
},
createOrder: {
description: "Place an order for a product",
inputSchema: {
productId: { type: "string", description: "Product ID" },
quantity: { type: "number", description: "Quantity" },
},
execute: async ({ productId, quantity }) => {
const res = await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId, quantity }),
});
return res.json();
},
},
},
}}
/>JS SDK example
import { SyncAgentClient } from "@syncagent/js";
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
toolsOnly: true,
tools: {
getWeather: {
description: "Get current weather for a city",
inputSchema: {
city: { type: "string", description: "City name" },
},
execute: async ({ city }) => {
const res = await fetch(`https://api.weather.com/v1/${city}`);
return res.json();
},
},
},
});
const result = await agent.chat([
{ role: "user", content: "What's the weather in Accra?" }
]);
console.log(result.text);REST API
curl -X POST https://syncagent.dev/api/v1/chat \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"toolsOnly": true,
"clientTools": {
"searchProducts": {
"description": "Search products by name",
"inputSchema": {
"query": { "type": "string", "description": "Search query" }
}
}
},
"messages": [{
"id": "1",
"role": "user",
"parts": [{ "type": "text", "text": "Find running shoes" }]
}]
}'When to use tools-only mode
- You want an AI assistant that calls your own REST APIs instead of querying a database directly
- Your data comes from third-party services (Stripe, Shopify, Salesforce, etc.)
- You need the agent to trigger actions (send emails, create tickets, deploy code) without any DB access
- You want full control over every tool the agent can use
How it works
- You pass
toolsOnly: trueand your customtools - SyncAgent skips database connection and schema discovery entirely
- The AI agent receives a system prompt listing only your custom tools
- When the user asks a question, the agent decides which of your tools to call
- Your
executefunction runs in your app, and the result is sent back to the AI
Multi-tenancy
Building a SaaS app where multiple organizations share the same database? Use the filterprop to scope every agent operation to the current user's organization. Enforced server-side — the agent cannot query outside this scope.
🔒The filter is applied server-side to every query, count, aggregation, insert, update, and delete. The agent is told about the scope in its system prompt and cannot override it.
Basic multi-tenant setup
// In your app, after the user logs in:
import { SyncAgentChat } from "@syncagent/react";
export default function Dashboard({ currentUser }) {
return (
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY,
connectionString: process.env.DATABASE_URL,
// Scope ALL queries to this organization
filter: { organizationId: currentUser.orgId },
}}
/>
);
}
// Every operation is now scoped:
// "Show all orders" → db.orders.find({ organizationId: "org_123" })
// "Count users" → db.users.countDocuments({ organizationId: "org_123" })
// "Add a product" → db.products.insertOne({ ...doc, organizationId: "org_123" })Per-user operation restrictions
Use operations to give different users different access levels. The client can only restrict further — never grant more than the project dashboard allows.
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY,
connectionString: process.env.DATABASE_URL,
filter: { organizationId: currentUser.orgId },
// Admins get full access, regular users read-only
operations: currentUser.role === "admin"
? ["read", "create", "update", "delete"]
: ["read"],
}}
/>Common filter patterns
// By organization ID (most common)
filter: { organizationId: org.id }
// By tenant slug
filter: { tenant: "acme-corp" }
// Personal data — scope to current user
filter: { userId: currentUser.id }
// Multiple conditions
filter: { orgId: org.id, deleted: false }
// SQL databases (same syntax)
filter: { tenant_id: tenant.id }Operations & Permissions
Control what the agent can do. Configure the maximum allowed operations in your project dashboard, then optionally restrict further per-session using the operations prop.
| Operation | Tools enabled | Behavior |
|---|---|---|
| read | query_documents, count_documents, aggregate_documents | Executes immediately, no confirmation needed |
| create | create_document | Agent states what it will insert, then executes |
| update | update_document | Agent states what it will change, then executes |
| delete | delete_document | Always asks for explicit confirmation first. Empty filter blocked. |
⚠️Deletes always require explicit user confirmation and are blocked with an empty filter. The agent will never delete all records in a collection.
Allowed & blocked collections
In your project Settings tab, you can restrict which collections the agent can see:
Allowed Collections: users, orders, products
(blank = all collections)
Blocked Collections: admin_logs, secrets, audit_trail
(always denied, overrides allowed list)Context & Auto Page Detection
The SDK automatically detects the current page from window.locationon every message — zero config needed. The agent knows what page the user is on, what record they're viewing, and any relevant query params.
💡Auto page detection works in all frameworks: React, Next.js, Vue, Angular, and vanilla JS. It's SSR-safe — returns empty context on the server.
What gets auto-detected
URL: /dashboard/orders/ord_123?tab=details&status=active
Auto-detected context:
currentPage: "orders" ← last meaningful path segment
currentPath: "/dashboard/orders/ord_123"
currentRecordId: "ord_123" ← detected as ID (ObjectId/UUID/numeric)
param_tab: "details" ← useful query params
param_status: "active"
URL: /app/settings#billing
currentPage: "settings"
currentPath: "/app/settings"
currentSection: "billing" ← from hash fragmentThe user can now say "show me this order" and the agent automatically queries the record with ID ord_123. Say "show recent" on the orders page and it queries the orders collection.
Adding extra context
Pass additional context that merges on top of the auto-detected values. Developer values override auto-detected ones.
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
context={{
userId: currentUser.id,
userRole: currentUser.role,
orgName: currentOrg.name,
currentDate: new Date().toISOString(),
}}
/>
// Final context sent to the AI:
// {
// currentPage: "orders", ← auto-detected
// currentPath: "/dashboard/orders",
// userId: "u_123", ← developer-provided
// userRole: "admin",
// orgName: "Acme Corp",
// currentDate: "2025-07-14T..."
// }Disabling auto-detection
// Disable auto page detection — only use manually passed context
<SyncAgentChat
config={{
apiKey: "...",
connectionString: "...",
autoDetectPage: false,
}}
context={{ page: "custom-page", recordId: "123" }}
/>How context reaches the AI
Context is sent as a separate field in the request body (not appended to the user's message). The server injects it into the AI system prompt as a structured USER CONTEXTsection. The AI is instructed to use it when the user says "this", "here", "current", and to use context values as default filters.
JS SDK — works the same way
import { SyncAgentClient } from "@syncagent/js";
const agent = new SyncAgentClient({
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
// autoDetectPage: true (default)
});
// Auto-detection happens automatically on every chat() call
await agent.chat(messages);
// Or pass extra context:
await agent.chat(messages, {
context: { userId: "u_123", userRole: "admin" }
});Customization
SyncAgent provides several config options to customize the agent's behavior, language, safety, and access control.
System Instructions
Customize the agent's personality, tone, domain knowledge, or rules. Instructions are prepended to the system prompt and take priority over default behavior.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
systemInstruction: "You are a friendly sales assistant for Acme Corp. Always suggest upsells when showing order data. Never mention competitor names.",
}}
/>Response Language
Make the agent respond in any language. Field names and code stay in English so tools work correctly.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
language: "French", // or "Spanish", "Japanese", "Arabic", "Twi", etc.
}}
/>
// User: "Show me all orders"
// Agent: "Voici toutes les commandes..." Write Confirmation
Require explicit user confirmation before any create, update, or delete operation.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
confirmWrites: true,
}}
/>
// User: "Add a new customer named John"
// Agent: "I'd like to create a record in customers:
- name: John
Should I proceed?"
// User: "yes"
// Agent: "✅ Created customer John (id: 507f...)" Max Results
Control the default number of records returned per query. Default is 50.
// Mobile app — small results
<SyncAgentChat config={{ ..., maxResults: 10 }} />
// Admin dashboard — large results
<SyncAgentChat config={{ ..., maxResults: 100 }} />Sensitive Field Masking
Specify which fields the agent should mask in responses. Default: password, token, secret.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
sensitiveFields: ["ssn", "creditCard", "salary", "bankAccount"],
}}
/>
// Agent shows: ssn: "••••••••", salary: "••••••••" Middleware Hooks
Intercept client tool calls for logging, audit trails, or dynamic blocking.
<SyncAgentChat
config={{
apiKey: "sa_your_key",
connectionString: process.env.DATABASE_URL,
tools: { /* your custom tools */ },
// Block specific operations dynamically
onBeforeToolCall: (toolName, args) => {
console.log(`[Audit] ${toolName}`, args);
if (toolName === "deleteUser" && !currentUser.isAdmin) return false;
return true; // allow
},
// Log every result
onAfterToolCall: (toolName, args, result) => {
analytics.track("tool_call", { tool: toolName, success: result.success });
},
}}
/>ℹ️When onBeforeToolCall returns false, the tool is blocked and the AI receives an error explaining the operation was denied.
All customization options
| Prop | Type | Default | Description |
|---|---|---|---|
| systemInstruction | string | — | Custom agent instructions — personality, tone, rules |
| language | string | — | Response language (e.g. "French", "Spanish") |
| confirmWrites | boolean | false | Ask for confirmation before create/update/delete |
| maxResults | number | 50 | Default max records per query |
| sensitiveFields | string[] | ["password","token","secret"] | Fields to mask in responses |
| onBeforeToolCall | (name, args) => boolean | — | Called before each client tool. Return false to block. |
| onAfterToolCall | (name, args, result) => void | — | Called after each client tool executes |
Conversation Persistence
Pass persistKey to save chat history to localStorage. Survives page refresh. The "New" button in the header clears it.
// Use a unique key per user or project
<SyncAgentChat
config={{ apiKey: "...", connectionString: "..." }}
persistKey={currentUser.id} // or project.id, or "global"
/>
// History is saved to localStorage under: sa_chat_{persistKey}
// Conversation history sidebar (🕐 button) shows past conversations💡Use the user's ID as the persist key so each user has their own conversation history. Use a project ID if you want all users of a project to share history.
Vanilla JS Widget
No npm required. Drop a script tag into any HTML page — works with plain HTML, PHP, Ruby on Rails, Django, or any server-rendered app.
<script src="https://syncagentdev.vercel.app/api/v1/widget"></script>
<script>
SyncAgent.init({
apiKey: "sa_your_key",
connectionString: "your_database_url",
// Appearance
position: "right", // "right" or "left"
accentColor: "#10b981", // brand color
title: "AI Assistant",
subtitle: "Ask about your data",
// Behavior
persistKey: "my-app", // localStorage persistence
open: false, // start open?
});
// Programmatic control
SyncAgent.open();
SyncAgent.close();
SyncAgent.toggle();
SyncAgent.clearHistory(); // clear persisted conversation
</script>Features included in the widget
- Markdown rendering (tables, code blocks, bold, lists)
- Live status indicator (connecting, querying, thinking)
- 👍/👎 reaction buttons on AI responses
- Copy button on AI responses
- Conversation persistence via localStorage
- New conversation button
- Stop button to abort streaming
- Dark mode support (prefers-color-scheme)
- Mobile responsive
REST API
SyncAgent is a standard REST API. Use it from any language — Python, Go, Ruby, PHP, Java, C#, Rust, or anything that can make HTTP requests.
ℹ️All API endpoints require Authorization: Bearer sa_your_key header.
POST /api/v1/chat — Send a message
Returns a plain text streaming response. Read it line by line.
curl -X POST https://syncagent.dev/api/v1/chat \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"connectionString": "mongodb+srv://user:pass@cluster/db",
"messages": [{
"id": "1",
"role": "user",
"parts": [{ "type": "text", "text": "Show all active users" }]
}]
}'import requests
res = requests.post(
"https://syncagent.dev/api/v1/chat",
headers={"Authorization": "Bearer sa_your_key"},
json={
"connectionString": "mongodb+srv://user:pass@cluster/db",
"messages": [{
"id": "1", "role": "user",
"parts": [{"type": "text", "text": "Show all active users"}]
}]
},
stream=True
)
for chunk in res.iter_content(decode_unicode=True):
print(chunk, end="")body, _ := json.Marshal(map[string]any{
"connectionString": "mongodb+srv://user:pass@cluster/db",
"messages": []map[string]any{{
"id": "1", "role": "user",
"parts": []map[string]string{{"type": "text", "text": "Show all active users"}},
}},
})
req, _ := http.NewRequest("POST", "https://syncagent.dev/api/v1/chat", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer sa_your_key")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
io.Copy(os.Stdout, res.Body)Request body parameters
| Prop | Type | Default | Description |
|---|---|---|---|
| connectionString | string | required* | Your database connection string. *Optional when toolsOnly is true. |
| messages | Message[] | required | Array of messages with id, role, and parts |
| filter | Record<string,any> | — | Mandatory query filter for multi-tenancy |
| operations | string[] | — | Restrict operations for this request |
| clientTools | Record<string,ToolDef> | — | Custom tool schemas (execute runs client-side) |
| toolsOnly | boolean | — | When true, disables all DB tools — agent only uses clientTools |
| context | Record<string,any> | — | Extra context (page, user, etc.) — injected into system prompt, not message text |
| systemInstruction | string | — | Custom agent instructions — personality, tone, rules. Prepended to system prompt. |
| confirmWrites | boolean | — | When true, agent asks for user confirmation before create/update/delete |
| language | string | — | Language the agent responds in (e.g. "French", "Spanish") |
| maxResults | number | — | Default max records per query (default 50) |
| sensitiveFields | string[] | — | Fields to mask in responses (default: password, token, secret) |
POST /api/v1/schema — Discover schema
curl -X POST https://syncagent.dev/api/v1/schema \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{ "connectionString": "mongodb+srv://user:pass@cluster/db" }'
# Response:
# {
# "success": true,
# "dbType": "mongodb",
# "collections": [
# {
# "name": "users",
# "documentCount": 1247,
# "fields": [
# { "name": "_id", "type": "ObjectId" },
# { "name": "email", "type": "string", "sample": "user@example.com" },
# { "name": "createdAt", "type": "Date" }
# ]
# }
# ]
# }Error responses
| Prop | Type | Default | Description |
|---|---|---|---|
| 401 | Unauthorized | — | Missing or invalid API key |
| 403 | Forbidden | — | Collection limit reached for your plan |
| 429 | Too Many Requests | — | Monthly request limit reached or rate limit exceeded |
| 504 | Gateway Timeout | — | Database or AI took too long (90s timeout) |
| 500 | Internal Server Error | — | Agent error — check error.message for details |
Conversations API
Store and retrieve conversation history server-side. Useful for multi-device sync, audit trails, and analytics. All endpoints require Authorization: Bearer sa_your_key.
POST /api/v1/conversations — Save a conversation
curl -X POST https://syncagentdev.vercel.app/api/v1/conversations \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"title": "Order analysis",
"messages": [
{ "role": "user", "content": "Show all orders over $500" },
{ "role": "assistant", "content": "Here are 12 orders..." }
],
"metadata": { "page": "orders" }
}'
# Response:
# { "success": true, "conversation": { "id": "conv_abc", "title": "Order analysis", "messageCount": 2 } }GET /api/v1/conversations — List conversations
curl https://syncagentdev.vercel.app/api/v1/conversations?userId=user_123&limit=20 \
-H "Authorization: Bearer sa_your_key"
# Response:
# { "success": true, "conversations": [...], "total": 42, "limit": 20, "offset": 0 }GET /api/v1/conversations/:id — Get full conversation
curl https://syncagentdev.vercel.app/api/v1/conversations/conv_abc \
-H "Authorization: Bearer sa_your_key"
# Response:
# { "success": true, "conversation": { "id": "conv_abc", "messages": [...], "metadata": {...} } }DELETE /api/v1/conversations — Delete a conversation
curl -X DELETE https://syncagentdev.vercel.app/api/v1/conversations \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{ "id": "conv_abc" }'Update an existing conversation
Pass the id field in the POST body to update instead of create:
curl -X POST https://syncagentdev.vercel.app/api/v1/conversations \
-H "Authorization: Bearer sa_your_key" \
-H "Content-Type: application/json" \
-d '{
"id": "conv_abc",
"messages": [
{ "role": "user", "content": "Show all orders over $500" },
{ "role": "assistant", "content": "Here are 12 orders..." },
{ "role": "user", "content": "Now filter by this month" },
{ "role": "assistant", "content": "Found 3 orders this month..." }
]
}'💡Conversations auto-expire after 30 days. Use this API to persist important conversations to your own database if needed.
API Key Scoping
Create API keys with restricted permissions. Useful for giving different access levels to different parts of your app (e.g., a read-only key for a public widget vs a full-access key for admin dashboards).
How it works
When generating a new API key in the dashboard, you can optionally restrict which operations that key allows. The key can only restrict further — it can never grant more than the project's configured operations.
// Key with read-only access (for public-facing widget)
// Generated in dashboard with operations: ["read"]
// Key with full access (for admin panel)
// Generated in dashboard with operations: ["read", "create", "update", "delete"]
// The SDK can further restrict at runtime:
const agent = new SyncAgentClient({
apiKey: "sa_readonly_key", // this key only allows "read"
connectionString: process.env.DATABASE_URL,
operations: ["read", "create"], // "create" is ignored — key doesn't allow it
});
// Effective operations: ["read"]Precedence
Operations are intersected at three levels:
- Project level — configured in dashboard Settings tab
- API key level — set when generating the key (optional)
- Request level — passed via SDK
operationsconfig
Each level can only restrict further, never expand access.
Security
Connection string safety
Your database connection string is never stored on SyncAgent servers. It is passed from your app to our API at runtime, used to process the current request, and immediately discarded.
⚠️Never put your connection string in client-side code (browser JavaScript). Use environment variables and pass it from your server.
In Next.js, use a server component or API route:
// ✅ CORRECT — server component passes connection string
// app/dashboard/page.tsx (server component)
import { SyncAgentChat } from "@syncagent/react";
export default function DashboardPage() {
return (
<SyncAgentChat
config={{
apiKey: process.env.NEXT_PUBLIC_SYNCAGENT_KEY, // public key is fine
connectionString: process.env.DATABASE_URL, // server-only env var
}}
/>
);
}
// ❌ WRONG — never do this in a client component
// "use client"
// const conn = process.env.NEXT_PUBLIC_DATABASE_URL; // exposed to browser!API key security
- API keys are hashed with bcrypt — we never store the raw key
- Only the first 12 characters (prefix) are stored for lookup
- Rotate keys anytime from the project dashboard
- You cannot revoke the last key — always keep at least one active
- Keys have no expiry by default — rotate them periodically
Rate limiting
Two layers of rate limiting protect your project:
- Monthly limit — enforced per plan (100 free, 5k starter, 50k pro)
- Per-second limit — max 5 concurrent requests per API key per 10 seconds
When limits are hit, the API returns HTTP 429 with a clear error message and your current usage.
Webhooks for usage alerts
Configure webhooks in your project to receive alerts when usage hits 50%, 80%, or 100% of your monthly limit. Webhooks are signed with HMAC-SHA256.
// Verify webhook signature in your endpoint
import crypto from "crypto";
app.post("/webhooks/syncagent", (req, res) => {
const signature = req.headers["x-syncagent-signature"];
const body = JSON.stringify(req.body);
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(body)
.digest("hex");
if (signature !== expected) {
return res.status(401).send("Invalid signature");
}
const { event, data } = req.body;
// event: "usage.50" | "usage.80" | "usage.100"
console.log(`Usage alert: ${data.percentage}% (${data.used}/${data.limit})`);
res.json({ received: true });
});Plans & Limits
| Plan | Requests/Month | Collections | Price |
|---|---|---|---|
| Free (+ 14-day trial) | 100 (500 during trial) | 5 | GH₵0 |
| Starter | 5,000 | 20 | GH₵150/mo |
| Pro | 50,000 | Unlimited | GH₵500/mo |
| Enterprise | Unlimited | Unlimited | Custom |
💡Every new project gets a 14-day trial with 500 free requests — no credit card required. After the trial, the project moves to the Free plan (100 requests/month).
What counts as a request?
Each message sent to the AI agent counts as one request. Schema discovery and API key generation do not count. A single user message may trigger multiple DB tool calls internally — that still counts as one request.
Collection limits
The collection limit applies to the number of collections/tables the agent can see in your database. If your database has more collections than your plan allows, the agent will only see the first N (sorted alphabetically). Use the Allowed Collections setting to control which ones are visible.
Database Connection Strings
mongodb+srv://user:pass@cluster.mongodb.net/mydb
mongodb://localhost:27017/mydbAlways include the database name at the end.
postgresql://user:pass@host:5432/mydb
postgres://user:pass@host/mydb?sslmode=requireWorks with Neon, Railway, Render, Supabase direct, AWS RDS.
mysql://user:pass@host:3306/mydb
mysql://user:pass@host/mydb?ssl=trueWorks with PlanetScale, Railway, AWS RDS, Google Cloud SQL.
/absolute/path/to/database.sqlite
file:./relative/path/db.sqliteUse absolute paths in production. Relative paths resolve from the server working directory.
Server=host,1433;Database=mydb;User Id=user;Password=pass;Encrypt=true;
Server=host;Database=mydb;Trusted_Connection=true;Works with Azure SQL, AWS RDS SQL Server, on-premise SQL Server 2016+.
https://your-project.supabase.co|your-anon-key
https://your-project.supabase.co|your-service-role-keyUse anon key for user-scoped access, service role key for admin access. Separated by |.