@syncagent/angular
Angular SDK for SyncAgent — injectable service with Angular Signals and RxJS observables for AI database chat.
Works with MongoDB, PostgreSQL, MySQL, SQLite, SQL Server, and Supabase.

Get Your API Key
- Sign up for a free account
- Go to your Dashboard → New Project → choose your database type
- Copy your API key (starts with
sa_)
Every new project gets a 14-day trial with 500 free requests — no credit card required. After the trial, you get 100 free requests/month on the Free plan.
Install
npm install @syncagent/angular @syncagent/js
Quick Start
import { Component, OnInit } from "@angular/core";
import { SyncAgentService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-root",
providers: [SyncAgentService],
template: `
<div class="chat">
<div *ngIf="agent.status() as s" class="status">⏳ {{ s.label }}</div>
<div class="messages">
<div *ngFor="let msg of agent.messages()" [class]="'message ' + msg.role">
{{ msg.content }}
</div>
</div>
<div *ngIf="agent.error() as err" class="error">⚠️ {{ err.message }}</div>
<div class="input-row">
<input [(ngModel)]="input" (keydown.enter)="send()" placeholder="Ask about your data..." [disabled]="agent.isLoading()" />
<button (click)="send()" [disabled]="agent.isLoading() || !input.trim()">Send</button>
<button *ngIf="agent.isLoading()" (click)="agent.stop()">Stop</button>
<button (click)="agent.reset()">Clear</button>
</div>
</div>
`,
})
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 = "";
}
}
Environment Setup
export const environment = {
production: false,
syncagentKey: "sa_your_api_key",
databaseUrl: "mongodb+srv://user:pass@cluster/db",
};
⚠️ Security note: In production, pass the connection string from your backend API instead of bundling it in the client.
Multi-tenant SaaS
@Component({ providers: [SyncAgentService] })
export class DashboardComponent implements OnInit {
constructor(
public agent: SyncAgentService,
private authService: AuthService,
) {}
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
import { Subject, takeUntil, filter } from "rxjs";
@Component({ providers: [SyncAgentService] })
export class ChatComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(public agent: SyncAgentService) {}
ngOnInit() {
this.agent.configure({ apiKey: "...", connectionString: "..." });
this.agent.messages$
.pipe(takeUntil(this.destroy$))
.subscribe(messages => console.log("Messages:", messages.length));
this.agent.message$
.pipe(takeUntil(this.destroy$))
.subscribe(msg => console.log("New:", msg.content.slice(0, 50)));
this.agent.status$
.pipe(takeUntil(this.destroy$), filter(Boolean))
.subscribe(({ step, label }) => console.log(`[${step}] ${label}`));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Custom Tools
this.agent.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
tools: {
sendEmail: {
description: "Send an email to a user",
inputSchema: {
to: { type: "string", description: "Recipient email" },
subject: { type: "string", description: "Subject line" },
body: { type: "string", description: "Email body" },
},
execute: async ({ to, subject, body }) => {
await this.emailService.send({ to, subject, body });
return { sent: true };
},
},
},
onData: (data) => {
if (data.collection === "orders") {
this.orders = data.data;
}
},
});
Tools-only Mode
Use toolsOnly: true when you want the agent to only call your custom tools — no database access:
this.agent.configure({
apiKey: environment.syncagentKey,
toolsOnly: true,
tools: {
searchProducts: {
description: "Search products by name",
inputSchema: { query: { type: "string", description: "Search query" } },
execute: async ({ query }) => {
const res = await fetch(`/api/products?q=${query}`);
return res.json();
},
},
createTicket: {
description: "Create a support ticket",
inputSchema: {
title: { type: "string" },
message: { type: "string" },
},
execute: async ({ title, message }) => {
return await this.ticketService.create({ title, message });
},
},
},
});
Customer Agent Mode
The CustomerChatService is a dedicated Angular service for building customer-facing support interfaces. It routes messages through SyncAgent's customer support pipeline — using your configured persona, flows, knowledge base, and escalation rules — instead of direct database access.
Provide it at the component or module level and call configure() before sending messages.
import { Component, OnInit } from "@angular/core";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-support-widget",
providers: [CustomerChatService],
template: `
<div class="chat">
@if (chat.welcomeMessage()) {
<p class="welcome">{{ chat.welcomeMessage() }}</p>
}
<div class="messages">
@for (msg of chat.messages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
@if (chat.isEscalated()) {
<p class="notice">You've been connected to a human agent.</p>
}
@if (chat.error() as err) {
<p class="error">{{ err.message }}</p>
}
<div class="input-row">
<input
[(ngModel)]="input"
(keydown.enter)="send()"
placeholder="How can we help?"
[disabled]="chat.isLoading()"
/>
<button (click)="send()" [disabled]="chat.isLoading() || !input.trim()">Send</button>
</div>
@if (chat.isResolved()) {
<div class="rating">
<p>How was your experience?</p>
<button *ngFor="let r of [1,2,3,4,5]" (click)="rate(r)">{{ r }}⭐</button>
</div>
}
</div>
`,
})
export class SupportWidgetComponent implements OnInit {
input = "";
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "user_abc123",
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Conversation resolved:", id),
});
}
send() {
if (!this.input.trim()) return;
this.chat.sendMessage(this.input);
this.input = "";
}
rate(rating: number) {
this.chat.rateConversation(rating);
}
}
Angular Signals
messages() | Message[] | Conversation message history |
conversationId() | string | null | Current conversation ID |
isLoading() | boolean | True while waiting for a response |
isEscalated() | boolean | True when escalated to a human agent |
isResolved() | boolean | True when the conversation is resolved |
error() | Error | null | Last error, if any |
welcomeMessage() | string | null | Welcome message (first interaction only) |
isConfigured() | boolean | True after configure() has been called |
RxJS Observables
messages$ | Observable<Message[]> | Conversation history stream |
isLoading$ | Observable<boolean> | Loading state stream |
error$ | Observable<Error | null> | Error stream |
escalated$ | Observable<void> | Emits when escalated to a human agent |
resolved$ | Observable<string> | Emits the conversation ID when resolved |
CustomerChatServiceConfig
Extends SyncAgentConfig with customer-mode-specific options:
apiKey | string | Your SyncAgent API key |
connectionString | string | Database connection string |
customerMode | true | Enables customer agent mode |
externalUserId | string | Unique identifier for the end-user |
onEscalated | () => void | Optional callback when escalated to a human agent |
onResolved | (conversationId: string) => void | Optional callback when the conversation is resolved |
Methods
configure(config) | configure(config: CustomerChatServiceConfig): void | Initialize the service with customer mode config. Must be called before other methods. |
sendMessage(content, metadata?) | sendMessage(content: string, metadata?: Record<string, any>): Promise<void> | Send a message and receive an AI response. Ignored if content is empty or already loading. |
rateConversation(rating) | rateConversation(rating: number): Promise<void> | Rate the current conversation (1-5). Throws if no active conversation. |
reset() | reset(): void | Clear all state (messages, conversation ID, errors) and start fresh. |
Guest Identification
When no externalUserId is provided, the CustomerChatService activates the guest identification flow. Guests must identify themselves before sending messages. The service exposes Angular signals and RxJS observables for reactive state management.
Signals & Observables
isIdentified() | Signal<boolean> | true when the guest has been identified (via form or stored identity) |
guestIdentity() | Signal<GuestIdentity | null> | The current guest identity, or null if not yet identified |
isIdentified$ | Observable<boolean> | Emits the current identification state (BehaviorSubject) |
guestIdentified$ | Observable<GuestIdentity> | Emits the GuestIdentity when identification completes |
identifyGuest(data)
name | string | Yes | Guest's display name |
email | string | Yes | Guest's email address |
phone | string | No | Guest's phone number |
Validates input, generates a deterministic guest identifier from the email, persists the identity to localStorage, updates signals/observables, and invokes the onGuestIdentified callback if configured.
Template Usage with Signals
import { Component, OnInit } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-support-widget",
providers: [CustomerChatService],
imports: [FormsModule],
template: `
@if (!chat.isIdentified()) {
<form (ngSubmit)="identify()">
<h2>Welcome</h2>
<p>Please introduce yourself to get started</p>
<label for="name">Name</label>
<input id="name" [(ngModel)]="name" name="name" required />
<label for="email">Email</label>
<input id="email" [(ngModel)]="email" name="email" type="email" required />
<label for="phone">Phone (optional)</label>
<input id="phone" [(ngModel)]="phone" name="phone" type="tel" />
@if (chat.error() as err) {
<p class="error">{{ err.message }}</p>
}
<button type="submit">Start Chat</button>
</form>
} @else {
<div class="chat">
<div class="messages">
@for (msg of chat.messages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
<div class="input-row">
<input
[(ngModel)]="input"
(keydown.enter)="send()"
placeholder="How can we help?"
[disabled]="chat.isLoading()"
/>
<button (click)="send()" [disabled]="chat.isLoading() || !input.trim()">Send</button>
</div>
</div>
}
`,
})
export class SupportWidgetComponent implements OnInit {
name = "";
email = "";
phone = "";
input = "";
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
onGuestIdentified: (identity) => {
console.log("Guest identified:", identity.guestId);
},
});
}
identify() {
this.chat.identifyGuest({
name: this.name,
email: this.email,
phone: this.phone || undefined,
});
}
send() {
if (!this.input.trim()) return;
this.chat.sendMessage(this.input);
this.input = "";
}
}
Reactive Handling with guestIdentified$
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-guest-chat",
providers: [CustomerChatService],
template: `<!-- ... -->`,
})
export class GuestChatComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(public chat: CustomerChatService) {}
ngOnInit() {
this.chat.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
});
this.chat.guestIdentified$
.pipe(takeUntil(this.destroy$))
.subscribe((identity) => {
console.log(`Guest ${identity.name} identified as ${identity.guestId}`);
});
this.chat.isIdentified$
.pipe(takeUntil(this.destroy$))
.subscribe((identified) => {
console.log("Identification state:", identified);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Dual Mode (Database + Customer Agent)
⚠️ Deprecated: createDual() will be removed in a future major version. Use DualChatService instead — pass externalUserId directly to enable both modes on a single instance.
Legacy pattern (deprecated):
import { Component, OnInit } from "@angular/core";
import { SyncAgentClient } from "@syncagent/js";
import { SyncAgentService, CustomerChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-dual-chat",
providers: [SyncAgentService, CustomerChatService],
template: `<!-- admin + customer UIs -->`,
})
export class DualChatComponent implements OnInit {
constructor(
public admin: SyncAgentService,
public support: CustomerChatService,
) {}
ngOnInit() {
const { db, support } = SyncAgentClient.createDual({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123",
});
this.admin.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
});
this.support.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
customerMode: true,
externalUserId: "customer_123",
});
}
}
Before (deprecated):
const { db, support } = SyncAgentClient.createDual({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123",
});
After (recommended):
import { DualChatService } from "@syncagent/angular";
@Component({
providers: [DualChatService],
})
export class DualChatComponent implements OnInit {
constructor(public dual: DualChatService) {}
ngOnInit() {
this.dual.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "customer_123",
});
}
}
Tip: In Angular, you can also use createDual() to get both client instances and pass them to your services' internal logic, or simply configure each service independently as shown above.
Unified Dual Mode
The DualChatService provides a single injectable service that manages both database agent and customer support chat from one component. Instead of wiring up SyncAgentService and CustomerChatService separately, inject DualChatService and call configure() with your config including externalUserId — both modes become available immediately.
import { Component, OnInit } from "@angular/core";
import { DualChatService } from "@syncagent/angular";
import { environment } from "./environments/environment";
@Component({
selector: "app-dual-chat",
providers: [DualChatService],
template: `
<div class="dual-chat">
<section class="db-panel">
<h3>Database Agent</h3>
<div class="messages">
@for (msg of dual.dbMessages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
@if (dual.dbError() as err) {
<p class="error">{{ err.message }}</p>
}
<div class="input-row">
<input
[(ngModel)]="dbInput"
(keydown.enter)="sendDb()"
placeholder="Ask about your data..."
[disabled]="dual.dbIsLoading()"
/>
<button (click)="sendDb()" [disabled]="dual.dbIsLoading() || !dbInput.trim()">Send</button>
</div>
</section>
<section class="support-panel">
<h3>Customer Support</h3>
<div class="messages">
@for (msg of dual.supportMessages(); track msg.content) {
<div [class]="'message ' + msg.role">{{ msg.content }}</div>
}
</div>
@if (dual.supportIsEscalated()) {
<p class="notice">Connected to a human agent.</p>
}
@if (dual.supportError() as err) {
<p class="error">{{ err.message }}</p>
}
<div class="input-row">
<input
[(ngModel)]="supportInput"
(keydown.enter)="sendSupport()"
placeholder="How can we help?"
[disabled]="dual.supportIsLoading()"
/>
<button (click)="sendSupport()" [disabled]="dual.supportIsLoading() || !supportInput.trim()">Send</button>
</div>
</section>
</div>
`,
})
export class DualChatComponent implements OnInit {
dbInput = "";
supportInput = "";
constructor(public dual: DualChatService) {}
ngOnInit() {
this.dual.configure({
apiKey: environment.syncagentKey,
connectionString: environment.databaseUrl,
externalUserId: "user_abc123",
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log("Resolved:", id),
});
}
sendDb() {
if (!this.dbInput.trim()) return;
this.dual.sendDbMessage(this.dbInput);
this.dbInput = "";
}
sendSupport() {
if (!this.supportInput.trim()) return;
this.dual.sendSupportMessage(this.supportInput);
this.supportInput = "";
}
}
Error Handling
If sendDbMessage(), sendSupportMessage(), stop(), or reset() is called before configure(), the service throws:
DualChatService: call configure() first
If sendSupportMessage() is called without externalUserId in the config, the service throws:
DualChatService: externalUserId is required for customer chat
Angular Signals
dbMessages() | Message[] | Database agent conversation message history |
dbIsLoading() | boolean | True while the database agent is processing |
dbError() | Error | null | Last error from the database agent, if any |
supportMessages() | Message[] | Customer support conversation message history |
supportIsLoading() | boolean | True while the support agent is processing |
supportError() | Error | null | Last error from the support agent, if any |
supportConversationId() | string | null | Current support conversation ID |
supportIsEscalated() | boolean | True when escalated to a human agent |
supportIsResolved() | boolean | True when the support conversation is resolved |
RxJS Observables
dbMessages$ | Observable<Message[]> | Database agent message history stream |
dbIsLoading$ | Observable<boolean> | Database agent loading state stream |
dbError$ | Observable<Error | null> | Database agent error stream |
supportMessages$ | Observable<Message[]> | Support agent message history stream |
supportIsLoading$ | Observable<boolean> | Support agent loading state stream |
supportError$ | Observable<Error | null> | Support agent error stream |
DualChatServiceConfig
apiKey | string | Your SyncAgent API key |
connectionString | string | Database connection string |
externalUserId | string | Unique identifier for the end-user (enables customer mode) |
context | Record<string, any> | Optional context passed to both agents |
onData | (data: ToolData) => void | Optional callback when the database agent returns query results |
onEscalated | () => void | Optional callback when the support conversation is escalated to a human agent |
onResolved | (conversationId: string) => void | Optional callback when the support conversation is resolved |
Methods
configure(config) | configure(config: DualChatServiceConfig): void | Initialize the service with dual-mode config. Must be called before other methods. |
sendDbMessage(content) | sendDbMessage(content: string): Promise<void> | Send a message to the database agent and receive a streamed response. |
sendSupportMessage(content, metadata?) | sendSupportMessage(content: string, metadata?: Record<string, any>): Promise<void> | Send a message to the customer support agent. Requires externalUserId in config. |
stop() | stop(): void | Abort any in-progress streams for both agents. |
reset() | reset(): void | Clear all state (messages, errors, conversation) for both agents and start fresh. |
API Reference
SyncAgentService
Methods:
configure(config) | Initialize with API key, connection string, and options |
sendMessage(content) | Send a message and start streaming |
stop() | Abort the current stream |
reset() | Clear all messages |
Angular Signals:
messages() | Message[] | Conversation history |
isLoading() | boolean | True while streaming |
error() | Error | null | Last error |
status() | {step,label} | null | Live status (connecting, querying, thinking, done) |
lastData() | ToolData | null | Last DB query result |
RxJS Observables:
messages$ | Observable<Message[]> | Conversation history stream |
isLoading$ | Observable<boolean> | Loading state stream |
error$ | Observable<Error|null> | Error stream |
status$ | Observable<{step,label}|null> | Status stream |
message$ | Observable<Message> | Emits each new assistant message |
Supported Databases
| MongoDB | mongodb+srv://user:pass@cluster.mongodb.net/mydb |
| PostgreSQL | postgresql://user:pass@host:5432/mydb |
| MySQL | mysql://user:pass@host:3306/mydb |
| SQLite | /absolute/path/to/database.sqlite |
| SQL Server | Server=host,1433;Database=mydb;User Id=user;Password=pass;Encrypt=true; |
| Supabase | https://xxx.supabase.co|your-anon-key |
Security
- Your database connection string is never stored on SyncAgent servers
- Passed at runtime, used once, immediately discarded
- API keys are hashed with bcrypt — raw keys are never stored
Plans & Pricing
| Free (+ 14-day trial) | 100 (500 during trial) | 5 | GH₵0 |
| Starter | 5,000 | 20 | GH₵150/mo |
| Pro | 50,000 | Unlimited | GH₵500/mo |
| Enterprise | Unlimited | Unlimited | Custom |
View full pricing →
Resources
License
MIT