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.

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,
        // 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
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

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/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.

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

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

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

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.

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.

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