# Igniter.js - Complete Documentation
The first AI-native TypeScript framework — architected for human-AI collaboration. Feature-sliced modules, deep TypeScript inference, and built-in training for 15+ Code Agents (Cursor, Claude Code, Copilot) create a low-entropy environment where both developers and AI work seamlessly. MCP-native APIs, AI agent orchestration, type-safe RPC, background jobs, multi-platform bots, and framework-agnostic runtime support — built for the next era of development.
---
# Blog
Articles, tutorials, and announcements from the Igniter.js team
---
# Announcing Igniter.js MCP Server: Native AI Integration for Modern Development
> Transform your AI coding tools into framework experts with Igniter.js's MCP Server integration. Get instant project understanding, intelligent code generation, and specialized AI assistance built directly into Cursor, Claude Code, and more.
URL: https://igniterjs.com/blog/announcing-igniter-mcp-server
As AI-native development tools reshape how we build software, understanding project context and maintaining architectural consistency has become the new bottleneck. With **Igniter.js MCP Server**, your AI coding assistants can now deeply understand your project structure, execute development commands, and provide intelligent assistance that goes far beyond basic code completion.
## The Evolution of AI-Assisted Development
The rise of AI-native IDEs like Cursor, Windsurf, and Claude Code has enabled developers to write code at unprecedented speeds. But with this velocity comes a new challenge: **AI tools need context to be truly effective**.
Without proper context, even the most advanced AI assistants struggle to:
* Understand your project's architectural patterns
* Follow your specific coding conventions
* Execute project-specific commands correctly
* Maintain consistency across feature implementations
This is where the **Model Context Protocol (MCP)** becomes transformative.
## What is MCP and Why Does It Matter?
The [Model Context Protocol](https://modelcontextprotocol.io/introduction) is an open standard that enables Large Language Models to interact with external systems in a structured, type-safe manner. Think of it as a universal API that AI tools can use to:
* Access your project's file structure and codebase
* Execute development tasks and build commands
* Query project-specific information
* Leverage specialized tools designed for your framework
For Igniter.js, this means your AI assistant becomes a framework expert that understands not just TypeScript, but the specific patterns, conventions, and architecture of your Igniter.js application.
## Getting Started: Install in Under 2 Minutes
Every Igniter.js starter template (v1.0.0+) comes with MCP Server support out of the box. Integration is as simple as adding a configuration file to your AI tool.
The Igniter.js MCP Server works with **any** AI coding tool that supports the Model Context Protocol, including Cursor, Windsurf, Claude Code, Claude Desktop, and VS Code Copilot.
### Quick Installation
Create or edit `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"Igniter": {
"command": "npx",
"args": ["@igniter-js/mcp-server"]
}
}
}
```
Restart Cursor to activate the MCP Server.
```bash
# Add the Igniter.js MCP Server
claude mcp add igniter npx @igniter-js/mcp-server
# Verify installation
claude mcp list
```
Create `.vscode/mcp.json` in your project root:
```json
{
"servers": {
"Igniter": {
"command": "npx",
"args": ["@igniter-js/mcp-server"]
}
}
}
```
Create or edit `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"Igniter": {
"command": "npx",
"args": ["@igniter-js/mcp-server"]
}
}
}
```
## What Can the MCP Server Do?
The Igniter.js MCP Server exposes a comprehensive set of tools that transform your AI assistant into a specialized Igniter.js developer:
### Development Workflow Tools
### Start Development Servers
Launch your application with live reloading and automatic client generation.
```typescript
// AI can execute: start the dev server
// Runs: igniter dev --watch
```
### Build for Production
Compile and optimize your application for deployment.
```typescript
// AI can execute: build the project
// Runs: igniter build
```
### Run Test Suites
Execute comprehensive tests with filtering and watch options.
```typescript
// AI can execute: run tests for the auth feature
// Runs: npm test -- --filter auth
```
### Code Analysis and Generation
* **Analyze Code Structure**: Identify architectural patterns and potential issues
* **Generate Type-Safe Clients**: Auto-generate fully typed API clients from your router
* **Create OpenAPI Documentation**: Generate interactive API documentation with Scalar UI
* **Scaffold Features**: Create complete feature modules from your Prisma schema
### AI-Powered Task Management
The MCP Server integrates with specialized AI agents (Claude, Gemini, GPT, Perplexity) to handle complex development tasks:
```typescript
// Ask your AI: "Create a new authentication feature with JWT tokens"
// The AI delegates to specialized agents who:
// 1. Analyze your current architecture
// 2. Generate the auth controller and procedures
// 3. Create database migrations
// 4. Write comprehensive tests
// 5. Update documentation
```
## Meet Lia: Your AI Framework Specialist
When you use an Igniter.js starter template, you're not just getting a codebase—you're getting **Lia**, a specialized AI agent trained specifically for Igniter.js development.
### How Lia Transforms Your AI Assistant
**Before MCP Integration:**
Your AI: "I see you have a Next.js project. Let me try to understand the structure..."
**After MCP Integration:**
Lia-Enhanced AI: "I can see this is an Igniter.js project with feature-based architecture. Your auth module is in `src/features/auth/`. I'll use the `generateController` tool to add the new endpoint following your established patterns."
### Lia's Capabilities
**Contextual Understanding**: Every action starts with comprehensive analysis of your project structure, conventions, and existing patterns.
**Tool Prioritization**: Instead of generic approaches, Lia automatically uses specialized Igniter.js tools for maximum accuracy and efficiency.
**Pattern Replication**: Learns from your existing code to maintain consistency across your entire application.
**Persistent Memory**: Stores architectural decisions and patterns, making future development faster and more consistent.
## Real-World Development Workflow
Here's a typical conversation with a Lia-enhanced AI assistant:
### You Ask for a Feature
**You:** "Add user profile management to the auth feature"
### Lia Analyzes Context
**AI:** "I'll analyze the current auth feature structure..."
*Automatically runs analysis on the auth directory*
### Intelligent Implementation
**AI:** "I can see you have a `user` controller with basic CRUD. I'll scaffold a `profile` controller following your patterns with Zod validation and the `ensure` plugin."
*Uses `generateController` tool*
### Testing and Validation
**AI:** "Now I'll test the implementation to ensure it integrates correctly..."
*Runs validation using MCP Server tools*
### Knowledge Persistence
**AI:** "Implementation complete! I've stored these patterns for future reference."
## Why This Matters for Your Workflow
### Before Igniter.js MCP Server
❌ AI guesses at project structure\
❌ Manual context switching between tools\
❌ Inconsistent code patterns\
❌ Repeated explanations of architecture\
❌ Generic solutions that don't fit your app
### After Igniter.js MCP Server
✅ AI deeply understands your project\
✅ Seamless tool integration\
✅ Consistent, pattern-aware code\
✅ Persistent architectural knowledge\
✅ Framework-specific, optimized solutions
## Advanced: Custom Rules for Maximum Productivity
Every Igniter.js starter includes custom rules that transform your AI assistant's behavior. These rules create a specialized development environment where:
* **File operations start with analysis** to understand context before making changes
* **Specialized tools are prioritized** over generic approaches
* **Architectural patterns are maintained** automatically across features
* **Development workflows are optimized** for Igniter.js best practices
The more you work with Lia-enhanced AI, the smarter it becomes. Every decision, pattern, and insight is stored, creating a continuously improving development assistant that understands your specific project better than any generic AI could.
## Start Building with AI-Native Development
Ready to experience the future of collaborative coding? Choose from our collection of official starter templates, each pre-configured with MCP Server support:
### Choose Your Stack
Select from Next.js, TanStack Start, Bun, Express, or Deno starters
### Install MCP Server
Add the configuration to your preferred AI tool (see tabs above)
### Start Conversing
Begin with simple questions like:
* "What's the current health status of my project?"
* "Show me all API endpoints"
* "Analyze the auth feature for potential issues"
## Integration Examples
### Creating a Complete Feature
```typescript
// You: "Create a posts feature with CRUD operations and real-time updates"
// Lia-enhanced AI automatically:
// 1. Analyzes your Prisma schema
// 2. Generates controller with type-safe actions
// 3. Creates validation schemas with Zod
// 4. Implements real-time SSE streaming
// 5. Generates API client with React hooks
// 6. Writes comprehensive tests
// 7. Updates documentation
```
### Debugging with Context
```typescript
// You: "Why is the user creation endpoint failing?"
// Lia-enhanced AI:
// 1. Analyzes the user controller
// 2. Checks validation schemas
// 3. Reviews database schema
// 4. Examines recent error logs
// 5. Identifies the issue: missing required field
// 6. Proposes and implements the fix
// 7. Runs tests to verify the solution
```
## Community and Next Steps
We're building the future of AI-native development, and we'd love to hear your experience!
* **Share Your Projects**: What are you building with Igniter.js and AI? Let us know on [Twitter](https://twitter.com/igniterjs)
* **Join the Discussion**: Connect with other developers in our [Discord community](https://discord.gg/igniterjs)
* **Contribute**: Help improve the MCP Server on [GitHub](https://github.com/felipebarcelospro/igniter-js)
## Resources
* [MCP Server Documentation](/docs/mcp-server)
* [Custom Rules Guide](/docs/ai-integration/custom-rules)
* [Starter Templates](/templates)
* [GitHub Repository](https://github.com/felipebarcelospro/igniter-js)
***
The world's most AI-native framework is ready for you. Start building smarter, faster, and with unprecedented collaboration between you and your AI coding assistant.
---
# Introducing Igniter.js v1: A Type-Safe TypeScript Framework, Built by AI Agents for AI Agents
> After 1.5 years of development, 4,000+ tests, and 13 production-ready packages — Igniter.js v1 is here. A fully type-safe TypeScript framework designed so AI coding agents produce consistent, scalable, and secure code from day one.
URL: https://igniterjs.com/blog/igniterjs-v1-launch
## Two Years Ago, I Saw the Problem Coming
It was late 2024. My developer community — 11,000+ subscribers of Vibe Dev — was hitting the same wall over and over. AI coding tools like Cursor and Copilot were getting shockingly good at generating code. But every agent built differently. One used Next.js with tRPC. Another spun up Express with handwritten routes. A third produced a NestJS monolith with half the types missing.
The result? Projects that worked on day one fell apart by week three. No consistency. No guardrails. No shared DNA between what one agent built and another maintained.
I asked myself: *what if, instead of forcing AI agents to learn yet another framework, I built a framework designed for them from the ground up?*
Igniter.js was born as a human-AI collaboration experiment. For 1.5 years, I worked alongside AI agents — the same ones my community was struggling with — to build a production-grade TypeScript framework. Every line of code, every architectural decision, every adapter was shaped by both human judgment and agent feedback. The framework was built *by* agents, *for* agents.
Today, I'm shipping v1.
## The Philosophy: Four Principles That Drive Every Decision
Igniter.js isn't just another framework. It's a bet on where software development is heading — a world where humans and AI build together, and the code needs to work for both.
**100% Type Safety, Zero Compromises.** When an AI agent generates code, types aren't a nice-to-have — they're the difference between a function that compiles and one that crashes at runtime. Every Igniter.js package exports fully typed APIs. No `any`. No escape hatches. No "trust me, it works."
**AGENTS.md in Every Package.** Documentation isn't an afterthought here. Every package ships an `AGENTS.md` file that AI coding tools can read directly from `node_modules`. An agent doesn't need to guess the API — it reads the file and knows exactly how to use `IgniterStore`, `IgniterJobs`, or `IgniterAgent` immediately. This is documentation as a first-class API for AI.
**Adapter Architecture — No Vendor Lock-In.** Swap Redis for SQLite. Exchange Express for Bun. Trade Discord for Telegram. Every integration point uses an adapter pattern so you're never trapped by yesterday's decisions.
**Built for Real-World Testing.** The framework ships with an MCP Server that AI agents can use to test endpoints, debug issues, and validate behavior directly inside the IDE — before marking any task complete.
## The Ecosystem: 13 Packages, One Coherent Vision
Each library solves a specific pain point in AI-assisted development. Here's the tour.
### @igniter-js/core — The Declarative Builder
Every AI agent builds API routes differently. Core gives you a single, type-safe builder that wires context, telemetry, store, plugins, and configuration into one coherent application shell.
```ts
import { Igniter } from "@igniter-js/core";
const app = Igniter.create()
.withContext<{ db: Database }>()
.withConfig({ basePATH: "/api/v1" })
.withStore(storeManager)
.withTelemetry(telemetryManager)
.addPlugin("auth", authPlugin)
.build();
```
Controllers, queries, and mutations are defined via dedicated factories — `createIgniterController`, `createIgniterQuery`, `createIgniterMutation` — so every route is fully typed from input to response.
### @igniter-js/store — Unified State, Any Backend
AI agents struggle with state management across requests. Store provides a unified Pub/Sub and caching API regardless of storage backend, with type-safe event schemas enforced at compile time.
```ts
import { IgniterStore, IgniterStoreRedisAdapter, IgniterStoreEvents } from "@igniter-js/store";
import { z } from "zod";
const UserEvents = IgniterStoreEvents.create("user")
.event("created", z.object({ userId: z.string(), email: z.string().email() }))
.event("deleted", z.object({ userId: z.string() }))
.build();
const store = IgniterStore.create()
.withAdapter(IgniterStoreRedisAdapter.create({ redis }))
.withService("my-api")
.addEvents(UserEvents)
.build();
await store.events.publish("user.created", { userId: "abc", email: "dev@igniter.dev" });
```
### @igniter-js/jobs — Async Processing That Doesn't Break Silently
Async processing is where AI-generated code often fails silently. Jobs gives you declarative, typed queue definitions with built-in telemetry, retry logic, and multi-tenant scope isolation.
```ts
import { IgniterJobs } from "@igniter-js/jobs";
import { IgniterJobsBullMQAdapter } from "@igniter-js/jobs/adapters";
const jobs = IgniterJobs.create()
.withAdapter(IgniterJobsBullMQAdapter.create({ connection }))
.withContext<{ db: Database }>(async () => ({ db: await getDb() }))
.job("sendWelcomeEmail", {
input: z.object({ userId: z.string() }),
handler: async ({ input, context }) => {
const user = await context.db.user.findUnique({ where: { id: input.userId } });
await mailer.send({ to: user.email, template: "welcome" });
},
})
.build();
await jobs.sendWelcomeEmail.dispatch({ userId: "abc" });
```
### @igniter-js/agents — Strongly-Typed AI Agent Framework
Building AI agents requires messy tool definitions. Agents gives you typed tool registration with auto-generated schemas, multi-agent orchestration, MCP integration, and persistent memory — all validated at compile time.
```ts
import { IgniterAgent, IgniterAgentTool, IgniterAgentToolset } from "@igniter-js/agents";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const searchDocs = IgniterAgentTool.create("searchDocs")
.withDescription("Search the project documentation")
.withInput(z.object({ query: z.string() }))
.withExecute(async ({ query }) => {
return await docsIndex.search(query);
})
.build();
const toolset = IgniterAgentToolset.create("utils")
.addTool(searchDocs)
.build();
const agent = IgniterAgent.create("assistant")
.withModel(openai("gpt-4"))
.addToolset(toolset)
.build();
const result = await agent.generate({ chatId: "chat_123", userId: "user_123", context: {} });
```
### @igniter-js/bot — One Command, Multiple Platforms
Building a bot for one platform is easy. For three? It's a nightmare. Bots unifies the API across Telegram, WhatsApp, and Discord with a single builder.
```ts
import { IgniterBot, telegram, discord, memoryStore } from "@igniter-js/bot";
const bot = IgniterBot.create()
.withHandle("@demo_bot")
.withSessionStore(memoryStore())
.addAdapters({
telegram: telegram({ token: process.env.TELEGRAM_TOKEN! }),
discord: discord({ token: process.env.DISCORD_TOKEN!, applicationId: process.env.DISCORD_APP_ID! }),
})
.addCommand("start", {
name: "start",
description: "Greets the user",
handle: async (ctx) => {
await ctx.reply("👋 Welcome to Igniter.js!");
},
})
.build();
await bot.start();
```
### @igniter-js/caller — Type-Safe HTTP Client, Schema-Driven
Frontend agents often craft malformed API calls. Caller generates fully-typed clients with built-in caching, retry logic, interceptors, and telemetry — all from schema definitions.
```ts
import { IgniterCaller } from "@igniter-js/caller";
const client = IgniterCaller.create()
.withBaseUrl("/api")
.withHeaders({ "Content-Type": "application/json" })
.build();
const user = await client.get("/users/abc").send(); // Fully typed response
```
### @igniter-js/cli — One Command to Start
```bash
npx create-igniter-app@latest my-app
```
The CLI scaffolds your project with starters for Next.js, Express, Bun, and TanStack Start — plus add-ons for auth, database, bots, jobs, store, telemetry, and more.
### @igniter-js/collections — Declarative Data Access with a Prisma-like API
AI agents write inconsistent data access patterns. Collections gives you a schema-first, typed query layer that treats files (Markdown, JSON) and key-value stores as structured databases.
```ts
import { IgniterCollections, IgniterCollectionModel } from "@igniter-js/collections";
import { NodeFsAdapter } from "@igniter-js/collections/adapters";
import { z } from "zod";
const Posts = IgniterCollectionModel.create("posts")
.withBasePath("content/posts")
.withSchema(z.object({ title: z.string(), published: z.boolean(), tags: z.array(z.string()) }))
.build();
const docs = IgniterCollections.create()
.withAdapter(new NodeFsAdapter())
.addCollection(Posts)
.build();
const published = await docs.posts.findMany({ where: { published: true } });
```
### @igniter-js/connectors — OAuth, Webhooks, and Third-Party Integrations
Every SaaS app needs external integrations. Connectors handles OAuth flows, webhook verification, and credential encryption with multi-tenant scope isolation — so agents don't have to reinvent the wheel.
```ts
import { IgniterConnector, IgniterConnectorManager } from "@igniter-js/connectors";
const slack = IgniterConnector.create()
.withConfig(z.object({ webhookUrl: z.string().url() }))
.addAction("notify", {
input: z.object({ message: z.string() }),
handler: async ({ input, config }) => {
await fetch(config.webhookUrl, { method: "POST", body: JSON.stringify({ text: input.message }) });
},
})
.build();
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.build();
const scoped = manager.scope("organization", "org_123");
await scoped.connect("slack", { webhookUrl: "https://hooks.slack.com/..." });
await scoped.action("slack", "notify").call({ message: "Deploy complete!" });
```
### @igniter-js/logger — Structured Logging, Enforced
AI agents produce terrible debug logs. Logger wraps Pino with a builder API, typed log levels, and built-in transports (console, file, HTTP) — making logs structured, searchable, and consistent across every Igniter.js package.
```ts
import { IgniterLogger, IgniterLogLevel } from "@igniter-js/logger";
const logger = IgniterLogger.create()
.withLevel(IgniterLogLevel.INFO)
.withAppName("my-api")
.withComponent("auth")
.build();
logger.info("User signed up", { userId: "abc", plan: "pro" });
```
### @igniter-js/mail — Typed Email Templates with React Email
Email is deceptively complex. Mail gives you a clean, typed API with React-based templates, provider abstraction (Resend, Postmark, SendGrid, SMTP), optional queue delivery, and full telemetry on every send.
```ts
import { IgniterMail } from "@igniter-js/mail";
const mail = IgniterMail.create()
.withFrom("hello@igniter.dev")
.withAdapter("resend", process.env.RESEND_API_KEY!)
.addTemplate("welcome", welcomeTemplate)
.build();
await mail.send({ to: "dev@igniter.dev", template: "welcome", data: { name: "Felipe" } });
```
### @igniter-js/telemetry — Auto-Instrumented Observability
AI agents rarely add proper observability. Telemetry gives you a typed event registry, session-based context propagation via AsyncLocalStorage, privacy-safe redaction, and 10 transport adapters (Logger, HTTP, OTLP, Sentry, Slack, Discord, Telegram, and more).
```ts
import { IgniterTelemetry, IgniterTelemetryEvents, LoggerTransportAdapter } from "@igniter-js/telemetry";
import { z } from "zod";
const JobsEvents = IgniterTelemetryEvents.namespace("igniter.jobs")
.event("job.completed", z.object({ "ctx.job.id": z.string(), "ctx.job.duration": z.number() }))
.build();
const telemetry = IgniterTelemetry.create()
.withService("my-api")
.withEnvironment(process.env.NODE_ENV!)
.addEvents(JobsEvents)
.addTransport(LoggerTransportAdapter.create({ logger: console }))
.withRedaction({ denylistKeys: ["password", "secret"] })
.build();
telemetry.emit("igniter.jobs.job.completed", {
attributes: { "ctx.job.id": "job_123", "ctx.job.duration": 142 },
});
```
### @igniter-js/mcp-server — AI Agents as First-Class Citizens
Agents need to read docs, test endpoints, and validate code. The MCP Server gives them a direct line to do this inside the IDE, turning Cursor or Claude Code into a framework expert. It exposes tools for CLI automation, API testing, documentation fetching, GitHub management, and more — all over STDIO.
```bash
npx @igniter-js/mcp-server
```
## What Comes Next
v1 is the foundation. Here's what's on the horizon:
* **Templates.** Production-ready starters for every stack — Next.js, Express, Bun, Deno.
* **Deeper AI Integration.** Tighter agent workflows, auto-generated AGENTS.md improvements, and MCP-powered testing loops.
* **Studio v2.** Interactive API playground built on Scalar, already in development.
Igniter.js is open source, MIT licensed, and built in Brazil for developers everywhere. It's the result of 1.5 years of betting that humans and AI can build better software together — not by treating agents as tools, but by treating them as teammates.
[Star the project on GitHub](https://github.com/felipebarcelospro/igniter-js) · [Read the docs](https://igniter.dev) · [Join the community](https://discord.gg/igniter)
***
**Ready to build?**
```bash
npx create-igniter-app@latest my-app
```
---
# Introducing Igniter.js: The Type-Safe Framework for Modern Full-Stack Development
> Discover how Igniter.js eliminates the friction of full-stack development with end-to-end type safety, real-time capabilities, and an exceptional developer experience. Built for the AI-assisted coding era.
URL: https://igniterjs.com/blog/introducing-igniter-js
The modern web development landscape presents a paradox: we have more powerful tools than ever, yet building full-stack applications remains unnecessarily complex. Type safety breaks at API boundaries, state synchronization requires intricate solutions, and backend infrastructure demands expertise in multiple systems.
**Igniter.js** solves these fundamental challenges by treating your entire application as a unified, type-safe system. Created by [Felipe Barcelos](https://github.com/felipebarcelospro), this framework emerges from years of real-world experience building scalable applications and recognizing critical gaps in existing solutions.
## The Full-Stack Development Problem
Modern applications typically involve juggling multiple technologies, each with its own paradigms:
### Type Safety Boundaries
Even with TypeScript on both frontend and backend, the communication layer—APIs, database queries, client-server interactions—often lacks proper type safety. This leads to:
❌ Runtime errors from mismatched types\
❌ Manual synchronization of types across codebases\
❌ Integration bugs discovered only in production\
❌ Time wasted debugging trivial type mismatches
### Complex State Management
Keeping client state synchronized with server data requires:
❌ Intricate caching strategies\
❌ Manual cache invalidation logic\
❌ Real-time update implementations\
❌ Optimistic update patterns\
❌ Error state handling
Each of these adds complexity and maintenance burden.
### Backend Infrastructure Maze
Building robust backend services means integrating:
❌ Database ORMs\
❌ Caching systems\
❌ Background job queues\
❌ Pub/sub messaging\
❌ Real-time WebSocket servers\
❌ Telemetry and monitoring
Each system requires separate configuration and rarely shares type information.
### Fragmented Developer Experience
Developers constantly switch between:
❌ Different mental models for frontend vs backend\
❌ Multiple toolchains and build processes\
❌ Separate debugging approaches\
❌ Disconnected testing strategies
This context-switching reduces productivity and increases cognitive load.
## Enter Igniter.js: A Unified Solution
Igniter.js addresses these challenges through a **unified, type-safe approach** that treats your entire application as a cohesive system.
Igniter.js is built on three principles:
1. **End-to-End Type Safety**: Every piece of data flowing through your application is type-safe, from database queries to UI components
2. **Developer Experience First**: Intuitive APIs, excellent tooling, and minimal boilerplate
3. **Unified Backend Architecture**: All backend services work together seamlessly with shared type safety
## Key Features
### 1. Type-Safe Controllers and Actions
Define your API with complete type safety and automatic validation:
```typescript
// features/users/controllers/users.controller.ts
import { igniter } from '@/igniter'
import { z } from 'zod'
export const userController = igniter.controller({
path: '/users',
actions: {
// Type-safe query with automatic validation
getUser: igniter.query({
path: '/:id',
query: z.object({
id: z.string().uuid()
}),
handler: async ({ request, response, context }) => {
const user = await context.db.user.findUnique({
where: { id: request.query.id }
})
if (!user) {
return response.notFound('User not found')
}
return response.success(user)
}
}),
// Type-safe mutation with validation
createUser: igniter.mutate({
path: '/',
method: 'POST',
body: z.object({
name: z.string().min(1),
email: z.string().email()
}),
handler: async ({ request, response, context }) => {
const user = await context.db.user.create({
data: request.body
})
return response.success({ user }, { status: 201 })
}
})
}
})
```
The beauty is on the client side—consuming this API is equally type-safe:
```tsx
'use client'
import { api } from '@/igniter.client'
function UserProfile({ userId }: { userId: string }) {
// Fully typed with automatic caching and revalidation
const userQuery = api.users.getUser.useQuery({
query: { id: userId }
})
const createUserMutation = api.users.createUser.useMutation({
onSuccess: (data) => {
console.log('User created:', data.user)
}
})
if (userQuery.isLoading) return
Loading...
if (userQuery.isError) return Error: {userQuery.error.message}
return (
{userQuery.data?.name}
{userQuery.data?.email}
)
}
```
Unlike some solutions, Igniter.js achieves this type safety through TypeScript's type inference—no build step required. Your IDE provides instant autocomplete and error checking.
### 2. Procedures: Reusable Type-Safe Middleware
Create powerful, composable middleware that extends your application context:
```typescript
// procedures/auth.procedure.ts
export const auth = igniter.procedure({
handler: async (options: { required: boolean }, { response, context }) => {
const user = await getCurrentUser(context.env.SECRET)
// If auth is required but there's no user, stop the request
if (options.required && !user) {
return response.unauthorized('Authentication required')
}
// The returned object merges into context
// Now context.auth.user is available in controllers!
return {
auth: { user }
}
}
})
// Usage in controller
export const userController = igniter.controller({
path: '/users',
actions: {
getCurrentUser: igniter.query({
path: '/me',
use: [auth({ required: true })], // TypeScript knows context.auth.user exists!
handler: async ({ context, response }) => {
const user = context.auth.user // Fully typed!
return response.success(user)
}
})
}
})
```
### 3. Real-Time Updates by Default
Igniter.js makes real-time synchronization trivial through automatic revalidation:
```typescript
// Backend: Regular query and mutation
export const postsController = igniter.controller({
path: '/posts',
actions: {
list: igniter.query({
path: '/',
stream: true, // Enable real-time streaming
handler: async ({ context, response }) => {
const posts = await context.db.post.findMany()
return response.success({ posts })
}
}),
create: igniter.mutate({
path: '/',
body: z.object({
title: z.string(),
content: z.string()
}),
handler: async ({ body, context, response }) => {
const newPost = await context.db.post.create({ data: body })
// Automatically triggers revalidation for all clients!
return response.created(newPost).revalidate(['posts.list'])
}
})
}
})
```
```tsx
// Frontend: Standard useQuery - automatically updates in real-time
function PostsList() {
const postsQuery = api.posts.list.useQuery()
return (
{postsQuery.data?.posts.map((post) => (
{post.title}
))}
)
}
// When ANY user creates a post, ALL users see it instantly!
// No WebSockets, no manual refetching, no additional complexity.
```
Igniter.js uses Server-Sent Events (SSE) under the hood, providing efficient server-to-client updates without the complexity of WebSocket infrastructure.
### 4. Background Jobs with Type Safety
Handle async processing with a powerful, type-safe job system:
```typescript
// Define jobs with type validation
export const jobs = igniter.jobs.merge({
emails: igniter.jobs.router({
jobs: {
sendWelcome: igniter.jobs.register({
name: 'sendWelcome',
input: z.object({
userId: z.string(),
email: z.string().email()
}),
handler: async ({ input }) => {
await sendEmail({
to: input.email,
template: 'welcome',
data: { userId: input.userId }
})
}
})
}
})
})
// Enqueue from anywhere
export const userController = igniter.controller({
path: '/users',
actions: {
create: igniter.mutate({
path: '/',
body: z.object({
name: z.string(),
email: z.string().email()
}),
handler: async ({ body, context, response }) => {
const user = await context.db.user.create({ data: body })
// Enqueue welcome email job with full type safety
await igniter.jobs.emails.enqueue({
task: 'sendWelcome',
input: {
userId: user.id,
email: user.email
}
})
return response.success({ user })
}
})
}
})
```
## Framework-Agnostic Architecture
Igniter.js integrates seamlessly with any JavaScript runtime and framework. Write once, deploy anywhere:
```typescript
// app/api/[...igniter]/route.ts
import { igniter } from '@/igniter'
export const { GET, POST, PUT, DELETE, PATCH } = igniter.nextjs()
```
```typescript
// server.ts
import express from 'express'
import { igniter } from './igniter'
const app = express()
app.use('/api', igniter.express())
app.listen(3000)
```
```typescript
// server.ts
import { igniter } from './igniter'
Bun.serve({
port: 3000,
fetch: igniter.fetch
})
```
```typescript
// worker.ts
import { igniter } from './igniter'
export default {
fetch: igniter.fetch
}
```
Use Igniter.js with React, Vue, Svelte, Angular, React Native, or any frontend framework. The type-safe client adapts to your chosen stack.
## Performance and Scalability
Built for production from day one:
### Performance Features
✅ **Zero Runtime Overhead**: Client code is fully tree-shakeable\
✅ **Minimal Bundle Size**: Only ship what you use\
✅ **Fast Cold Starts**: Optimized for serverless environments\
✅ **Efficient Serialization**: Optimized data transfer\
✅ **Edge-Ready**: Native support for edge runtimes
### Scalability Architecture
✅ **Horizontal Scaling**: Stateless design enables easy scaling\
✅ **Edge Deployment**: Works on Cloudflare Workers, Vercel Edge\
✅ **Background Processing**: Reliable job queues with Redis/BullMQ\
✅ **Real-time Streaming**: Efficient SSE for live data\
✅ **Intelligent Caching**: Built-in query caching with smart invalidation\
✅ **Database Agnostic**: Works with Prisma, Drizzle, or any ORM
## Getting Started
Create a new Igniter.js project in under 2 minutes:
### Initialize Project
```bash
npx @igniter-js/cli init my-app
```
Choose your preferred setup:
* Next.js + React
* TanStack Start
* Bun + React
* Express.js API
* Custom setup
### Install Dependencies
```bash
cd my-app
npm install
```
```bash
cd my-app
pnpm install
```
```bash
cd my-app
yarn install
```
```bash
cd my-app
bun install
```
### Start Development
```bash
npm run dev
```
Your app is now running at `http://localhost:3000` with full type safety!
## Comparison with Existing Solutions
### vs. tRPC
While tRPC provides excellent type safety for APIs, Igniter.js offers:
✅ Integrated job queues\
✅ Real-time updates out of the box\
✅ AI-powered development tools\
✅ More comprehensive DX features
### vs. Next.js
Next.js is a fantastic React framework, but doesn't provide:
✅ Backend abstractions\
✅ Type safety across the full stack\
✅ Built-in state management\
✅ Real-time capabilities
Igniter.js complements Next.js by providing the backend architecture.
### vs. Remix
Remix offers great full-stack capabilities but lacks:
✅ Framework-agnostic design\
✅ The same level of type safety\
✅ AI integration\
✅ Built-in job queues
### vs. T3 Stack
The T3 Stack combines excellent tools but requires:
❌ Significant configuration\
❌ Multiple separate integrations\
❌ Manual setup for queues and real-time
Igniter.js provides all features integrated out of the box.
## What's Next?
Explore the ecosystem:
* [Quick Start Guide](/docs/getting-started/quick-start)
* [Starter Templates](/templates)
* [Core Concepts](/docs/core-concepts)
* [API Reference](/docs/api-reference)
* [Real-Time Guide](/docs/advanced-features/real-time)
## Join the Community
* [GitHub Repository](https://github.com/felipebarcelospro/igniter-js)
* [Documentation](/docs)
* [Discord Community](https://discord.gg/igniterjs)
* [Twitter](https://twitter.com/igniterjs)
## Conclusion
Igniter.js represents a new paradigm in full-stack development where type safety, developer experience, and modern capabilities are fundamental design principles, not afterthoughts.
By eliminating traditional boundaries between frontend and backend, Igniter.js enables you to build more reliable, maintainable, and feature-rich applications with significantly less complexity.
Whether you're building a simple CRUD app, a complex enterprise system, or an AI-powered platform, Igniter.js provides the tools you need to focus on delivering value to your users.
Get started with Igniter.js today and experience the future of full-stack development. Choose a [starter template](/templates) and build your first feature in minutes.
***
*Igniter.js is created and maintained by [Felipe Barcelos](https://github.com/felipebarcelospro) and the open-source community. Thank you to all contributors shaping the future of full-stack development.*
---
# Introducing Igniter Studio: Your Interactive API Development Playground
> Stop switching between tools. Igniter Studio brings interactive API testing, real-time monitoring, and AI-powered insights directly into your development workflow. Built on Scalar, designed for the future.
URL: https://igniterjs.com/blog/introducing-igniter-studio
Modern API development shouldn't require juggling multiple applications. Yet developers constantly switch between their code editor, terminal, and separate API testing tools like Postman or Insomnia. This context-switching destroys focus and slows development.
**Igniter Studio** eliminates this friction by providing a beautiful, interactive API playground built directly into your Igniter.js application. But this is just the beginning—we're building toward a complete AI-powered command center for modern development.
## The Context-Switching Problem
A typical development workflow looks like this:
### Write Code
Make changes in your code editor
### Start Server
Switch to terminal, run development server
### Test API
Switch to Postman/Insomnia, manually configure request
### Check Logs
Switch back to terminal to view logs
### Debug Issue
Switch back to code editor
Each context switch breaks your flow and consumes mental energy. **What if all of this happened in one place?**
## Introducing Igniter Studio
Igniter Studio is an interactive API playground that lives alongside your application, automatically consuming your OpenAPI specification to provide a rich, type-aware testing interface.
Built on [Scalar API Reference](https://github.com/scalar/scalar), it offers a modern, beautiful UI that makes API exploration intuitive and enjoyable.
### Get Started in Seconds
Launch Igniter Studio by adding a single flag to your development command:
```bash
npx @igniter-js/cli dev --docs
```
That's it! Navigate to `/docs` on your local server and you'll see your entire API ready to explore.
Igniter Studio automatically detects your API structure from the generated OpenAPI specification. No manual setup, no configuration files—it just works.
## See It in Action
Watch how seamlessly Igniter Studio integrates into your development workflow:
VIDEO
## Current Features
### Interactive API Explorer
Browse your entire API structure with an intuitive, searchable interface. Each endpoint displays:
* **Complete type information** derived from your Zod schemas
* **Request/response examples** automatically generated from your types
* **Authentication requirements** clearly documented
* **Parameter descriptions** from your JSDoc comments
### Live API Testing
Test endpoints directly in the browser with:
### Smart Request Builder
Auto-complete for parameters, headers, and request bodies based on your API types
### Real-Time Responses
See responses instantly with syntax highlighting and formatting
### Request History
Access previous requests to quickly iterate on testing
### Code Generation
Export working code samples in multiple languages (cURL, JavaScript, Python, etc.)
### Automatic Documentation
Your API documentation stays in sync with your code because it's generated from the same source of truth:
```typescript
// Your controller automatically generates documentation
export const userController = igniter.controller({
path: '/users',
actions: {
getUser: igniter.query({
path: '/:id',
description: 'Retrieve a user by ID', // Shows in Studio
query: z.object({
id: z.string().uuid().describe('The user UUID')
}),
handler: async ({ request, response, context }) => {
// Implementation
}
})
}
})
```
This description and parameter documentation automatically appears in Igniter Studio—no separate documentation files to maintain.
## The Vision: Your AI-Powered Command Center
Igniter Studio today is powerful, but it's just the foundation. We're building toward a complete development command center that brings together everything you need to build, test, and monitor your application.
### Coming Soon: Real-Time Monitoring
Currently in development for a future release.
See your application's heartbeat in real-time:
* **Live Request Stream**: Watch API requests as they happen with detailed payload inspection
* **Performance Metrics**: Track response times, error rates, and throughput
* **Error Tracking**: Catch and diagnose issues the moment they occur
* **Request Replay**: Reproduce problematic requests for debugging
### Coming Soon: Queue & Job Management
Visual dashboards for background processing:
* **Job Queue Visualization**: See pending, active, and completed jobs
* **Job Status Monitoring**: Track progress of long-running tasks
* **Manual Job Control**: Retry failed jobs, cancel stuck operations
* **Performance Analytics**: Understand job execution patterns
### Coming Soon: Integrated Telemetry
Turn OpenTelemetry data into actionable insights:
* **Distributed Tracing**: Follow requests across services and operations
* **Performance Profiling**: Identify bottlenecks in your application
* **Custom Metrics**: Track business-specific KPIs
* **Alerting**: Get notified when metrics cross thresholds
### Coming Soon: Custom Dashboards
Create personalized views for your specific needs:
* **Business Metrics**: Track sign-ups, conversions, revenue
* **Technical Health**: Monitor system resources, dependencies
* **Team Dashboards**: Share important metrics across your team
* **Embedded Widgets**: Add dashboard panels to your application UI
## The Game-Changer: Integrated AI Assistant
The most transformative feature on our roadmap is **Lia**, your AI assistant, integrated directly into Igniter Studio.
### Natural Language Development
Imagine debugging and managing your application using conversational language:
### Ask Questions
**You:** "Lia, what was the payload of the last failed request to `/users`?"
**Lia:** "The last failed request at 14:32:15 had an invalid email format in the request body..."
### Execute Commands
**You:** "Lia, run the test suite for the auth feature."
**Lia:** "Running tests... 12 passed, 1 failed. The `login` test is failing due to..."
### Create Dashboards
**You:** "Lia, create a dashboard tracking API error rates over 24 hours."
**Lia:** "Dashboard created. I've added widgets for total errors, error rate trend, and top failing endpoints..."
### Get Guidance
**You:** "Lia, how do I add a new scope to authentication?"
**Lia:** "To add a new authentication scope, you'll need to modify your auth procedure. Here's how\..."
### Context-Aware Assistance
Unlike generic AI chatbots, Lia has **full context** of your application:
* Real-time access to your code structure
* Live monitoring data from your running application
* Complete request/response history
* Historical performance metrics
* Your specific architectural patterns
This creates an unprecedented development experience where your AI assistant truly understands your application's state and can provide precise, actionable guidance.
## Technical Architecture
Igniter Studio is designed to be lightweight and non-intrusive:
### Development Mode
In development, Studio runs alongside your application:
```typescript
// Automatically enabled with --docs flag
npx @igniter-js/cli dev --docs
// Accessible at http://localhost:3000/docs
```
### Production Deployment
For production, you have options:
Completely disable Studio in production:
```typescript
// igniter.config.ts
export default {
studio: {
enabled: process.env.NODE_ENV === 'development'
}
}
```
Enable Studio with authentication:
```typescript
// igniter.config.ts
export default {
studio: {
enabled: true,
auth: {
type: 'basic',
users: [{
username: process.env.STUDIO_USER,
password: process.env.STUDIO_PASS
}]
}
}
}
```
Deploy Studio as a separate instance:
```bash
# Deploy only Studio with read-only access
npx @igniter-js/cli studio --remote
```
## Integration with Development Workflow
Igniter Studio seamlessly fits into your existing workflow:
### Local Development
```bash
# Start dev server with Studio
npm run dev -- --docs
# Or configure it permanently
# package.json
{
"scripts": {
"dev": "igniter dev --docs"
}
}
```
### Team Collaboration
Share API documentation with your team by deploying Studio:
```bash
# Deploy Studio to a shareable URL
vercel deploy --only-studio
# Or use your existing deployment
# Studio is available at /docs on any environment
```
### CI/CD Integration
Validate API contracts in your pipeline:
```bash
# Generate OpenAPI spec for validation
igniter openapi > spec.json
# Compare with previous version
diff spec.json spec.previous.json
```
## Get Started Today
Igniter Studio is available now in all Igniter.js projects. Start exploring your API in a whole new way:
### Start Your Project
Use any Igniter.js starter template or existing project
### Launch Studio
```bash
npx @igniter-js/cli dev --docs
```
### Open in Browser
Navigate to `http://localhost:3000/docs`
### Start Exploring
Browse your API, test endpoints, and experience the future of API development
## Starter Templates
Every Igniter.js starter template comes with Studio pre-configured and ready to use:
* [Next.js Full-Stack Starter](/templates/starter-nextjs)
* [TanStack Start Starter](/templates/starter-tanstack-start)
* [Bun + React Starter](/templates/starter-bun-react-app)
* [Express API Starter](/templates/starter-express-rest-api)
* [Bun API Starter](/templates/starter-bun-rest-api)
* [Deno API Starter](/templates/starter-deno-rest-api)
## Share Your Feedback
Igniter Studio is evolving based on your needs. We'd love to hear:
* What features would make your workflow even better?
* What pain points does Studio solve for you?
* What should we prioritize on the roadmap?
Join the conversation:
* [GitHub Discussions](https://github.com/felipebarcelospro/igniter-js/discussions)
* [Discord Community](https://discord.gg/igniterjs)
* [Twitter](https://twitter.com/igniterjs)
## Resources
* [Studio Documentation](/docs/tools/studio)
* [OpenAPI Generation](/docs/advanced-features/openapi)
* [Deployment Guide](/docs/deployment)
* [Starter Templates](/templates)
***
The future of API development is here. Stop juggling tools and start building with Igniter Studio—your new development command center.
---
# Introducing Igniter.js Templates: Production-Ready Starters for Every Stack
> Skip the boilerplate and start building. Our curated collection of production-ready templates gives you the perfect foundation for Next.js, React, Express, Bun, Deno, and more—all with best practices baked in.
URL: https://igniterjs.com/blog/introducing-igniter-templates
Starting a new project shouldn't mean spending hours configuring build tools, setting up databases, implementing authentication, and establishing project structure. Yet that's exactly what developers face every time they begin a new application.
**Igniter.js Templates** eliminate this friction. Our curated collection of production-ready starter templates provides battle-tested foundations that follow industry best practices and leverage the full power of the Igniter.js ecosystem.
## The Problem: Setup Friction Kills Momentum
Every experienced developer knows the pain:
### Choose Your Stack
Research and decide on frameworks, libraries, and tools
### Configure Build Tools
Set up TypeScript, bundlers, linters, formatters
### Database Setup
Configure ORM, migrations, connection pooling
### Project Structure
Establish folder organization and naming conventions
### Development Environment
Docker compose, environment variables, local services
### Actually Start Coding
Finally begin building features (hours or days later)
By the time you're ready to write actual business logic, your initial excitement has often faded. **What if you could skip straight to building features?**
## The Solution: Production-Ready Templates
Igniter.js Templates are not toy examples or minimal boilerplates. They're comprehensive, production-grade starter projects that include:
✅ **Complete Type Safety**: End-to-end TypeScript with strict mode enabled\
✅ **Database Integration**: Prisma ORM pre-configured with PostgreSQL\
✅ **Modern Tooling**: Latest versions of all dependencies\
✅ **Docker Development**: One-command local environment setup\
✅ **Best Practices**: Established patterns for scaling\
✅ **Feature Examples**: Real implementations to learn from\
✅ **Testing Setup**: Vitest configured and ready\
✅ **CI/CD Ready**: GitHub Actions workflows included
## Template Collection
Our initial collection covers the most popular stacks and use cases:
### Full-Stack Applications
These templates include both frontend and backend, fully integrated with Igniter.js for end-to-end type safety.
#### Next.js Full-Stack App
The most comprehensive template, perfect for building modern web applications.
**Stack**: Next.js 15, React 19, Prisma, PostgreSQL, Redis, BullMQ, Tailwind CSS
**Best For**:
* SaaS applications
* Content-driven websites
* Admin dashboards
* Customer portals
**Key Features**:
* App Router with Server Components
* Real-time updates via SSE
* Background job processing
* Authentication ready
* shadcn/ui components
```bash
npx @igniter-js/cli init my-app --template nextjs
```
[View Template Details](/templates/starter-nextjs)
***
#### TanStack Start Full-Stack App
Modern React framework with powerful file-based routing and data fetching.
**Stack**: TanStack Start, React 19, Prisma, PostgreSQL, Tailwind CSS
**Best For**:
* SPAs with complex routing
* Apps requiring fine-grained data loading
* Projects prioritizing performance
**Key Features**:
* File-based routing
* Automatic code splitting
* Optimized data loading
* Type-safe navigation
```bash
npx @igniter-js/cli init my-app --template tanstack-start
```
[View Template Details](/templates/starter-tanstack-start)
***
#### Bun + React SPA
Lightning-fast development with Bun's revolutionary runtime.
**Stack**: Bun, React 18, Prisma, PostgreSQL, Tailwind CSS
**Best For**:
* Single-page applications
* Projects prioritizing speed
* Node.js alternative seekers
**Key Features**:
* Instant hot reload with Bun
* Native TypeScript support
* Minimal configuration
* 3x faster install times
```bash
npx @igniter-js/cli init my-app --template bun-react
```
[View Template Details](/templates/starter-bun-react-app)
### Backend APIs
Build APIs that work with any frontend framework—React, Vue, Svelte, Angular, or mobile applications.
#### Express REST API
The classic Node.js framework, enhanced with Igniter.js type safety.
**Stack**: Express.js, TypeScript, Prisma, PostgreSQL
**Best For**:
* Traditional REST APIs
* Microservices
* Teams familiar with Express
* Projects requiring Express middleware
**Key Features**:
* Express ecosystem compatibility
* Middleware support
* Well-documented patterns
* Easy deployment
```bash
npx @igniter-js/cli init my-api --template express-api
```
[View Template Details](/templates/starter-express-rest-api)
***
#### Bun REST API
Modern backend powered by Bun's performance-first runtime.
**Stack**: Bun, TypeScript, Prisma, PostgreSQL
**Best For**:
* High-performance APIs
* WebSocket-heavy applications
* Modern backend architectures
**Key Features**:
* Native WebSocket support
* Built-in testing framework
* Fast cold starts
* Minimal dependencies
```bash
npx @igniter-js/cli init my-api --template bun-api
```
[View Template Details](/templates/starter-bun-rest-api)
***
#### Deno REST API
Secure-by-default runtime with modern JavaScript features.
**Stack**: Deno, TypeScript, Prisma, PostgreSQL
**Best For**:
* Security-focused applications
* Edge deployments
* Teams wanting modern defaults
**Key Features**:
* Secure by default
* Native TypeScript
* Standard library included
* Deno Deploy ready
```bash
npx @igniter-js/cli init my-api --template deno-api
```
[View Template Details](/templates/starter-deno-rest-api)
## Consistent Architecture Across Templates
Every template follows the same proven architecture, making it easy to switch between stacks or apply patterns across projects:
### Feature-Based Organization
All templates use a scalable, feature-based folder structure:
### Shared Conventions
**Controller Pattern**: Type-safe API endpoints with automatic validation
```typescript
export const userController = igniter.controller({
path: '/users',
actions: {
list: igniter.query({
path: '/',
handler: async ({ context, response }) => {
const users = await context.db.user.findMany()
return response.success({ users })
}
})
}
})
```
**Procedure Pattern**: Reusable business logic and middleware
```typescript
export const auth = igniter.procedure({
handler: async (options, { context, response }) => {
const user = await getCurrentUser(context)
if (options.required && !user) {
return response.unauthorized()
}
return { auth: { user } }
}
})
```
**Schema Pattern**: Centralized validation with Zod
```typescript
export const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email()
})
```
## Getting Started: 2-Minute Setup
Every template includes comprehensive setup instructions, but the basics are the same:
### Initialize Project
```bash
npx @igniter-js/cli init my-app
# Follow prompts to select template
```
```bash
npx @igniter-js/cli init my-app --template nextjs
```
### Install Dependencies
```bash
cd my-app
npm install
```
```bash
cd my-app
pnpm install
```
```bash
cd my-app
yarn install
```
```bash
cd my-app
bun install
```
### Start Local Services
```bash
# Start PostgreSQL and Redis with Docker
docker-compose up -d
```
### Configure Environment
```bash
# Copy example environment file
cp .env.example .env
# Edit .env with your settings
# DATABASE_URL and REDIS_URL are pre-configured for Docker
```
### Initialize Database
```bash
npx prisma db push
```
### Start Development Server
```bash
npm run dev
```
Your application is now running at `http://localhost:3000`
In under 2 minutes, you have a fully functional application with database, caching, type-safe API, and modern frontend—all working together.
## What's Included in Every Template
### Development Experience
**Instant Feedback**: Hot reload on every change\
**Type Safety**: Full TypeScript coverage with strict mode\
**Code Quality**: ESLint and Prettier pre-configured\
**Testing**: Vitest setup with example tests\
**Debugging**: VS Code launch configurations included
### Production Readiness
**Environment Management**: `.env` files with validation\
**Error Handling**: Comprehensive error boundaries\
**Logging**: Structured logging setup\
**Security**: CORS, rate limiting, input validation\
**Performance**: Built-in caching strategies
### Documentation
**README**: Detailed setup and usage instructions\
**AGENT.md**: AI assistant integration guide\
**Code Comments**: Well-documented examples\
**Architecture Docs**: Explanation of patterns and decisions
## Contributing Your Own Template
We welcome community contributions! Share your expertise by creating templates for new frameworks, use cases, or technology stacks.
### Fork and Clone
```bash
git clone https://github.com/felipebarcelospro/igniter-js.git
cd igniter-js
```
### Create Template Directory
```bash
mkdir apps/starter-your-template-name
cd apps/starter-your-template-name
```
### Build Your Template
Include these essentials:
* Comprehensive `README.md` with setup instructions
* `AGENT.md` describing template purpose and features
* TypeScript with strict mode enabled
* Error handling and validation throughout
* Security best practices
* Example features demonstrating patterns
### Add Documentation
Create template documentation page:
```bash
mkdir apps/docs/content/templates/
# Create MDX file with frontmatter and details
```
### Test Thoroughly
Verify:
* Clean installation process
* All dependencies install correctly
* Development server starts without errors
* Build process completes successfully
* TypeScript compilation passes
* Tests run and pass
### Submit Pull Request
Include:
* Detailed description of template features
* Target use cases and audience
* Any unique setup requirements
* Screenshots or demo links
We review all template submissions to ensure they meet our quality standards and provide genuine value to the community. Thank you for helping make Igniter.js better!
## Community Templates
Beyond our official templates, the community is building amazing starters for specific use cases:
* **E-commerce Platform**: Full shopping cart, payments, and inventory
* **SaaS Boilerplate**: Multi-tenancy, billing, and user management
* **Mobile API**: Optimized for React Native and Flutter apps
* **Real-Time Chat**: WebSocket and SSE implementation
* **Analytics Dashboard**: Data visualization and reporting
Browse community templates and submit your own at [igniterjs.com/templates/community](/templates/community)
## What's Next?
We're continuously expanding our template collection based on community needs. Upcoming templates include:
* **Monorepo Starter**: Turborepo setup with shared packages
* **GraphQL API**: Apollo Server integration
* **Mobile Full-Stack**: React Native with Igniter.js backend
* **Electron Desktop**: Cross-platform desktop applications
* **Chrome Extension**: Browser extension with Igniter.js API
Help us prioritize by voting on template requests in our [GitHub Discussions](https://github.com/felipebarcelospro/igniter-js/discussions).
## Try It Today
Stop spending hours on setup. Start building features in minutes with Igniter.js Templates:
```bash
# Choose your adventure
npx @igniter-js/cli init my-awesome-app
# Get coding immediately
cd my-awesome-app
npm run dev
```
Join thousands of developers who are already building faster with production-ready templates.
## Resources
* [Template Gallery](/templates)
* [CLI Documentation](/docs/cli)
* [Template Architecture Guide](/docs/templates/architecture)
* [Contributing Guide](https://github.com/felipebarcelospro/igniter-js/blob/main/CONTRIBUTING.md)
## Community
Share what you're building and get help from the community:
* [Discord Community](https://discord.gg/igniterjs)
* [GitHub Discussions](https://github.com/felipebarcelospro/igniter-js/discussions)
* [Twitter](https://twitter.com/igniterjs)
***
Your next great project is just one command away. Choose a template and start building today.
---
# Build a Production-Ready Real-Time Chat with Igniter.js and Next.js
> Learn how to build a fully type-safe, real-time chat application from scratch using Igniter.js, Next.js, Prisma, and Server-Sent Events. Complete tutorial with live demo and source code.
URL: https://igniterjs.com/blog/real-time-chat-with-igniterjs
Real-time features have become essential in modern web applications. Users expect instant updates without manual refreshing, whether it's a chat room, live notifications, or collaborative editing. In this comprehensive tutorial, you'll build a production-ready, type-safe chat application that showcases the power of Igniter.js's real-time capabilities.
A fully functional real-time chat application with:
* **End-to-end type safety** from database to UI components
* **Instant message delivery** using Server-Sent Events (SSE)
* **Automatic UI synchronization** across all connected clients
* **Production-ready architecture** with Prisma and PostgreSQL
* **Beautiful, responsive interface** built with Tailwind CSS
**Live Demo**: [igniter-js-sample-realtime-chat.vercel.app](https://igniter-js-sample-realtime-chat.vercel.app/)
## Why Server-Sent Events?
While WebSockets have traditionally been the go-to for real-time features, many use cases are actually **server-to-client dominant**. A chat application is perfect example: users send messages (client-to-server), and the server pushes updates to all clients (server-to-client).
**Server-Sent Events (SSE)** excel in this scenario:
✅ **Simpler than WebSockets**: Built on HTTP, easier to implement and debug\
✅ **Efficient**: Single long-lived connection per client\
✅ **Automatic reconnection**: Built into the browser spec\
✅ **Works everywhere**: No special server infrastructure needed\
✅ **Firewall-friendly**: Uses standard HTTP connections
**Use SSE when**: Server primarily pushes data to clients (notifications, live feeds, chat)\
**Use WebSockets when**: You need true bidirectional communication (gaming, video calls)
## Prerequisites
Before starting, ensure you have:
* **Node.js** v18 or higher
* **npm**, **pnpm**, **yarn**, or **bun** package manager
* **Docker** (for local PostgreSQL database)
* Basic knowledge of TypeScript and React
## Project Setup
### Step 1: Initialize the Project
Use the Igniter.js CLI to scaffold a new Next.js application:
```bash
npx @igniter-js/cli init realtime-chat
```
### Choose Template
Select **Next.js App Router** when prompted
### Navigate to Project
```bash
cd realtime-chat
```
### Install Dependencies
```bash
npm install
```
```bash
pnpm install
```
```bash
yarn install
```
```bash
bun install
```
### Step 2: Database Schema
Define your data model using Prisma. Open `prisma/schema.prisma` and replace its content:
```prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Message {
id String @id @default(uuid())
content String
sender String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("messages")
}
```
This simple schema stores messages with their content, sender name, and timestamps.
### Step 3: Environment Configuration
Create a `.env` file in your project root:
```bash
# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/chat?schema=public"
# App
NEXT_PUBLIC_APP_URL="http://localhost:3000"
```
### Step 4: Start Local Database
If you don't have PostgreSQL running, use Docker:
```bash
docker run --name chat-db \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=chat \
-p 5432:5432 \
-d postgres:16
```
Or use the included `docker-compose.yml`:
```bash
docker-compose up -d
```
### Step 5: Apply Database Schema
Sync your Prisma schema with the database:
```bash
npx prisma db push
```
Your project foundation is ready! Let's build the backend.
## Building the Backend API
### Step 6: Generate the Message Feature
Igniter.js CLI can scaffold complete features from your Prisma schema:
```bash
npx @igniter-js/cli generate feature message --schema prisma:Message
```
This creates:
The generator creates:
* **Controller**: CRUD operations for messages
* **Procedure**: Reusable repository pattern for database access
* **Schema**: Zod validation schemas derived from Prisma
### Step 7: Enable Real-Time Streaming
Open `src/features/message/controllers/message.controller.ts` and modify the `list` action:
```typescript
import { igniter } from '@/igniter'
import { messageProcedure } from '../procedures/message.procedure'
export const messageController = igniter.controller({
path: '/messages',
actions: {
// Enable streaming for real-time updates
list: igniter.query({
name: 'list',
description: 'List all messages with real-time updates',
path: '/',
stream: true, // ← This enables SSE!
use: [messageProcedure()],
handler: async ({ context, response }) => {
const messages = await context.messageRepository.findAll()
return response.success(messages)
}
}),
// ... other generated actions
}
})
```
The `stream: true` flag transforms this endpoint into an SSE stream that clients can subscribe to.
### Step 8: Add Real-Time Revalidation
When a new message is created, we want all connected clients to receive it instantly. Modify the `create` action:
```typescript
export const messageController = igniter.controller({
path: '/messages',
actions: {
list: igniter.query({
// ... list action from above
}),
create: igniter.mutation({
name: 'create',
description: 'Create a new message',
path: '/',
method: 'POST',
body: z.object({
content: z.string().min(1, 'Message cannot be empty'),
sender: z.string().min(1, 'Sender name is required')
}),
use: [messageProcedure()],
handler: async ({ request, context, response }) => {
// Create the message
const message = await context.messageRepository.create(request.body)
// Trigger revalidation for all connected clients
return response.created(message).revalidate(['message.list'])
}
})
}
})
```
The `.revalidate(['message.list'])` call tells Igniter.js to notify all clients subscribed to the `message.list` stream that new data is available.
### Step 9: Register the Controller
Open `src/igniter.router.ts` and add the message controller:
```typescript
import { igniter } from '@/igniter'
import { messageController } from '@/features/message/controllers/message.controller'
export const AppRouter = igniter.router({
controllers: {
message: messageController
}
})
export type AppRouter = typeof AppRouter
```
Your backend API is now fully functional with real-time capabilities! Let's test it before building the frontend.
## Testing the Backend
Before building the UI, verify your API works correctly:
### Test Creating a Message
```bash
curl -X POST http://localhost:3000/api/v1/messages \
-H "Content-Type: application/json" \
-d '{
"content": "Hello, world!",
"sender": "TestUser"
}'
```
### Test Listing Messages
```bash
curl http://localhost:3000/api/v1/messages
```
### Test SSE Stream
```bash
curl -N http://localhost:3000/api/v1/messages/stream
```
You should see a connection stay open, ready to receive updates.
## Building the Frontend
### Step 10: Create the Chat Component
Create `src/features/message/components/chat.tsx`:
```tsx
'use client'
import { useEffect, useRef, useState } from 'react'
import { api } from '@/igniter.client'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Send } from 'lucide-react'
export function Chat() {
const [sender, setSender] = useState('')
const [message, setMessage] = useState('')
const [isSenderSet, setIsSenderSet] = useState(false)
const scrollAreaRef = useRef(null)
// Fetch messages with automatic real-time updates
const { data } = api.message.list.useQuery({
refetchOnWindowFocus: false
})
const messages = data || []
// Mutation for creating messages
const createMessage = api.message.create.useMutation()
// Load sender from localStorage on mount
useEffect(() => {
const storedSender = localStorage.getItem('chat-sender')
if (storedSender) {
setSender(storedSender)
setIsSenderSet(true)
}
}, [])
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
if (scrollAreaRef.current) {
const scrollElement = scrollAreaRef.current.querySelector('div > div')
if (scrollElement) {
scrollElement.scrollTo({
top: scrollElement.scrollHeight,
behavior: 'smooth'
})
}
}
}, [messages])
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault()
if (message.trim() && sender.trim()) {
await createMessage.mutate({
body: {
content: message,
sender: sender
}
})
setMessage('')
}
}
const handleSetSender = () => {
if (sender.trim()) {
localStorage.setItem('chat-sender', sender.trim())
setIsSenderSet(true)
}
}
return (
Real-Time Chat with Igniter.js
{messages.map((msg: any) => (
{msg.sender}
{msg.content}
{new Date(msg.createdAt).toLocaleTimeString()}
))}
{/* Name entry modal */}
{!isSenderSet && (
)}
)
}
```
### Understanding the Component
Let's break down the key parts:
**1. Message Query with Auto-Updates**
```typescript
const { data } = api.message.list.useQuery({
refetchOnWindowFocus: false
})
```
This hook:
* Fetches initial messages on mount
* Automatically refetches when `.revalidate()` is triggered on the backend
* Provides loading and error states
* Caches results for performance
**2. Message Creation Mutation**
```typescript
const createMessage = api.message.create.useMutation()
```
This hook:
* Sends messages to the backend
* Provides loading state during submission
* Handles errors gracefully
* Triggers automatic revalidation on success
**3. Auto-Scroll Behavior**
```typescript
useEffect(() => {
// Scroll to bottom when messages update
}, [messages])
```
Ensures users always see the latest messages without manual scrolling.
### Step 11: Add to Page
Open `src/app/page.tsx` and use the Chat component:
```tsx
import { Chat } from '@/features/message/components/chat'
export default function HomePage() {
return (
)
}
```
## See It in Action
### Start Development Server
```bash
npm run dev
```
### Open Multiple Windows
Open `http://localhost:3000` in two browser windows side-by-side
### Test Real-Time Updates
Type a message in one window and watch it appear instantly in both!
You've built a production-ready, type-safe, real-time chat application! Messages appear instantly across all connected clients with zero manual synchronization.
## How It Works: Under the Hood
### The Real-Time Flow
### Client Subscribes
When `useQuery()` is called with a streamed endpoint, the client opens an SSE connection
### User Sends Message
Client calls `createMessage.mutate()`, sending data to the backend
### Backend Processes
Controller creates the message and calls `.revalidate(['message.list'])`
### Server Notifies Clients
Igniter.js sends a revalidation event through the SSE connection
### Clients Refetch
All subscribed clients automatically refetch the latest messages
### UI Updates
React re-renders with new data, showing the message to all users
### Type Safety Throughout
```typescript
// Backend defines the contract
export const messageController = igniter.controller({
// ... controller definition
})
// Frontend gets automatic types
api.message.list.useQuery() // ← TypeScript knows the return type!
api.message.create.useMutation() // ← TypeScript validates the body!
```
No code generation, no manual type definitions—pure TypeScript inference.
## Production Deployment
### Environment Variables
Update `.env` for production:
```bash
DATABASE_URL="postgresql://user:pass@host:5432/db"
NEXT_PUBLIC_APP_URL="https://your-domain.com"
```
### Scaling Considerations
#### Single Server
For small-to-medium apps, a single server handles SSE connections efficiently.
#### Multiple Servers (Horizontal Scaling)
For larger apps, use Redis for pub/sub across servers:
```bash
npm install @igniter-js/adapter-redis ioredis
```
```typescript
// src/igniter.ts
import { redisStoreAdapter } from '@igniter-js/adapter-redis'
import Redis from 'ioredis'
const redis = new Redis(process.env.REDIS_URL)
export const igniter = createIgniter({
store: redisStoreAdapter({ client: redis })
})
```
Now `.revalidate()` publishes to Redis, and all server instances receive updates.
### Deploy to Vercel
```bash
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel --prod
```
Your chat app is now live with full real-time capabilities!
## Advanced Features
### Add Message Persistence
Already included! Messages are stored in PostgreSQL via Prisma.
### Add User Authentication
Integrate an auth procedure:
```typescript
export const messageController = igniter.controller({
path: '/messages',
actions: {
create: igniter.mutation({
use: [auth({ required: true })],
handler: async ({ context, response }) => {
// context.auth.user is now available
const message = await context.db.message.create({
data: {
content: request.body.content,
sender: context.auth.user.name
}
})
return response.created(message).revalidate(['message.list'])
}
})
}
})
```
### Add Typing Indicators
Extend the schema and use ephemeral events:
```typescript
// Publish typing event without storing in DB
igniter.realtime.publish('message.typing', {
sender: context.auth.user.name,
isTyping: true
})
```
### Add Message Reactions
Add a reactions field to the schema and create a new mutation for toggling reactions.
## Troubleshooting
**Check**:
* `stream: true` is set on the `list` action
* `.revalidate(['message.list'])` is called in the `create` mutation
* Browser DevTools Network tab shows an open SSE connection to `/api/v1/messages`
**Solution**:
* Restart TypeScript server in your IDE
* Ensure `igniter.router.ts` exports `AppRouter` type
* Check that `igniter.client.ts` imports the router type correctly
**Check**:
* PostgreSQL is running (`docker ps`)
* `DATABASE_URL` in `.env` is correct
* Run `npx prisma db push` to sync schema
## Next Steps
Explore more Igniter.js features:
* [Background Jobs](/docs/features/jobs) - Process tasks asynchronously
* [Authentication](/docs/features/auth) - Add user authentication
* [File Uploads](/docs/features/uploads) - Handle file uploads
* [WebSockets](/docs/advanced-features/websockets) - For bidirectional communication
* [Testing](/docs/testing) - Write tests for your features
## Resources
* **[Live Demo](https://igniter-js-sample-realtime-chat.vercel.app/)**
* **[Source Code](https://github.com/felipebarcelospro/igniter-js/tree/main/apps/sample-realtime-chat)**
* [Real-Time Documentation](/docs/advanced-features/real-time)
* [useQuery Hook](/docs/client/use-query)
* [useMutation Hook](/docs/client/use-mutation)
* [Server-Sent Events Spec](https://html.spec.whatwg.org/multipage/server-sent-events.html)
***
You've just built a production-ready real-time application with complete type safety and minimal code. This same pattern scales to notifications, live dashboards, collaborative editing, and any feature requiring instant updates.
**Happy building with Igniter.js!** 🚀
---
# Documentation
Complete documentation for Igniter.js framework
---
# API Reference
> Complete API reference for @igniter-js/agents. All builders, types, error codes, adapters, and utility functions with parameter tables and examples.
URL: https://igniterjs.com/docs/agents/api-reference
## API Reference
Complete reference for every public API in `@igniter-js/agents`.
***
## Imports
```typescript
import {
// Builders
IgniterAgent,
IgniterAgentManager,
IgniterAgentTool,
IgniterAgentToolset,
IgniterAgentMCPClient,
IgniterAgentPrompt,
// Adapters
IgniterAgentInMemoryAdapter,
IgniterAgentJSONFileAdapter,
// Errors
IgniterAgentError,
IgniterAgentConfigError,
IgniterAgentMCPError,
IgniterAgentToolError,
IgniterAgentMemoryError,
IgniterAgentAdapterError,
IgniterAgentErrorCode,
isIgniterAgentError,
isIgniterAgentMCPError,
isIgniterAgentToolError,
// Types
type IgniterAgentHooks,
type IgniterAgentMemoryConfig,
type IgniterAgentGenerateOptions,
type IgniterAgentStreamOptions,
type IgniterAgentBuiltAgent,
type IgniterAgentBuiltTool,
type IgniterAgentBuiltToolset,
} from '@igniter-js/agents'
```
***
## IgniterAgent
Primary entry point for creating AI agents.
### `IgniterAgent.create(name)`
Creates a new agent builder.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------------- |
| `name` | `string` | Yes | Unique agent name |
### Builder Methods
| Method | Signature | Description |
| ---------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ |
| `.withModel(model)` | `(model: LanguageModel) => Builder` | Sets the AI language model. Accepts any Vercel AI SDK model. |
| `.withPrompt(prompt)` | `(prompt: IgniterAgentPromptTemplate) => Builder` | Sets the system prompt template. |
| `.withContextSchema(schema)` | `(schema: ZodSchema) => Builder` | Sets Zod schema for type-safe context. |
| `.addToolset(toolset)` | `(toolset: IgniterAgentToolset) => Builder` | Registers a custom toolset. |
| `.addMCP(config)` | `(config: IgniterAgentMCPConfigUnion) => Builder` | Registers an MCP client configuration. |
| `.withMemory(config)` | `(config: IgniterAgentMemoryConfig) => Builder` | Configures persistent memory. |
| `.withLogger(logger)` | `(logger: IgniterLogger) => Builder` | Attaches a logger instance. |
| `.withTelemetry(telemetry)` | `(telemetry: IgniterTelemetryManager) => Builder` | Attaches a telemetry manager. |
| `.onAgentStart(cb)` | `(name: string) => void` | Fired when agent starts. |
| `.onAgentError(cb)` | `(name: string, error: Error) => void` | Fired on agent error. |
| `.onToolCallStart(cb)` | `(agentName: string, toolName: string, input: unknown) => void` | Fired before tool execution. |
| `.onToolCallEnd(cb)` | `(agentName: string, toolName: string, output: unknown) => void` | Fired after successful tool execution. |
| `.onToolCallError(cb)` | `(agentName: string, toolName: string, error: Error) => void` | Fired on tool execution failure. |
| `.onMCPStart(cb)` | `(agentName: string, mcpName: string) => void` | Fired before MCP connection. |
| `.onMCPError(cb)` | `(agentName: string, mcpName: string, error: Error) => void` | Fired on MCP connection failure. |
| `.build()` | `() => IgniterAgentBuiltAgent` | Builds the agent. |
### Built Agent Methods
| Method | Signature | Description | |
| ----------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------- |
| `.start()` | `() => Promise` | Initializes MCP connections. Must be called before generate/stream. | |
| `.stop()` | `() => Promise` | Disconnects MCP connections gracefully. | |
| `.generate(options)` | `(options: IgniterAgentGenerateOptions) => Promise` | Generates a response. | |
| `.stream(options)` | `(options: IgniterAgentStreamOptions) => Promise` | Streams a response. | |
| `.getName()` | `() => string` | Returns the agent name. | |
| `.getConfig()` | `() => Partial` | Returns current config. | |
| `.getToolsets()` | `() => Record` | Returns toolsets (including MCP). | |
| `.attachLogger(logger)` | `(logger?: IgniterLogger) => void` | Attaches logger at runtime. | |
| `.attachTelemetry(telemetry)` | `(telemetry?: IgniterTelemetryManager) => void` | Attaches telemetry at runtime. | |
| `.attachHooks(hooks)` | `(hooks?: IgniterAgentHooks) => void` | Attaches hooks at runtime. | |
| `.memory` | \`IgniterAgentMemoryCore | undefined\` | Memory runtime (if configured). |
### `IgniterAgentGenerateOptions`
| Field | Type | Required | Description |
| ---------- | ------------------------- | -------- | ----------------------------------------------- |
| `chatId` | `string` | Yes | Unique conversation identifier |
| `userId` | `string` | Yes | User identifier for memory scoping |
| `context` | `Record` | Yes | Context data (typed if schema set) |
| `message` | `{ role, content }` | No | Single message (use `message` OR `messages`) |
| `messages` | `ModelMessage[]` | No | Multiple messages (use `message` OR `messages`) |
### `IgniterAgentOutput`
| Field | Type | Description |
| -------------- | -------------------- | --------------------------- |
| `content` | `string` | The generated text response |
| `toolCalls` | `ToolCall[]` | Tools the AI called |
| `toolResults` | `ToolResult[]` | Results from tool calls |
| `finishReason` | `string` | Why generation stopped |
| `usage` | `LanguageModelUsage` | Token usage statistics |
| `messages` | `ModelMessage[]` | Full message history |
***
## IgniterAgentManager
Orchestrates multiple agents with shared resources.
### `IgniterAgentManager.create()`
Creates a new manager builder.
### Builder Methods
| Method | Signature | Description |
| ---------------------------- | ------------------------------------------------- | ------------------------------------------------------ |
| `.addAgent(name, agent)` | `(name: string, agent: BuiltAgent) => Builder` | Registers an agent. Type-safe — names tracked in type. |
| `.withLogger(logger)` | `(logger: IgniterLogger) => Builder` | Shared logger for all agents. |
| `.withTelemetry(telemetry)` | `(telemetry: IgniterTelemetryManager) => Builder` | Shared telemetry for all agents. |
| `.withAutoStart(bool)` | `(bool: boolean) => Builder` | Auto-start on registration (default: false). |
| `.withContinueOnError(bool)` | `(bool: boolean) => Builder` | Continue batch start on error (default: true). |
| `.onAgentStart(cb)` | `(name: string) => void` | Applied to all registered agents. |
| `.onAgentError(cb)` | `(name: string, error: Error) => void` | Applied to all registered agents. |
| `.onToolCallStart(cb)` | `(agentName, toolName, input) => void` | Applied to all registered agents. |
| `.onToolCallEnd(cb)` | `(agentName, toolName, output) => void` | Applied to all registered agents. |
| `.onToolCallError(cb)` | `(agentName, toolName, error) => void` | Applied to all registered agents. |
| `.onMCPStart(cb)` | `(agentName, mcpName) => void` | Applied to all registered agents. |
| `.onMCPError(cb)` | `(agentName, mcpName, error) => void` | Applied to all registered agents. |
| `.build()` | `() => IgniterAgentManagerCore` | Builds the manager. |
### Built Manager Methods
| Method | Signature | Description |
| ------------------------ | ------------------------------------------- | ------------------------------------------- |
| `.register(name, agent)` | `(name: string, agent: BuiltAgent) => this` | Registers an agent (chainable). |
| `.unregister(name)` | `(name: string) => boolean` | Unregisters an agent. |
| `.has(name)` | `(name: string) => boolean` | Checks if an agent is registered. |
| `.get(name)` | `(name: string) => BuiltAgent` | Gets an agent by name. Throws if not found. |
| `.start(name)` | `(name: string) => Promise` | Starts a single agent. |
| `.startAll()` | `() => Promise>` | Starts all agents in parallel. |
| `.getNames()` | `() => string[]` | Returns registered agent names. |
| `.getStatus()` | `() => IgniterAgentInfo[]` | Returns status for all agents. |
| `.getFailedAgents()` | `() => IgniterAgentInfo[]` | Returns agents in error state. |
***
## IgniterAgentTool
Creates individual tool definitions.
### `IgniterAgentTool.create(name)`
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | --------------------------------- |
| `name` | `string` | Yes | Unique tool name within a toolset |
### Builder Methods
| Method | Required | Description |
| ------------------------ | -------- | ------------------------------------------ |
| `.withDescription(text)` | Yes | Human-readable description shown to the AI |
| `.withInput(zodSchema)` | Yes | Zod schema defining input parameters |
| `.withOutput(zodSchema)` | No | Zod schema for output validation |
| `.withExecute(handler)` | Yes | Async function performing the tool's logic |
| `.build()` | Yes | Returns the built tool definition |
### Execute Handler
```typescript
type ExecuteHandler = (
params: TInput,
options: IgniterAgentToolExecuteOptions
) => Promise
interface IgniterAgentToolExecuteOptions {
toolCallId: string
messages: ModelMessage[]
abortSignal?: AbortSignal
}
```
### Built Tool Properties
| Property | Type | Description | |
| -------------- | ------------------------- | ----------------------- | ------------------------ |
| `name` | `string` | Tool name | |
| `description` | `string` | Tool description | |
| `inputSchema` | `ZodSchema` | Input validation schema | |
| `outputSchema` | \`ZodSchema | undefined\` | Output validation schema |
| `execute` | `Function` | The execution handler | |
| `$Infer` | `{ Name, Input, Output }` | Type inference helper | |
***
## IgniterAgentToolset
Groups related tools into a named collection.
### `IgniterAgentToolset.create(name)`
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | --------------------------------------------------- |
| `name` | `string` | Yes | Unique toolset name (used as prefix for tool names) |
### Builder Methods
| Method | Description |
| ----------------- | -------------------------------- |
| `.withName(name)` | Renames the toolset |
| `.addTool(tool)` | Adds a built tool to the toolset |
| `.build()` | Returns the built toolset |
| `.getName()` | Returns the toolset name |
| `.getTools()` | Returns the tools object |
| `.getToolCount()` | Returns the number of tools |
### Built Toolset Properties
| Property | Type | Description |
| --------- | ----------------- | ----------------------------------------- |
| `name` | `string` | Toolset name |
| `type` | `'custom'` | Always 'custom' for user-defined toolsets |
| `status` | `'connected'` | Always 'connected' for custom toolsets |
| `tools` | `ToolSet` | The tools collection |
| `toolset` | `ToolSet` | Raw tools reference |
| `$Infer` | `{ Name, Tools }` | Type inference helper |
***
## IgniterAgentMCPClient
Configures connections to MCP servers.
### `IgniterAgentMCPClient.create(name)`
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------------------------- |
| `name` | `string` | Yes | Unique MCP configuration name |
### Common Methods
| Method | Description | |
| ------------------- | ---------------------------- | ------------------------------ |
| \`.withType('stdio' | 'http')\` | Sets transport type (required) |
| `.withName(name)` | Renames the configuration | |
| `.build()` | Returns the built MCP config | |
### Stdio-Specific Methods (after `withType('stdio')`)
| Method | Description |
| ------------------- | ---------------------------------------- |
| `.withCommand(cmd)` | Command to execute (e.g., `npx`, `node`) |
| `.withArgs(args)` | Command-line arguments array |
| `.withEnv(env)` | Environment variables record |
### HTTP-Specific Methods (after `withType('http')`)
| Method | Description |
| ----------------------- | ----------------------- |
| `.withURL(url)` | MCP server endpoint URL |
| `.withHeaders(headers)` | HTTP headers record |
### Built MCP Config Properties
| Property | Type | Description | |
| --------- | ------------------------------------- | --------------------- | -------------- |
| `name` | `string` | Configuration name | |
| `type` | \`'stdio' | 'http'\` | Transport type |
| `command` | `string` (stdio only) | Executable command | |
| `args` | `string[]` (stdio only) | Command arguments | |
| `env` | `Record` (stdio only) | Environment variables | |
| `url` | `string` (http only) | Server URL | |
| `headers` | `Record` (http only) | HTTP headers | |
***
## IgniterAgentPrompt
Creates template-based system prompts.
### `IgniterAgentPrompt.create(template)`
| Parameter | Type | Required | Description |
| ---------- | -------- | -------- | ------------------------------------------------ |
| `template` | `string` | Yes | Template string with `{{variable}}` placeholders |
### Methods
| Method | Description |
| --------------------------- | ---------------------------------- |
| `.build(context)` | Renders template with context data |
| `.addAppended(key, prompt)` | Appends another prompt or string |
| `.removeAppended(key)` | Removes an appended prompt |
| `.getAppended()` | Returns appended prompts object |
| `.getTemplate()` | Returns raw template string |
***
## Adapters
### IgniterAgentInMemoryAdapter
```typescript
import { IgniterAgentInMemoryAdapter } from '@igniter-js/agents'
const adapter = IgniterAgentInMemoryAdapter.create({
namespace: 'myapp', // Namespace prefix for keys
maxMessages: 1000, // Max messages per chat
maxChats: 100, // Max chat sessions
})
```
| Method | Description |
| -------------------------------- | ------------------------------- |
| `.create(options)` | Factory method |
| `.connect()` | No-op (interface compatibility) |
| `.disconnect()` | No-op (interface compatibility) |
| `.isConnected()` | Always returns `true` |
| `.clear()` | Clears all stored data |
| `getWorkingMemory(params)` | Retrieves scoped working memory |
| `updateWorkingMemory(params)` | Updates scoped working memory |
| `saveMessage(message)` | Saves a conversation message |
| `getMessages(params)` | Gets messages for a chat |
| `saveChat(chat)` | Saves/updates a chat session |
| `getChats(params)` | Lists chat sessions |
| `getChat(chatId)` | Gets a single chat |
| `updateChatTitle(chatId, title)` | Updates chat title |
| `deleteChat(chatId)` | Deletes a chat and its messages |
### IgniterAgentJSONFileAdapter
```typescript
import { IgniterAgentJSONFileAdapter } from '@igniter-js/agents'
const adapter = IgniterAgentJSONFileAdapter.create({
filePath: './data/agent-memory.json',
namespace: 'prod',
})
```
Same interface as InMemoryAdapter. Data persists to disk.
***
## Memory Configuration
### `IgniterAgentMemoryConfig`
```typescript
interface IgniterAgentMemoryConfig {
provider: IgniterAgentMemoryProvider // Adapter instance
working?: {
enabled: boolean // Enable working memory
scope: string // Scope key (e.g., 'chat')
}
history?: {
enabled: boolean // Enable chat history
limit: number // Max messages per chat
}
chats?: {
enabled: boolean // Enable chat sessions
}
}
```
***
## Error Codes
### `IgniterAgentErrorCode`
| Code | Value | Description |
| ------------------------------ | ----------------------------------------- | ------------------------- |
| `UNKNOWN` | `IGNITER_AGENT_UNKNOWN_ERROR` | Generic/unknown error |
| `INVALID_CONFIG` | `IGNITER_AGENT_INVALID_CONFIG` | Invalid configuration |
| `MISSING_REQUIRED` | `IGNITER_AGENT_MISSING_REQUIRED` | Required value missing |
| `AGENT_NOT_INITIALIZED` | `IGNITER_AGENT_NOT_INITIALIZED` | Agent not initialized |
| `AGENT_MODEL_MISSING` | `IGNITER_AGENT_MODEL_MISSING` | Model not configured |
| `AGENT_BUILD_FAILED` | `IGNITER_AGENT_BUILD_FAILED` | Agent build failed |
| `MCP_CONNECTION_FAILED` | `IGNITER_AGENT_MCP_CONNECTION_FAILED` | MCP connection failed |
| `MCP_CLIENT_NOT_FOUND` | `IGNITER_AGENT_MCP_CLIENT_NOT_FOUND` | MCP client not found |
| `MCP_TOOL_ERROR` | `IGNITER_AGENT_MCP_TOOL_ERROR` | MCP tool execution failed |
| `MCP_INVALID_CONFIG` | `IGNITER_AGENT_MCP_INVALID_CONFIG` | Invalid MCP configuration |
| `TOOL_EXECUTION_FAILED` | `IGNITER_AGENT_TOOL_EXECUTION_FAILED` | Tool execution failed |
| `TOOL_NOT_FOUND` | `IGNITER_AGENT_TOOL_NOT_FOUND` | Tool not found |
| `TOOL_VALIDATION_FAILED` | `IGNITER_AGENT_TOOL_VALIDATION_FAILED` | Tool validation failed |
| `AGENT_CONTEXT_SCHEMA_INVALID` | `IGNITER_AGENT_CONTEXT_SCHEMA_INVALID` | Invalid context schema |
| `MEMORY_PROVIDER_ERROR` | `IGNITER_AGENT_MEMORY_PROVIDER_ERROR` | Memory provider error |
| `MEMORY_NOT_FOUND` | `IGNITER_AGENT_MEMORY_NOT_FOUND` | Memory not found |
| `MEMORY_UPDATE_FAILED` | `IGNITER_AGENT_MEMORY_UPDATE_FAILED` | Memory update failed |
| `ADAPTER_CONNECTION_FAILED` | `IGNITER_AGENT_ADAPTER_CONNECTION_FAILED` | Adapter connection failed |
| `ADAPTER_OPERATION_FAILED` | `IGNITER_AGENT_ADAPTER_OPERATION_FAILED` | Adapter operation failed |
| `ADAPTER_NOT_INITIALIZED` | `IGNITER_AGENT_ADAPTER_NOT_INITIALIZED` | Adapter not initialized |
### Error Classes
| Class | Extends | Description |
| -------------------------- | ------------------- | ------------------------ |
| `IgniterAgentError` | `IgniterError` | Base error class |
| `IgniterAgentConfigError` | `IgniterAgentError` | Configuration errors |
| `IgniterAgentMCPError` | `IgniterAgentError` | MCP operation errors |
| `IgniterAgentToolError` | `IgniterAgentError` | Tool execution errors |
| `IgniterAgentMemoryError` | `IgniterAgentError` | Memory operation errors |
| `IgniterAgentAdapterError` | `IgniterAgentError` | Adapter operation errors |
### Type Guards
| Function | Signature | Description |
| -------------------------------- | ---------------------------------------------------- | --------------------------------------- |
| `isIgniterAgentError(error)` | `(error: unknown) => error is IgniterAgentError` | Checks if error is an IgniterAgentError |
| `isIgniterAgentMCPError(error)` | `(error: unknown) => error is IgniterAgentMCPError` | Checks if error is an MCP error |
| `isIgniterAgentToolError(error)` | `(error: unknown) => error is IgniterAgentToolError` | Checks if error is a tool error |
***
## Type Exports
### `IgniterAgentHooks`
```typescript
interface IgniterAgentHooks {
onAgentStart?: (name: string) => void
onAgentError?: (name: string, error: Error) => void
onToolCallStart?: (agentName: string, toolName: string, input: unknown) => void
onToolCallEnd?: (agentName: string, toolName: string, output: unknown) => void
onToolCallError?: (agentName: string, toolName: string, error: Error) => void
onMCPStart?: (agentName: string, mcpName: string) => void
onMCPError?: (agentName: string, mcpName: string, error: Error) => void
}
```
### `IgniterAgentBuiltAgent`
The type returned by `agent.build()`:
```typescript
type IgniterAgentBuiltAgent<
TContextSchema = ZodSchema,
TToolsets = Record,
TModel = LanguageModel,
TInstructions = IgniterAgentPromptTemplate,
>
```
### `IgniterAgentBuiltTool`
The type returned by `tool.build()`:
```typescript
type IgniterAgentBuiltTool<
TName = string,
TInput = unknown,
TOutput = unknown,
>
```
### `IgniterAgentBuiltToolset`
The type returned by `toolset.build()`:
```typescript
type IgniterAgentBuiltToolset<
TName = string,
TTools = ToolSet,
>
```
---
# Best Practices
> Production-ready patterns for agent design, tool implementation, memory management, MCP configuration, and multi-agent orchestration.
URL: https://igniterjs.com/docs/agents/best-practices
## Best Practices
Guidelines for building production-grade agents with `@igniter-js/agents`.
***
## Agent Design
### ✅ Do: Create focused, single-purpose agents
```typescript
// Good: Each agent has a clear domain
const supportBot = IgniterAgent.create('support')
.withPrompt(IgniterAgentPrompt.create('You are customer support.'))
.addToolset(supportToolset)
.build()
const codeBot = IgniterAgent.create('code')
.withPrompt(IgniterAgentPrompt.create('You are a code assistant.'))
.addToolset(codeToolset)
.build()
```
### ❌ Don't: Overload a single agent with too many responsibilities
```typescript
// Bad: One agent trying to do everything
const megaBot = IgniterAgent.create('everything')
.addToolset(supportToolset)
.addToolset(codeToolset)
.addToolset(analyticsToolset)
.addToolset(devopsToolset)
.addToolset(salesToolset)
.build()
// → Slow, confused, expensive
```
### ✅ Do: Set clear context schemas
```typescript
const agent = IgniterAgent.create('typed')
.withContextSchema(
z.object({
userId: z.string(),
userRole: z.enum(['admin', 'user']),
tenantId: z.string(),
})
)
.build()
```
### ❌ Don't: Skip context schemas in production
```typescript
// Bad: Untyped context opens the door to runtime errors
const agent = IgniterAgent.create('untyped').build()
await agent.generate({
chatId: 'x',
userId: 'x',
context: { usrRole: 'admin' }, // Typo caught zero times
message: { role: 'user', content: '...' },
})
```
***
## Tool Design
### ✅ Do: Write specific, actionable descriptions
```typescript
// Good: The AI knows exactly when to use this
IgniterAgentTool.create('get_order')
.withDescription(
'Retrieve order details by order ID. Use this when a customer asks about their order status, delivery, or items.'
)
```
### ❌ Don't: Write vague descriptions
```typescript
// Bad: The AI won't know when to call this
IgniterAgentTool.create('get_order')
.withDescription('Gets stuff')
```
### ✅ Do: Validate inputs with Zod
```typescript
// Good: Compile-time and runtime safety
.withInput(
z.object({
orderId: z.string().min(1).describe('Order ID in format ORD-XXXX'),
includeItems: z.boolean().default(true),
})
)
```
### ❌ Don't: Use `z.any()` or `z.unknown()` for inputs
```typescript
// Bad: No type safety
.withInput(z.any())
```
### ✅ Do: Return structured data from tools
```typescript
// Good: Predictable, typed output
.withExecute(async ({ orderId }) => {
return {
found: true,
orderId,
status: 'shipped',
items: [{ name: 'Widget', qty: 2 }],
}
})
```
### ❌ Don't: Return raw API responses directly
```typescript
// Bad: The AI gets an opaque blob
.withExecute(async ({ orderId }) => {
return api.get(`/orders/${orderId}`) // Returns raw HTTP response
})
```
***
## Memory Management
### ✅ Do: Enable memory for multi-turn conversations
```typescript
const agent = IgniterAgent.create('bot')
.withMemory({
provider: adapter,
working: { enabled: true, scope: 'chat' },
history: { enabled: true, limit: 100 },
chats: { enabled: true },
})
.build()
```
### ❌ Don't: Use in-memory adapter in production
```typescript
// Bad: Lost on restart
const memory = IgniterAgentInMemoryAdapter.create({ namespace: 'prod' })
```
### ✅ Do: Build a persistent adapter for production
```typescript
// Redis, PostgreSQL, or file-based for production
const memory = new RedisMemoryAdapter(redis)
```
***
## MCP Configuration
### ✅ Do: Handle MCP connection errors gracefully
```typescript
const agent = IgniterAgent.create('bot')
.addMCP(filesystemMCP)
.onMCPError((agent, mcp, err) => {
logger.warn(`MCP '${mcp}' unavailable: ${err.message}`)
// Continue with reduced capabilities
})
.build()
```
### ❌ Don't: Ignore MCP connection failures
```typescript
// Bad: Unhandled MCP error crashes the agent
await agent.start() // throws if MCP unreachable
```
### ✅ Do: Use environment variables for sensitive MCP config
```typescript
.withEnv({
API_KEY: process.env.MCP_API_KEY!,
// Never hardcode secrets
})
```
### ❌ Don't: Hardcode credentials in MCP config
```typescript
// Bad: Committed to source control
.withEnv({
API_KEY: 'sk-prod-abc123',
})
```
***
## Observability
### ✅ Do: Instrument with hooks and telemetry
```typescript
const agent = IgniterAgent.create('bot')
.withTelemetry(telemetry)
.onAgentError((name, err) => alerting.error(err))
.onToolCallError((agent, tool, err) => metrics.increment('tool.errors'))
.build()
```
### ❌ Don't: Run agents in production without observability
```typescript
// Bad: Silent failures, no audit trail
const agent = IgniterAgent.create('bot').withModel(openai('gpt-4o')).build()
```
***
## Multi-Agent Patterns
### ✅ Do: Use the manager for shared resources
```typescript
const manager = IgniterAgentManager.create()
.withTelemetry(sharedTelemetry)
.withLogger(sharedLogger)
.addAgent('support', supportBot)
.addAgent('sales', salesBot)
.build()
```
### ❌ Don't: Duplicate telemetry/logger per agent
```typescript
// Bad: Redundant configuration
const supportBot = IgniterAgent.create('support')
.withTelemetry(t1)
.withLogger(l1)
.build()
const salesBot = IgniterAgent.create('sales')
.withTelemetry(t2)
.withLogger(l2)
.build()
```
### ✅ Do: Set `continueOnError: true` for resilient multi-agent systems
```typescript
const manager = IgniterAgentManager.create()
.withContinueOnError(true) // One failing agent won't crash the others
.build()
```
***
## Performance
### ✅ Do: Reuse agent instances
```typescript
// Good: Create once, use many times
const agent = IgniterAgent.create('bot').withModel(openai('gpt-4o')).build()
await agent.start()
for (const message of messages) {
await agent.generate({ chatId, userId, context, message })
}
```
### ❌ Don't: Create new agents per request
```typescript
// Bad: Expensive — MCP reconnection on every request
async function handle(msg) {
const agent = IgniterAgent.create('bot')...build()
await agent.start()
return agent.generate(...)
}
```
### ✅ Do: Use `gpt-4o-mini` for development and `gpt-4o` for production
### ✅ Do: Limit message history to reduce token costs
```typescript
const memoryConfig = {
history: { enabled: true, limit: 20 }, // Keep last 20 messages
}
```
---
# Building Agents
> Complete guide to the IgniterAgent builder API. Model selection, prompt configuration, context schemas, lifecycle hooks, memory, and MCP integration.
URL: https://igniterjs.com/docs/agents/building-agents
## Building Agents
The `IgniterAgent` builder is the primary entry point for creating AI agents. It provides a type-safe, immutable fluent API that infers the full agent configuration at compile time.
***
## The Builder Pattern
Every agent starts with `IgniterAgent.create()` and ends with `.build()`. In between, you chain configuration methods — each returning a **new builder instance** with updated type parameters.
```typescript
import { IgniterAgent } from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
const agent = IgniterAgent
.create('my-agent') // IgniterAgentBuilder<'my-agent', ...>
.withModel(openai('gpt-4')) // ...<..., LanguageModel, ...>
.build() // IgniterAgentBuiltAgent
```
The builder is **immutable** — every method call creates a new builder instance. This means you can fork configurations from a base builder safely.
***
## Agent Configuration
### Model Selection
Choose any model provider compatible with the Vercel AI SDK:
```typescript
import { openai } from '@ai-sdk/openai'
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
// OpenAI
const gptAgent = IgniterAgent.create('gpt').withModel(openai('gpt-4o')).build()
// Anthropic
const claudeAgent = IgniterAgent.create('claude').withModel(anthropic('claude-3-5-sonnet-latest')).build()
// Google
const geminiAgent = IgniterAgent.create('gemini').withModel(google('gemini-2.0-flash')).build()
```
### Prompt Instructions
Set the agent's system prompt using `IgniterAgentPrompt`:
```typescript
import { IgniterAgent, IgniterAgentPrompt } from '@igniter-js/agents'
const agent = IgniterAgent
.create('assistant')
.withModel(openai('gpt-4o'))
.withPrompt(
IgniterAgentPrompt.create(`
You are a helpful coding assistant.
- Always provide type-safe TypeScript
- Include JSDoc comments
- Follow the project's ESLint rules
`)
)
.build()
```
Prompts support `{{variable}}` interpolation using context data passed at generation time. See [Prompts](/docs/agents/prompts) for full details.
### Context Schema
Define a Zod schema for type-safe context passed to each `.generate()` call:
```typescript
import { z } from 'zod'
const contextSchema = z.object({
userId: z.string(),
userRole: z.enum(['admin', 'user', 'guest']),
preferences: z.object({
language: z.string().default('en'),
theme: z.string().optional(),
}),
})
const agent = IgniterAgent
.create('typed-bot')
.withModel(openai('gpt-4o'))
.withContextSchema(contextSchema)
.build()
// Context is fully type-checked
await agent.generate({
chatId: 'chat_001',
userId: 'user_001',
context: {
userRole: 'admin', // ✅ Must be 'admin' | 'user' | 'guest'
preferences: { language: 'pt' },
},
message: { role: 'user', content: 'Hello!' },
})
```
If you set a context schema, the `context` field in `.generate()` and `.stream()` becomes required and type-checked against the schema. Context data is also available in prompt templates via `{{variable}}`.
***
## Adding Tools
### Custom Toolsets
Tools defined with `IgniterAgentTool` and grouped with `IgniterAgentToolset`:
```typescript
import { IgniterAgentTool, IgniterAgentToolset } from '@igniter-js/agents'
import { z } from 'zod'
const searchTool = IgniterAgentTool
.create('search_docs')
.withDescription('Search the documentation')
.withInput(z.object({ query: z.string() }))
.withExecute(async ({ query }) => {
// Search your docs here
return { results: ['Result 1', 'Result 2'] }
})
.build()
const docsToolset = IgniterAgentToolset
.create('docs')
.addTool(searchTool)
.build()
const agent = IgniterAgent
.create('docs-bot')
.withModel(openai('gpt-4o'))
.addToolset(docsToolset)
.build()
```
See [Tools & Toolsets](/docs/agents/tools-toolsets) for the complete tool builder API.
### MCP Clients
Connect to external MCP servers for tools discovered at runtime:
```typescript
import { IgniterAgentMCPClient } from '@igniter-js/agents'
const filesystemMCP = IgniterAgentMCPClient
.create('filesystem')
.withType('stdio')
.withCommand('npx')
.withArgs(['-y', '@modelcontextprotocol/server-filesystem', '/tmp'])
.build()
const agent = IgniterAgent
.create('mcp-agent')
.withModel(openai('gpt-4o'))
.addMCP(filesystemMCP)
.build()
```
See [MCP Integration](/docs/agents/mcp-integration) for full transport configuration.
***
## Memory Configuration
Persistent memory enables agents to remember conversations across turns:
```typescript
import { IgniterAgentInMemoryAdapter } from '@igniter-js/agents'
const adapter = IgniterAgentInMemoryAdapter.create({ namespace: 'app' })
const memoryConfig = {
provider: adapter,
working: { enabled: true, scope: 'chat' },
history: { enabled: true, limit: 100 },
chats: { enabled: true },
}
const agent = IgniterAgent
.create('memory-bot')
.withModel(openai('gpt-4o'))
.withMemory(memoryConfig)
.build()
```
See [Memory System](/docs/agents/memory) for adapter options and custom implementations.
***
## Observability
### Logger
Attach an `IgniterLogger` instance for structured logs:
```typescript
import { IgniterLogger } from '@igniter-js/common'
const agent = IgniterAgent
.create('logged-bot')
.withModel(openai('gpt-4o'))
.withLogger(myLogger)
.build()
```
### Telemetry
Attach an `IgniterTelemetryManager` for structured observability events:
```typescript
import { IgniterTelemetry } from '@igniter-js/telemetry'
const telemetry = IgniterTelemetry.create().build()
const agent = IgniterAgent
.create('observed-bot')
.withModel(openai('gpt-4o'))
.withTelemetry(telemetry)
.build()
```
### Lifecycle Hooks
Subscribe to agent and tool events directly on the builder:
```typescript
const agent = IgniterAgent
.create('hooked-bot')
.withModel(openai('gpt-4o'))
.onAgentStart((name) => console.log(`${name} started`))
.onAgentError((name, error) => console.error(`${name} failed:`, error))
.onToolCallStart((agentName, tool, input) => console.log(`[${agentName}] → ${tool}`))
.onToolCallEnd((agentName, tool, output) => console.log(`[${agentName}] ← ${tool}`))
.onToolCallError((agentName, tool, error) => console.error(`[${agentName}] ✕ ${tool}`, error))
.onMCPStart((agentName, mcp) => console.log(`${agentName}: connecting MCP ${mcp}`))
.onMCPError((agentName, mcp, error) => console.error(`${agentName}: MCP ${mcp} failed`, error))
.build()
```
See [Hooks & Telemetry](/docs/agents/hooks-telemetry) for the complete event model.
***
## Agent Lifecycle
### The Agent Runtime
Once built, the agent exposes these methods:
| Method | Description |
| ------------------------- | ----------------------------------------------------------------------------- |
| `agent.start()` | Initializes MCP connections. Must be called before `generate()` or `stream()` |
| `agent.stop()` | Disconnects MCP connections gracefully |
| `agent.generate(options)` | Generates a single response (non-streaming) |
| `agent.stream(options)` | Streams a response token-by-token |
| `agent.getName()` | Returns the agent's configured name |
| `agent.getConfig()` | Returns the full configuration object |
| `agent.getToolsets()` | Returns the configured toolsets (including MCP) |
### Generate vs Stream
```typescript
// Single response
const result = await agent.generate({
chatId: 'chat_001',
userId: 'user_001',
context: {},
message: { role: 'user', content: 'Hello!' },
})
console.log(result.content) // Full response text
console.log(result.toolCalls) // Tools the agent called
console.log(result.toolResults) // Results from tool calls
console.log(result.finishReason) // Why generation stopped
// Streaming response
const stream = await agent.stream({
chatId: 'chat_001',
userId: 'user_001',
context: {},
message: { role: 'user', content: 'Tell me a story' },
})
for await (const chunk of stream.textStream) {
process.stdout.write(chunk)
}
```
### Generate Options
```typescript
interface IgniterAgentGenerateOptions {
chatId: string // Unique conversation identifier
userId: string // User identifier for memory scoping
context: Record // Context data (typed if schema set)
message: { // Single message or...
role: 'user' | 'assistant'
content: string
}
messages?: ModelMessage[] // ...OR multiple messages
}
```
***
## Complete Example
A fully-configured agent with tools, memory, hooks, and telemetry:
```typescript
import {
IgniterAgent,
IgniterAgentTool,
IgniterAgentToolset,
IgniterAgentPrompt,
IgniterAgentInMemoryAdapter,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
import { IgniterTelemetry } from '@igniter-js/telemetry'
// Tools
const getTasks = IgniterAgentTool
.create('get_tasks')
.withDescription('Get tasks for today')
.withInput(z.object({ priority: z.enum(['high', 'medium', 'low']).optional() }))
.withExecute(async ({ priority }) => {
return [
{ id: 1, title: 'Review PR', priority: 'high' },
{ id: 2, title: 'Update docs', priority: 'medium' },
]
})
.build()
const taskToolset = IgniterAgentToolset
.create('tasks')
.addTool(getTasks)
.build()
// Memory
const memoryAdapter = IgniterAgentInMemoryAdapter.create({ namespace: 'prod' })
// Telemetry
const telemetry = IgniterTelemetry.create().build()
// Agent
const agent = IgniterAgent
.create('productivity-bot')
.withModel(openai('gpt-4o'))
.withPrompt(IgniterAgentPrompt.create('You are a productivity assistant.'))
.withContextSchema(z.object({ userId: z.string() }))
.addToolset(taskToolset)
.withMemory({
provider: memoryAdapter,
working: { enabled: true, scope: 'chat' },
history: { enabled: true, limit: 100 },
chats: { enabled: true },
})
.withTelemetry(telemetry)
.onAgentStart((name) => console.log(`${name} is live`))
.onToolCallStart((agent, tool, input) => console.log(`🔧 ${tool} called`))
.build()
await agent.start()
const result = await agent.generate({
chatId: 'chat_001',
userId: 'user_001',
context: { userId: 'user_001' },
message: { role: 'user', content: 'What do I need to do today?' },
})
console.log(result.content)
```
---
# Hooks & Telemetry
> Monitor agent operations with lifecycle hooks and structured telemetry events. Agent start/error, tool execution, MCP connections, and memory operations.
URL: https://igniterjs.com/docs/agents/hooks-telemetry
## Hooks & Telemetry
Agents provide two complementary observability mechanisms: **lifecycle hooks** for inline callbacks and **telemetry events** for structured, production-grade monitoring.
***
## Lifecycle Hooks
Hooks give you direct callbacks on key agent events. Configure them on the agent or manager builder.
### Available Hooks
| Hook | Signature | When it fires |
| ----------------- | ---------------------------------------------------------------- | ------------------------------------------------ |
| `onAgentStart` | `(name: string) => void` | Agent starts successfully |
| `onAgentError` | `(name: string, error: Error) => void` | Agent fails to start or encounters a fatal error |
| `onToolCallStart` | `(agentName: string, toolName: string, input: unknown) => void` | Before a tool executes |
| `onToolCallEnd` | `(agentName: string, toolName: string, output: unknown) => void` | After a tool completes successfully |
| `onToolCallError` | `(agentName: string, toolName: string, error: Error) => void` | When a tool execution fails |
| `onMCPStart` | `(agentName: string, mcpName: string) => void` | Before connecting to an MCP server |
| `onMCPError` | `(agentName: string, mcpName: string, error: Error) => void` | When an MCP connection fails |
### Configuring Hooks
On a single agent:
```typescript
const agent = IgniterAgent
.create('bot')
.withModel(openai('gpt-4o'))
.onAgentStart((name) => console.log(`${name} is live`))
.onAgentError((name, err) => console.error(`${name} crashed:`, err))
.onToolCallStart((agent, tool, input) => {
console.log(`[${agent}] Calling ${tool} with:`, input)
})
.onToolCallEnd((agent, tool, output) => {
console.log(`[${agent}] ${tool} returned:`, output)
})
.onToolCallError((agent, tool, err) => {
console.error(`[${agent}] ${tool} failed:`, err.message)
})
.onMCPStart((agent, mcp) => console.log(`${agent}: connecting MCP '${mcp}'`))
.onMCPError((agent, mcp, err) => {
console.error(`${agent}: MCP '${mcp}' failed:`, err.message)
})
.build()
```
On a manager (applied to all registered agents):
```typescript
const manager = IgniterAgentManager
.create()
.onToolCallStart((agentName, tool, input) => {
// Track tool usage across all agents
metrics.increment('tool_calls', { agent: agentName, tool })
})
.onToolCallEnd((agentName, tool, output) => {
metrics.timing('tool_duration', { agent: agentName, tool })
})
.build()
```
### Runtime Hook Attachment
Hooks can also be attached after building via `attachHooks()`:
```typescript
const agent = IgniterAgent.create('bot').withModel(openai('gpt-4o')).build()
agent.attachHooks({
onAgentStart: (name) => console.log(`Started: ${name}`),
onToolCallStart: (agentName, tool) => console.log(`Tool: ${tool}`),
})
```
When using a manager, hooks set on the manager are **merged** with any hooks already set on individual agents. Both will fire.
***
## Telemetry Integration
For production-grade observability, use `@igniter-js/telemetry` to emit structured events:
```typescript
import { IgniterTelemetry } from '@igniter-js/telemetry'
const telemetry = IgniterTelemetry.create().build()
const agent = IgniterAgent
.create('bot')
.withModel(openai('gpt-4o'))
.withTelemetry(telemetry)
.build()
```
### Telemetry Events
All events use the `igniter.agent.*` namespace:
#### Lifecycle Events
| Event | Level | Key Attributes |
| --------------------------------------- | ----- | ------------------------------------------------------------------------ |
| `igniter.agent.lifecycle.start.started` | debug | `ctx.agent.name`, `ctx.lifecycle.toolsetCount`, `ctx.lifecycle.mcpCount` |
| `igniter.agent.lifecycle.start.success` | debug | + `ctx.lifecycle.durationMs` |
| `igniter.agent.lifecycle.start.error` | error | + `ctx.error.code`, `ctx.error.message` |
| `igniter.agent.lifecycle.stop.started` | debug | Same as start |
| `igniter.agent.lifecycle.stop.success` | debug | + duration |
| `igniter.agent.lifecycle.stop.error` | error | + error details |
#### MCP Events
| Event | Key Attributes |
| -------------------------------------- | ------------------------------------------- |
| `igniter.agent.mcp.connect.started` | `ctx.mcp.name`, `ctx.mcp.type` |
| `igniter.agent.mcp.connect.success` | + `ctx.mcp.toolCount`, `ctx.mcp.durationMs` |
| `igniter.agent.mcp.connect.error` | + `ctx.error.*` |
| `igniter.agent.mcp.disconnect.started` | `ctx.mcp.name`, `ctx.mcp.type` |
| `igniter.agent.mcp.disconnect.success` | + `ctx.mcp.durationMs` |
| `igniter.agent.mcp.disconnect.error` | + `ctx.error.*` |
#### Memory Events
| Event | Key Attributes |
| ---------------------------------------- | --------------------------------------------- |
| `igniter.agent.memory.operation.started` | `ctx.memory.operation`, `ctx.memory.scope` |
| `igniter.agent.memory.operation.success` | + `ctx.memory.durationMs`, `ctx.memory.count` |
| `igniter.agent.memory.operation.error` | + `ctx.error.code`, `ctx.error.message` |
### Processing Telemetry Events
```typescript
import { IgniterTelemetry } from '@igniter-js/telemetry'
const telemetry = IgniterTelemetry
.create()
.onEvent('igniter.agent.lifecycle.start.error', (event) => {
// Send to your alerting system
alerting.send({
severity: 'critical',
message: `Agent ${event.attributes['ctx.agent.name']} failed to start`,
})
})
.onEvent('igniter.agent.mcp.connect.error', (event) => {
alerting.send({
severity: 'warning',
message: `MCP '${event.attributes['ctx.mcp.name']}' failed`,
})
})
.onEvent('igniter.agent.memory.operation.error', (event) => {
console.error(
`Memory operation '${event.attributes['ctx.memory.operation']}' failed:`,
event.attributes['ctx.error.message']
)
})
.build()
```
***
## Logger Integration
Attach an `IgniterLogger` for structured debug/info/error logging:
```typescript
import { IgniterLogger } from '@igniter-js/common'
const agent = IgniterAgent
.create('bot')
.withModel(openai('gpt-4o'))
.withLogger(myLogger)
.build()
// The logger receives structured messages:
// debug: "IgniterAgent.start started" { agent: 'bot', toolsetCount: 2, mcpCount: 1 }
// success: "IgniterAgent.start success" { agent: 'bot', durationMs: 342 }
// error: "IgniterAgent.mcp.connect failed" Error(...)
```
When using a manager, loggers are automatically scoped with `logger.child('IgniterAgent', { agent: name })`:
```typescript
const manager = IgniterAgentManager
.create()
.withLogger(myLogger) // Automatically scoped per agent
.addAgent('support', supportAgent)
.build()
```
***
## Choosing Hooks vs Telemetry
| Use Case | Hooks | Telemetry |
| ------------------------- | ---------------- | ------------------- |
| **Inline logging** | ✅ Best | Overkill |
| **Custom side effects** | ✅ Best | Not designed for |
| **Production monitoring** | No | ✅ Best |
| **Aggregated metrics** | Manual | ✅ Built-in |
| **Alerting** | Manual | ✅ Structured events |
| **Debugging** | ✅ Immediate | ✅ Searchable |
| **Performance** | Minimal overhead | Optimized pipeline |
You can use **both** — hooks for immediate reactions (logging, side effects) and telemetry for production monitoring (metrics, alerting, dashboards).
---
# Introduction
> Production-ready, type-safe AI agent framework for Igniter.js. Build intelligent agents with custom tools, persistent memory, telemetry, and multi-agent orchestration.
URL: https://igniterjs.com/docs/agents
## Overview
`@igniter-js/agents` is a **production-grade, type-safe** AI agent framework built on the [Vercel AI SDK](https://sdk.vercel.ai). It provides a fluent builder API for creating AI agents with custom tools, persistent memory, MCP integration, telemetry, and multi-agent orchestration — all with full TypeScript type inference.
Whether you're building a simple chatbot, a multi-agent customer support system, or an autonomous coding assistant, agents gives you the foundation with compile-time safety and production observability.
Agents is a **fully independent package** — use it in any Node.js or Bun application. No framework lock-in. Works with Express, Next.js, Fastify, Hono, or background workers.
### Key Features
* **Type-safe tooling** — Define tools with Zod schemas and get full type inference on inputs, outputs, and execution handlers
* **Fluent builder API** — Immutable, chainable configuration for agents, tools, toolsets, MCP clients, and prompts
* **Persistent memory** — Adapter-based storage for working memory, chat history, and chat sessions
* **MCP integration** — Native support for Model Context Protocol servers via stdio and HTTP transports
* **Multi-agent orchestration** — Manage multiple specialized agents with a single manager, shared telemetry, and hooks
* **Comprehensive telemetry** — Structured observability events for every lifecycle phase, tool call, MCP connection, and memory operation
* **Lifecycle hooks** — Subscribe to agent start/error, tool call start/end/error, and MCP connection events
* **Prompt templates** — Context-aware prompt building with `{{variable}}` interpolation and appended prompt support
* **Pluggable adapters** — In-memory and JSON file adapters included; build custom adapters for any storage backend
* **Predictable errors** — Stable error codes (`AGENT_NOT_INITIALIZED`, `MEMORY_PROVIDER_ERROR`, `INVALID_CONFIG`) for programmatic handling
***
## Architecture
Agents follows the **builder pattern** — every component is built through a fluent, chainable API. The agent core handles model interaction, tool execution, MCP lifecycle, memory persistence, telemetry, and hooks.
```
┌─────────────────────────────────────────────────────┐
│ IgniterAgentManager │
│ (multi-agent orchestration, shared telemetry) │
├─────────────────────────────────────────────────────┤
│ IgniterAgentCore │
│ (model interaction, tool exec, MCP lifecycle) │
├──────────┬──────────┬──────────┬───────────────────┤
│ Tools │ Toolsets │ MCP │ Memory │
│ (typed │ (grouped │ (stdio, │ (working, chat, │
│ funcs) │ tools) │ http) │ history) │
├──────────┴──────────┴──────────┴───────────────────┤
│ Prompt Templates (interpolation) │
├─────────────────────────────────────────────────────┤
│ Telemetry + Logger + Hooks (observability) │
└─────────────────────────────────────────────────────┘
```
Every operation flows through the same pipeline: **validate input → execute → capture telemetry → fire hooks → return result**.
### Mental Model
| Concept | What it is | Example |
| -------------- | ------------------------------------------ | ------------------------------------------------------------------------ |
| **Agent** | AI-powered runtime with tools and memory | `IgniterAgent.create('assistant').withModel(...).build()` |
| **Tool** | Typed, executable function the AI can call | `IgniterAgentTool.create('getWeather').withInput(z.object({...}))` |
| **Toolset** | Collection of related tools by domain | `IgniterAgentToolset.create('github').addTool(...).addTool(...).build()` |
| **MCP Client** | Connection to external MCP servers | `IgniterAgentMCPClient.create('filesystem').withType('stdio').build()` |
| **Memory** | Persistent storage for context and history | `{ provider: adapter, working: {...}, history: {...} }` |
| **Manager** | Orchestrates multiple agents | `IgniterAgentManager.create().addAgent('support', agent).build()` |
| **Prompt** | Template-based system instructions | `IgniterAgentPrompt.create('You are {{role}}.')` |
| **Hooks** | Lifecycle and tool execution callbacks | `{ onAgentStart: ..., onToolCallStart: ... }` |
***
## Quick Look
Here's a complete agent with a custom tool, ready to run:
```typescript
import {
IgniterAgent,
IgniterAgentTool,
IgniterAgentToolset,
IgniterAgentPrompt,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
// 1. Define a typed tool
const weatherTool = IgniterAgentTool
.create('get_weather')
.withDescription('Get current weather for a location')
.withInput(
z.object({
location: z.string().describe('City name'),
unit: z.enum(['C', 'F']).default('C'),
})
)
.withExecute(async ({ location, unit }) => {
// Call your weather API here
return { location, temperature: 22, unit, condition: 'Sunny' }
})
.build()
// 2. Group tools into a toolset
const weatherToolset = IgniterAgentToolset
.create('weather')
.addTool(weatherTool)
.build()
// 3. Create the agent
const agent = IgniterAgent
.create('weather-bot')
.withModel(openai('gpt-4'))
.withPrompt(
IgniterAgentPrompt.create(
'You are a helpful weather assistant. Use tools to answer weather questions.'
)
)
.addToolset(weatherToolset)
.build()
// 4. Start and use
await agent.start()
const result = await agent.generate({
chatId: 'chat_123',
userId: 'user_456',
context: {},
message: {
role: 'user',
content: "What's the weather in London?",
},
})
console.log(result.content)
```
**✅ That's it!** A fully type-safe agent with custom tools, ready for production.
***
## Package Tour
Fluent API for building agents (model, tools, MCP, memory, hooks)
Manager builder for multi-agent orchestration
Fluent API for single tool definitions
Fluent API for tool collections
Fluent API for MCP client configuration
Template interpolation with
{"{{variable}}"}
support
Agent runtime: generate, stream, MCP lifecycle, tool execution
Multi-agent orchestration: register, startAll, routing
Memory runtime wrapper with telemetry and error handling
In-memory storage for development/testing
JSON file persistence for single-machine deployments
Core agent type definitions
Builder output types
Memory provider interface
Lifecycle hook signatures
Adapter configuration types
Stable error codes and error classes
***
## Next Steps
* [Installation](/docs/agents/installation) — All package managers, peer dependencies, and runtime requirements
* [Quick Start](/docs/agents/quick-start) — Three complete scenarios in under 5 minutes each
* [Building Agents](/docs/agents/building-agents) — Deep dive into the agent builder API
* [Tools & Toolsets](/docs/agents/tools-toolsets) — Type-safe tool creation and organization
---
# Installation
> Install @igniter-js/agents with any package manager. Peer dependencies, AI model provider setup, optional MCP support, and runtime requirements.
URL: https://igniterjs.com/docs/agents/installation
## Installation
### Core Package
```bash
npm install @igniter-js/agents ai zod
```
```bash
pnpm add @igniter-js/agents ai zod
```
```bash
yarn add @igniter-js/agents ai zod
```
```bash
bun add @igniter-js/agents ai zod
```
### AI Model Provider (Required)
Choose and install at least one AI model provider. Agents works with any provider supported by the [Vercel AI SDK](https://sdk.vercel.ai/docs/models):
```bash
npm install @ai-sdk/openai
```
Set your API key:
```bash
export OPENAI_API_KEY="sk-..."
```
```bash
npm install @ai-sdk/anthropic
```
```bash
export ANTHROPIC_API_KEY="sk-ant-..."
```
```bash
npm install @ai-sdk/google
```
```bash
export GOOGLE_GENERATIVE_AI_API_KEY="..."
```
```bash
npm install @ai-sdk/mistral
```
```bash
export MISTRAL_API_KEY="..."
```
You can also use **Ollama** for local models, **Groq**, **Cohere**, **DeepSeek**, or any other [AI SDK provider](https://sdk.vercel.ai/providers/ai-sdk-providers).
### Optional: MCP Support
To connect agents to external Model Context Protocol servers:
```bash
npm install @ai-sdk/mcp @modelcontextprotocol/sdk
```
### Optional: Telemetry
For production observability with structured events:
```bash
npm install @igniter-js/telemetry
```
***
## Peer Dependencies
| Package | Version | Required | Purpose |
| ----------------------------- | ------- | -------- | ----------------------------------------------- |
| `ai` | `>=4.0` | ✅ Yes | Core AI SDK for model interaction |
| `zod` | `>=3.0` | ✅ Yes | Runtime schema validation for tools and context |
| `@ai-sdk/openai` (or similar) | `>=1.0` | ✅ Yes | AI model provider |
| `@igniter-js/telemetry` | `*` | No | Structured telemetry events |
| `@ai-sdk/mcp` | `*` | No | MCP server integration |
| `@modelcontextprotocol/sdk` | `*` | No | MCP client transport layer |
***
## Runtime Requirements
| Environment | Version | Notes |
| ----------- | --------------- | ------------------- |
| **Node.js** | `>= 22.0.0` | Full support |
| **Bun** | `>= 1.0.0` | Full support |
| **Deno** | `>= 1.30.0` | Full support |
| **Browser** | ❌ Not supported | Server-only package |
Agents is **server-only**. Importing it in a browser environment will trigger the shim protection. Use it in API routes, server actions, background workers, or CLI tools.
***
## Verify Installation
After installing, verify everything is wired correctly:
```typescript
import {
IgniterAgent,
IgniterAgentTool,
IgniterAgentToolset,
IgniterAgentPrompt,
IgniterAgentMCPClient,
IgniterAgentManager,
IgniterAgentInMemoryAdapter,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
// Quick smoke test — should build without errors
const agent = IgniterAgent
.create('test')
.withModel(openai('gpt-4o-mini'))
.withPrompt(IgniterAgentPrompt.create('You are a test agent.'))
.build()
console.log('Agent name:', agent.getName())
// → Agent name: test
```
**✅ Success!** If you see the agent name printed, your installation is ready.
***
## Next Steps
* [Quick Start](/docs/agents/quick-start) — Build your first agent in under 5 minutes
* [Building Agents](/docs/agents/building-agents) — Deep dive into the agent builder API
---
# MCP Integration
> Connect agents to external tool providers via Model Context Protocol. Stdio and HTTP transports, environment variables, headers, and runtime tool discovery.
URL: https://igniterjs.com/docs/agents/mcp-integration
## MCP Integration
The **Model Context Protocol (MCP)** allows agents to discover and use tools from external servers at runtime. `@igniter-js/agents` supports both **stdio** (local process) and **HTTP** (remote server) transports.
***
## What is MCP?
MCP is an open protocol that standardizes how AI applications connect to external tool providers. An MCP server exposes tools — the agent discovers them at startup and can invoke them just like custom tools.
```mermaid
graph LR
A[Your Agent] -->|stdio| B[Local MCP Server]
A -->|HTTP| C[Remote MCP Server]
B --> D[File System]
C --> E[External API]
```
MCP tools are discovered at runtime when `agent.start()` is called. The agent doesn't need to know about them at build time.
***
## Installation
MCP support requires additional dependencies:
```bash
npm install @ai-sdk/mcp @modelcontextprotocol/sdk
```
***
## MCP Client Builder
### Stdio Transport (Local Process)
Spawns a local process and communicates via stdin/stdout:
```typescript
import { IgniterAgentMCPClient } from '@igniter-js/agents'
const filesystemMCP = IgniterAgentMCPClient
.create('filesystem')
.withType('stdio')
.withCommand('npx')
.withArgs([
'-y',
'@modelcontextprotocol/server-filesystem',
'/home/user/workspace',
])
.withEnv({
DEBUG: 'mcp:*',
HOME: process.env.HOME!,
})
.build()
```
### HTTP Transport (Remote Server)
Connects to a remote MCP server over HTTP/HTTPS:
```typescript
const remoteMCP = IgniterAgentMCPClient
.create('opencode')
.withType('http')
.withURL('https://sandbox.example.com/mcp')
.withHeaders({
'Authorization': `Bearer ${process.env.MCP_API_KEY}`,
'Content-Type': 'application/json',
})
.build()
```
***
## MCP Client Builder API
### Common Methods
| Method | Description |
| ------------------------------ | --------------------------------------------------- |
| `.create(name)` | Creates a new MCP client builder with a unique name |
| `.withType('stdio' \| 'http')` | Sets the transport type (required) |
| `.withName(name)` | Renames the configuration |
| `.build()` | Returns the built MCP config |
### Stdio-Specific Methods
| Method | Description |
| ------------------- | ------------------------------------------------------ |
| `.withCommand(cmd)` | The command to execute (e.g., `npx`, `node`, `python`) |
| `.withArgs(args)` | Command-line arguments array |
| `.withEnv(env)` | Environment variables record |
### HTTP-Specific Methods
| Method | Description |
| ----------------------- | ---------------------------------------------- |
| `.withURL(url)` | The MCP server endpoint URL |
| `.withHeaders(headers)` | HTTP headers record (auth, content-type, etc.) |
***
## Registering MCP Clients
Use `.addMCP()` on the agent builder:
```typescript
const agent = IgniterAgent
.create('multi-mcp-agent')
.withModel(openai('gpt-4o'))
.addMCP(filesystemMCP)
.addMCP(githubMCP)
.addMCP(remoteMCP)
.build()
// All MCP connections are established here
await agent.start()
```
MCP connections are **established at `start()` time, not at build time**. If an MCP server is unreachable, `start()` will throw an error. Use `.onMCPError()` to handle connection failures gracefully.
***
## Real-World MCP Examples
### GitHub MCP Server
```typescript
const githubMCP = IgniterAgentMCPClient
.create('github')
.withType('stdio')
.withCommand('npx')
.withArgs(['-y', '@modelcontextprotocol/server-github'])
.withEnv({
GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN!,
})
.build()
```
### Brave Search MCP
```typescript
const searchMCP = IgniterAgentMCPClient
.create('brave-search')
.withType('stdio')
.withCommand('npx')
.withArgs(['-y', '@modelcontextprotocol/server-brave-search'])
.withEnv({
BRAVE_API_KEY: process.env.BRAVE_API_KEY!,
})
.build()
```
### PostgreSQL MCP
```typescript
const postgresMCP = IgniterAgentMCPClient
.create('database')
.withType('stdio')
.withCommand('npx')
.withArgs(['-y', '@modelcontextprotocol/server-postgres'])
.withEnv({
DATABASE_URL: process.env.DATABASE_URL!,
})
.build()
```
***
## MCP Lifecycle
```
agent.build() → MCP configs are registered (not connected)
agent.start() → Each MCP is connected in sequence
Tools are discovered from each server
Hooks fire: onMCPStart for each connection
agent.generate() → AI can use tools from all connected MCPs
agent.stop() → Each MCP is disconnected gracefully
```
### Error Handling
```typescript
const agent = IgniterAgent
.create('robust-agent')
.withModel(openai('gpt-4o'))
.addMCP(filesystemMCP)
.onMCPStart((agentName, mcp) => {
console.log(`✅ ${agentName}: MCP '${mcp}' connected`)
})
.onMCPError((agentName, mcp, error) => {
console.error(`❌ ${agentName}: MCP '${mcp}' failed:`, error.message)
})
.build()
try {
await agent.start()
} catch (error) {
console.error('Agent startup failed:', error)
}
```
***
## MCP vs Custom Tools
| Feature | Custom Tools | MCP Tools |
| --------------- | --------------------------- | ------------------------- |
| **Definition** | In-code with Zod schemas | Discovered at runtime |
| **Type Safety** | Full compile-time inference | Runtime only |
| **Deployment** | Bundled with your app | External process/server |
| **Updates** | Require redeploy | Independent |
| **Use Case** | App-specific logic | Shared/standardized tools |
Use **custom tools** for your application's core business logic. Use **MCP** for standardized tools (filesystem, database, search, GitHub) that can be shared across agents and maintained independently.
***
## Next Steps
* [Multi-Agent Orchestration](/docs/agents/multi-agent) — Manage multiple agents with a single manager
* [Hooks & Telemetry](/docs/agents/hooks-telemetry) — Monitor MCP connections with telemetry events
---
# Memory System
> Persistent memory for agent conversations. Working memory, chat history, chat sessions, in-memory adapter, JSON file adapter, and building custom adapters.
URL: https://igniterjs.com/docs/agents/memory
## Memory System
The memory system enables agents to maintain context across multiple conversation turns. It provides three layers of persistence: **working memory** (key-value state), **chat history** (message logs), and **chat sessions** (conversation metadata). All layers are backed by pluggable adapters.
***
## Memory Architecture
```
┌──────────────────────────────────────────────┐
│ IgniterAgentMemoryCore │
│ (telemetry wrapping, error handling, logging) │
├──────────────────────────────────────────────┤
│ Memory Provider Interface │
├──────────────┬──────────────┬────────────────┤
│ Working │ Chat │ Chat │
│ Memory │ History │ Sessions │
│ (key-value) │ (messages) │ (metadata) │
├──────────────┴──────────────┴────────────────┤
│ Adapter Implementations │
├──────────────┬──────────────┬────────────────┤
│ In-Memory │ JSON File │ Custom │
│ Adapter │ Adapter │ (Redis, PG) │
└──────────────┴──────────────┴────────────────┘
```
***
## Enabling Memory
Memory is configured on the agent builder via `.withMemory()`:
```typescript
import { IgniterAgentInMemoryAdapter } from '@igniter-js/agents'
const adapter = IgniterAgentInMemoryAdapter.create({
namespace: 'my-app',
maxChats: 50,
})
const memoryConfig = {
provider: adapter,
working: { enabled: true, scope: 'chat' },
history: { enabled: true, limit: 100 },
chats: { enabled: true },
}
const agent = IgniterAgent
.create('memory-bot')
.withModel(openai('gpt-4o'))
.withMemory(memoryConfig)
.build()
```
### Memory Config Options
| Option | Type | Description |
| ----------------- | ---------------------------- | ---------------------------------------------------------- |
| `provider` | `IgniterAgentMemoryProvider` | The adapter instance |
| `working.enabled` | `boolean` | Enable working memory (key-value state) |
| `working.scope` | `string` | Scope key for working memory (e.g., `'chat'`, `'session'`) |
| `history.enabled` | `boolean` | Enable chat message history |
| `history.limit` | `number` | Maximum messages to keep per chat |
| `chats.enabled` | `boolean` | Enable chat session management |
***
## Working Memory
Working memory provides a key-value store scoped to an identifier (typically the chat ID). It's ideal for storing preferences, state, and transient context.
```typescript
// Inside the agent's memory runtime:
await agent.memory.updateWorkingMemory({
scope: 'chat',
identifier: 'chat_123',
content: 'User prefers TypeScript. Uses VS Code. Project: igniter-js.',
})
const memory = await agent.memory.getWorkingMemory({
scope: 'chat',
identifier: 'chat_123',
})
console.log(memory?.content)
// → "User prefers TypeScript. Uses VS Code. Project: igniter-js."
console.log(memory?.updatedAt)
// → 2026-06-02T17:00:00.000Z
```
Working memory is automatically injected into the agent's context before each generation call, so the AI can reference it in its responses.
***
## Chat History & Sessions
The memory system manages full conversation persistence:
```typescript
// Messages are automatically saved during generation
// You can also query them:
const messages = await agent.memory.getMessages({
chatId: 'chat_123',
limit: 50,
})
// Manage chat sessions
await agent.memory.saveChat({
chatId: 'chat_123',
userId: 'user_001',
title: 'TypeScript Help',
createdAt: new Date(),
updatedAt: new Date(),
messageCount: 12,
})
const chats = await agent.memory.getChats({
userId: 'user_001',
})
await agent.memory.updateChatTitle('chat_123', 'Updated Title')
await agent.memory.deleteChat('chat_123')
```
***
## Built-in Adapters
### In-Memory Adapter
For development, testing, and short-lived applications. Data is lost on restart.
```typescript
import { IgniterAgentInMemoryAdapter } from '@igniter-js/agents'
const adapter = IgniterAgentInMemoryAdapter.create({
namespace: 'dev', // Namespace for multi-app isolation
maxMessages: 1000, // Max messages per chat (default: 1000)
maxChats: 100, // Max chat sessions (default: 100)
})
// Utility methods
await adapter.clear() // Clear all data
console.log(adapter.isConnected()) // Always true
```
### JSON File Adapter
For single-machine production deployments. Data persists to a JSON file.
```typescript
import { IgniterAgentJSONFileAdapter } from '@igniter-js/agents'
const adapter = IgniterAgentJSONFileAdapter.create({
filePath: './data/agent-memory.json',
namespace: 'prod',
})
// Data is automatically persisted to disk
// Survives process restarts
```
JSON File adapter is suitable for single-machine deployments. For multi-server architectures, use a shared backend like Redis or PostgreSQL via a custom adapter.
***
## Building a Custom Adapter
Implement the `IgniterAgentMemoryProvider` interface to create a custom adapter:
```typescript
import type {
IgniterAgentMemoryProvider,
IgniterAgentWorkingMemory,
IgniterAgentConversationMessage,
IgniterAgentChatSession,
IgniterAgentWorkingMemoryParams,
IgniterAgentUpdateWorkingMemoryParams,
IgniterAgentGetMessagesParams,
IgniterAgentGetChatsParams,
} from '@igniter-js/agents'
class RedisMemoryAdapter implements IgniterAgentMemoryProvider {
constructor(private redis: Redis) {}
// Working memory
async getWorkingMemory(params: IgniterAgentWorkingMemoryParams) {
const key = `wm:${params.scope}:${params.identifier}`
return this.redis.get(key).then(JSON.parse)
}
async updateWorkingMemory(params: IgniterAgentUpdateWorkingMemoryParams) {
const key = `wm:${params.scope}:${params.identifier}`
await this.redis.set(key, JSON.stringify({
content: params.content,
updatedAt: new Date().toISOString(),
}))
}
// Messages
async saveMessage(message: IgniterAgentConversationMessage) {
const key = `msgs:${message.chatId}`
await this.redis.lpush(key, JSON.stringify(message))
await this.redis.ltrim(key, 0, 999) // Keep last 1000
}
async getMessages(params: IgniterAgentGetMessagesParams) {
const key = `msgs:${params.chatId}`
const raw = await this.redis.lrange(key, 0, params.limit ?? 50)
return raw.map(m => JSON.parse(m)) as T[]
}
// Chat sessions
async saveChat(chat: IgniterAgentChatSession) {
await this.redis.hset('chats', chat.chatId, JSON.stringify(chat))
}
async getChats(params: IgniterAgentGetChatsParams) {
const raw = await this.redis.hvals('chats')
return raw.map(c => JSON.parse(c))
}
async getChat(chatId: string) {
const raw = await this.redis.hget('chats', chatId)
return raw ? JSON.parse(raw) : null
}
async updateChatTitle(chatId: string, title: string) {
const chat = await this.getChat(chatId)
if (chat) {
chat.title = title
await this.saveChat(chat)
}
}
async deleteChat(chatId: string) {
await this.redis.hdel('chats', chatId)
await this.redis.del(`msgs:${chatId}`)
}
}
```
### Memory Provider Interface
| Method | Required | Description |
| -------------------------------- | -------- | ----------------------------------- |
| `getWorkingMemory(params)` | No | Retrieve scoped working memory |
| `updateWorkingMemory(params)` | No | Update/create scoped working memory |
| `saveMessage(message)` | No | Save a conversation message |
| `getMessages(params)` | No | Get messages for a chat |
| `saveChat(chat)` | No | Save/update a chat session |
| `getChats(params)` | No | List chat sessions |
| `getChat(chatId)` | No | Get a single chat session |
| `updateChatTitle(chatId, title)` | No | Update chat title |
| `deleteChat(chatId)` | No | Delete a chat and its messages |
***
## Telemetry & Memory
When telemetry is enabled, every memory operation emits structured events:
| Event | Attributes |
| ---------------------------------------- | ------------------------------------------------------------ |
| `igniter.agent.memory.operation.started` | `ctx.agent.name`, `ctx.memory.operation`, `ctx.memory.scope` |
| `igniter.agent.memory.operation.success` | + `ctx.memory.durationMs`, `ctx.memory.count` |
| `igniter.agent.memory.operation.error` | + `ctx.error.code`, `ctx.error.message` |
---
# Multi-Agent Orchestration
> Manage multiple specialized agents with a single manager. Agent registration, batch start/stop, shared telemetry, lifecycle hooks, and routing.
URL: https://igniterjs.com/docs/agents/multi-agent
## Multi-Agent Orchestration
`IgniterAgentManager` provides centralized orchestration for multiple agents. It shares common resources (logger, telemetry, hooks), manages lifecycle in batch, and enables request routing between agents.
***
## The Manager Builder
Like the agent builder, the manager uses a fluent API:
```typescript
import {
IgniterAgentManager,
IgniterAgent,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
// Create specialized agents
const supportAgent = IgniterAgent
.create('support')
.withModel(openai('gpt-4o'))
.withPrompt(IgniterAgentPrompt.create('You are customer support.'))
.build()
const salesAgent = IgniterAgent
.create('sales')
.withModel(openai('gpt-4o'))
.withPrompt(IgniterAgentPrompt.create('You are a sales representative.'))
.build()
const codeAgent = IgniterAgent
.create('code')
.withModel(openai('gpt-4o'))
.addToolset(codeToolset)
.build()
// Create the manager
const manager = IgniterAgentManager
.create()
.addAgent('support', supportAgent)
.addAgent('sales', salesAgent)
.addAgent('code', codeAgent)
.withAutoStart(true) // Start agents immediately
.withContinueOnError(true) // Don't fail all if one errors
.build()
```
***
## Manager Builder API
| Method | Description |
| ---------------------------- | --------------------------------------------------------------- |
| `.create()` | Creates a new manager builder |
| `.addAgent(name, agent)` | Registers an agent (type-safe — names are tracked in the type) |
| `.withLogger(logger)` | Shared logger for all agents |
| `.withTelemetry(telemetry)` | Shared telemetry for all agents |
| `.withAutoStart(bool)` | Auto-start agents on registration (default: `false`) |
| `.withContinueOnError(bool)` | Continue if an agent fails during batch start (default: `true`) |
| `.onAgentStart(callback)` | Fired when any agent starts |
| `.onAgentError(callback)` | Fired when any agent errors |
| `.onToolCallStart(callback)` | Fired when any tool call starts |
| `.onToolCallEnd(callback)` | Fired when any tool call completes |
| `.onToolCallError(callback)` | Fired when any tool call fails |
| `.onMCPStart(callback)` | Fired when any MCP connection starts |
| `.onMCPError(callback)` | Fired when any MCP connection fails |
| `.build()` | Returns the built manager |
***
## Agent Registration & Lifecycle
### Programmatic Registration
Agents can also be registered after the manager is built:
```typescript
const manager = IgniterAgentManager.create().build()
manager.register('support', supportAgent)
manager.register('sales', salesAgent)
// Check registration
console.log(manager.has('support')) // true
console.log(manager.has('unknown')) // false
// Unregister an agent
manager.unregister('old-agent')
```
Registering an agent with a name that already exists throws `IgniterAgentError` with code `INVALID_CONFIG`.
### Starting and Managing Agents
```typescript
// Start a single agent
await manager.start('support')
// Start all agents in parallel
const results = await manager.startAll()
for (const [name, result] of results) {
if (result instanceof Error) {
console.error(`❌ ${name} failed:`, result.message)
} else {
console.log(`✅ ${name} started with ${Object.keys(result).length} toolsets`)
}
}
// Get agent names
console.log(manager.getNames()) // ['support', 'sales', 'code']
// Get agent status
console.log(manager.getStatus())
// → [{ name: 'support', status: 'running', ... }, ...]
// Get failed agents
console.log(manager.getFailedAgents())
// → [{ name: 'code', error: Error(...), ... }]
```
***
## Using Agents via the Manager
### Direct Access
```typescript
// Get an agent by name
const support = manager.get('support')
const result = await support.generate({
chatId: 'chat_001',
userId: 'user_001',
context: {},
message: { role: 'user', content: 'I need help with my order' },
})
console.log(result.content)
```
### Agent Routing Pattern
Build a router that selects the right agent based on the request:
```typescript
type AgentName = 'support' | 'sales' | 'code'
function routeRequest(intent: string): AgentName {
switch (intent) {
case 'help':
case 'refund':
return 'support'
case 'pricing':
case 'demo':
return 'sales'
case 'debug':
case 'code':
return 'code'
default:
return 'support'
}
}
async function handleRequest(intent: string, message: string) {
const agentName = routeRequest(intent)
const agent = manager.get(agentName)
return agent.generate({
chatId: `chat_${Date.now()}`,
userId: 'user_001',
context: { intent },
message: { role: 'user', content: message },
})
}
```
***
## Shared Hooks & Observability
Manager-level hooks apply to all registered agents:
```typescript
const manager = IgniterAgentManager
.create()
.addAgent('support', supportAgent)
.addAgent('sales', salesAgent)
.onAgentStart((name) => {
console.log(`🚀 Agent '${name}' started`)
})
.onAgentError((name, error) => {
console.error(`💥 Agent '${name}' failed:`, error.message)
})
.onToolCallStart((agentName, toolName, input) => {
console.log(`🔧 [${agentName}] → ${toolName}`)
})
.onToolCallEnd((agentName, toolName, output) => {
console.log(`✅ [${agentName}] ← ${toolName}`)
})
.build()
```
### Shared Telemetry
When telemetry is configured on the manager, it's automatically propagated to all agents:
```typescript
import { IgniterTelemetry } from '@igniter-js/telemetry'
const telemetry = IgniterTelemetry.create().build()
const manager = IgniterAgentManager
.create()
.withTelemetry(telemetry)
.withLogger(myLogger)
.addAgent('support', supportAgent)
.addAgent('sales', salesAgent)
.build()
// All agents now share the same telemetry and logger
```
***
## Agent Status Lifecycle
Each agent tracked by the manager goes through these states:
| Status | Description |
| ---------- | ------------------------------------------- |
| `idle` | Registered but not yet started |
| `starting` | Currently initializing MCP connections |
| `running` | Successfully started and ready for requests |
| `error` | Failed to start or encountered an error |
```typescript
const status = manager.getStatus()
status.forEach(s => {
console.log(`${s.name}: ${s.status}`)
console.log(` Registered: ${s.registeredAt}`)
console.log(` Started: ${s.startedAt ?? 'N/A'}`)
console.log(` Toolsets: ${s.toolsetCount}`)
if (s.error) {
console.log(` Error: ${s.error.message}`)
}
})
```
***
## Complete Multi-Agent System
```typescript
import {
IgniterAgent,
IgniterAgentManager,
IgniterAgentTool,
IgniterAgentToolset,
IgniterAgentPrompt,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
// Support agent with a knowledge base tool
const searchKB = IgniterAgentTool
.create('search_kb')
.withDescription('Search the support knowledge base')
.withInput(z.object({ query: z.string() }))
.withExecute(async ({ query }) => {
return kb.search(query)
})
.build()
const supportAgent = IgniterAgent
.create('support')
.withModel(openai('gpt-4o'))
.withPrompt(IgniterAgentPrompt.create('You are Tier 1 support.'))
.addToolset(
IgniterAgentToolset.create('kb').addTool(searchKB).build()
)
.build()
// Code agent with an execution tool
const runCode = IgniterAgentTool
.create('run')
.withDescription('Execute code in a sandbox')
.withInput(z.object({ code: z.string(), language: z.string() }))
.withExecute(async ({ code, language }) => {
return sandbox.execute(code, language)
})
.build()
const codeAgent = IgniterAgent
.create('code')
.withModel(openai('gpt-4o'))
.withPrompt(IgniterAgentPrompt.create('You are a senior developer.'))
.addToolset(
IgniterAgentToolset.create('sandbox').addTool(runCode).build()
)
.build()
// Manager orchestrates both
const manager = IgniterAgentManager
.create()
.addAgent('support', supportAgent)
.addAgent('code', codeAgent)
.withContinueOnError(true)
.onAgentStart((name) => console.log(`✅ ${name} is live`))
.onAgentError((name, err) => console.error(`❌ ${name}:`, err.message))
.build()
// Start all agents
const results = await manager.startAll()
console.log('Running agents:', manager.getNames())
```
---
# Prompts
> Create dynamic system prompts with {{variable}} interpolation, appended prompts for modular instruction composition, and context-aware templates.
URL: https://igniterjs.com/docs/agents/prompts
## Prompts
`IgniterAgentPrompt` provides a lightweight template system for building agent system instructions with `{{variable}}` interpolation and support for composing prompts from multiple sources.
***
## Basic Templates
Use `{{variable}}` syntax to inject context data into your prompts:
```typescript
import { IgniterAgentPrompt, IgniterAgent } from '@igniter-js/agents'
const prompt = IgniterAgentPrompt
.create('You are {{role}}. Your tone is {{tone}}. User: {{user.name}}')
const agent = IgniterAgent
.create('bot')
.withModel(openai('gpt-4o'))
.withPrompt(prompt)
.build()
const result = await agent.generate({
chatId: 'chat_001',
userId: 'user_001',
context: {
role: 'customer support',
tone: 'friendly and professional',
user: { name: 'Alice' },
},
message: { role: 'user', content: 'Hello!' },
})
// System prompt resolves to:
// "You are customer support. Your tone is friendly and professional. User: Alice"
```
### How Interpolation Works
* Variables are resolved by dot-path from the `context` object
* `{{role}}` → `context.role`
* `{{user.name}}` → `context.user.name`
* `{{company.address.city}}` → `context.company.address.city`
* Missing variables resolve to an empty string `""`
* `null` values resolve to an empty string
Variable names are case-sensitive and must match the context keys exactly. `{{role}}` and `{{Role}}` are different variables.
***
## Dynamic Prompt Examples
### Multi-Language Support
```typescript
const multilingualPrompt = IgniterAgentPrompt.create(`
You are a {{role}}.
Always respond in {{language}}.
{{#preferences.formal}}
Use formal language.
{{/preferences.formal}}
`)
const result = await agent.generate({
chatId: 'chat_001',
userId: 'user_001',
context: {
role: 'travel agent',
language: 'Portuguese',
},
message: { role: 'user', content: 'Find me a flight' },
})
```
### Role-Based Prompts
```typescript
const roleAwarePrompt = IgniterAgentPrompt.create(`
You are {{agent.name}}, a {{agent.role}}.
Capabilities:
{{agent.capabilities}}
Current user: {{user.email}}
Role: {{user.role}}
`)
// Different contexts produce different prompts
await agent.generate({
chatId: 'chat_001',
userId: 'user_admin',
context: {
agent: {
name: 'Atlas',
role: 'system administrator',
capabilities: '- Manage users\n- Configure database\n- Monitor logs',
},
user: { email: 'admin@corp.com', role: 'admin' },
},
message: { role: 'user', content: 'Check system status' },
})
```
***
## Appended Prompts
Compose prompts from multiple sources using `.addAppended()`:
```typescript
import { IgniterAgentPrompt } from '@igniter-js/agents'
// Base prompt
const basePrompt = IgniterAgentPrompt.create(`
You are {{agent.role}}.
`)
// Add capability-specific prompts
const prompt = basePrompt
.addAppended('tools', `
Available tools: {{agent.tools}}
`)
.addAppended('rules', `
Rules:
- Never share sensitive data
- Always ask for confirmation before destructive actions
`)
.addAppended('context', `
Current date: {{system.date}}
Environment: {{system.env}}
`)
// Full prompt when built:
// You are developer assistant.
//
// Available tools: github, docker, database
//
// Rules:
// - Never share sensitive data
// - Always ask for confirmation before destructive actions
//
// Current date: 2026-06-02
// Environment: production
```
### Managing Appended Prompts
```typescript
// Remove an appended prompt
const withoutContext = prompt.removeAppended('context')
// Check what's appended
console.log(prompt.getAppended()) // { tools: ..., rules: ..., context: ... }
// Get the base template
console.log(prompt.getTemplate()) // "You are {{agent.role}}."
```
Appended prompts are joined with `\n\n` and interpolated with the same context. Each appended prompt can be a string or another `IgniterAgentPrompt` instance.
***
## Pattern: Composable Prompt Library
Build a library of reusable prompt snippets:
```typescript
// lib/prompts/base.ts
export const basePrompt = (role: string) =>
IgniterAgentPrompt.create(`You are ${role}.`)
// lib/prompts/security.ts
export const securityRules = IgniterAgentPrompt.create(`
Security Rules:
- Never expose API keys or secrets
- Validate all user input
- Log all data access
`)
// lib/prompts/tool-usage.ts
export const toolUsageGuide = IgniterAgentPrompt.create(`
Tool Usage:
- Call tools when you need external data
- Handle tool errors gracefully
- Cite tool sources in responses
`)
```
```typescript
// Compose for a specific agent
import { basePrompt, securityRules, toolUsageGuide } from './lib/prompts'
const prompt = basePrompt('security analyst')
.addAppended('security', securityRules)
.addAppended('tools', toolUsageGuide)
const agent = IgniterAgent
.create('security-bot')
.withModel(openai('gpt-4o'))
.withPrompt(prompt)
.build()
```
***
## API Reference
### `IgniterAgentPromptBuilder`
| Method | Description |
| --------------------------- | ----------------------------------------------------- |
| `.create(template)` | Creates a new prompt builder with a template string |
| `.build(context)` | Renders the template with the provided context data |
| `.addAppended(key, prompt)` | Appends another prompt (string or IgniterAgentPrompt) |
| `.removeAppended(key)` | Removes an appended prompt by key |
| `.getAppended()` | Returns the appended prompts object |
| `.getTemplate()` | Returns the raw template string |
---
# Quick Start
> Build your first Igniter.js agent in under 5 minutes. Three complete scenarios basic agent with tools, agent with persistent memory, and agent with MCP integration.
URL: https://igniterjs.com/docs/agents/quick-start
## Quick Start
Choose your scenario below. Each takes under 5 minutes from zero to a working agent.
Make sure you've completed the [installation](/docs/agents/installation) step first.
***
## Scenario 1: Basic Agent with a Custom Tool
The most common pattern — define a tool, group it in a toolset, and wire it into an agent.
### Import Dependencies
```typescript
import {
IgniterAgent,
IgniterAgentTool,
IgniterAgentToolset,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
```
### Define a Tool
Every tool needs a **name**, **description** (the AI uses this to decide when to call it), **input schema** (Zod), and an **execute handler**.
```typescript
const weatherTool = IgniterAgentTool
.create('get_weather')
.withDescription('Get current weather for a location')
.withInput(
z.object({
location: z.string().describe('City name or coordinates'),
unit: z.enum(['C', 'F']).default('C'),
})
)
.withExecute(async ({ location, unit }) => {
// Replace with your real weather API call
const response = await fetch(
`https://api.weather.example/current?q=${location}&units=${unit}`
)
return response.json()
})
.build()
```
### Create a Toolset
Toolsets group related tools by domain. The toolset name becomes a prefix for each tool when the AI sees them.
```typescript
const weatherToolset = IgniterAgentToolset
.create('weather')
.addTool(weatherTool)
.build()
```
### Build and Run the Agent
```typescript
const agent = IgniterAgent
.create('weather-assistant')
.withModel(openai('gpt-4o-mini'))
.addToolset(weatherToolset)
.build()
// Start initializes MCP connections (none here, but always call it)
await agent.start()
// Generate a response — the AI decides whether to use the tool
const result = await agent.generate({
chatId: 'chat_001',
userId: 'user_001',
context: {},
message: {
role: 'user',
content: "What's the weather like in Tokyo?",
},
})
console.log(result.content)
```
**✅ Success!** You've built an agent that can decide when to call your tool and use the result in its response.
***
## Scenario 2: Agent with Persistent Memory
For multi-turn conversations where the agent needs to remember past interactions.
### Create a Memory Adapter
```typescript
import {
IgniterAgent,
IgniterAgentInMemoryAdapter,
IgniterAgentPrompt,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
const memoryAdapter = IgniterAgentInMemoryAdapter.create({
namespace: 'my-app',
maxChats: 50,
})
```
`IgniterAgentInMemoryAdapter` is great for development. For production, use `IgniterAgentJSONFileAdapter` or build a custom adapter (Redis, PostgreSQL, etc.).
### Configure Memory and Build Agent
```typescript
const memoryConfig = {
provider: memoryAdapter,
working: { enabled: true, scope: 'chat' },
history: { enabled: true, limit: 100 },
chats: { enabled: true },
}
const agent = IgniterAgent
.create('memory-bot')
.withModel(openai('gpt-4o-mini'))
.withMemory(memoryConfig)
.withPrompt(
IgniterAgentPrompt.create(
'You are a helpful assistant. Remember details about the user across messages.'
)
)
.build()
await agent.start()
```
### Run a Multi-Turn Conversation
```typescript
// First turn
let response = await agent.generate({
chatId: 'chat_memory_001',
userId: 'user_001',
context: {},
message: {
role: 'user',
content: 'My name is Alice and my favorite color is blue.',
},
})
console.log('Turn 1:', response.content)
// Second turn — agent remembers from the first
response = await agent.generate({
chatId: 'chat_memory_001',
userId: 'user_001',
context: {},
message: {
role: 'user',
content: "What's my name and favorite color?",
},
})
console.log('Turn 2:', response.content)
// → "Your name is Alice and your favorite color is blue."
```
**✅ Success!** The agent maintains context across turns using the memory adapter.
***
## Scenario 3: Agent with MCP Integration
Connect to external tools via the Model Context Protocol.
### Install MCP Dependencies
```bash
npm install @ai-sdk/mcp @modelcontextprotocol/sdk
```
### Configure an MCP Client
```typescript
import {
IgniterAgent,
IgniterAgentMCPClient,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
// Stdio transport — spawns a local process
const filesystemMCP = IgniterAgentMCPClient
.create('filesystem')
.withType('stdio')
.withCommand('npx')
.withArgs([
'-y',
'@modelcontextprotocol/server-filesystem',
'/tmp/agent-workspace',
])
.withEnv({ DEBUG: 'mcp:*' })
.build()
// HTTP transport — connects to a remote server
const remoteMCP = IgniterAgentMCPClient
.create('opencode')
.withType('http')
.withURL('https://sandbox.example.com/mcp')
.withHeaders({
'X-API-Key': process.env.MCP_API_KEY!,
})
.build()
```
### Build and Use the Agent
```typescript
const agent = IgniterAgent
.create('mcp-agent')
.withModel(openai('gpt-4o-mini'))
.addMCP(filesystemMCP)
.addMCP(remoteMCP)
.build()
// MCP connections are established here
await agent.start()
const result = await agent.generate({
chatId: 'chat_mcp_001',
userId: 'user_001',
context: {},
message: {
role: 'user',
content: 'List all files in my workspace directory.',
},
})
console.log(result.content)
```
**✅ Success!** Your agent can now use tools from remote MCP servers, discovered at runtime.
***
## Next Steps
* [Building Agents](/docs/agents/building-agents) — Full agent builder API reference
* [Tools & Toolsets](/docs/agents/tools-toolsets) — Creating type-safe tools in depth
* [Memory System](/docs/agents/memory) — Working memory, chat history, and custom adapters
* [MCP Integration](/docs/agents/mcp-integration) — Stdio, HTTP transports, and server discovery
---
# Real-World Examples
> Production-ready agent patterns. Customer support triage, code review assistant, data analyst with database tools, e-commerce concierge, and multi-agent workflow orchestrator.
URL: https://igniterjs.com/docs/agents/real-world
## Real-World Examples
Five complete, production-ready agent patterns that demonstrate the full capabilities of `@igniter-js/agents`.
***
## 1. Customer Support Triage Agent
An agent that classifies support tickets, searches the knowledge base, and escalates when needed.
```typescript
import {
IgniterAgent,
IgniterAgentTool,
IgniterAgentToolset,
IgniterAgentPrompt,
IgniterAgentInMemoryAdapter,
} from '@igniter-js/agents'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
// Tool: Search knowledge base
const searchKB = IgniterAgentTool
.create('search_kb')
.withDescription('Search the support knowledge base for relevant articles')
.withInput(
z.object({
query: z.string().describe('Search query'),
category: z.enum(['billing', 'technical', 'account']).optional(),
})
)
.withExecute(async ({ query, category }) => {
const articles = await knowledgeBase.search(query, { category })
return articles.map(a => ({
id: a.id,
title: a.title,
relevance: a.score,
summary: a.summary,
}))
})
.build()
// Tool: Create escalation ticket
const createEscalation = IgniterAgentTool
.create('escalate')
.withDescription('Create an escalation ticket when the issue cannot be resolved')
.withInput(
z.object({
priority: z.enum(['high', 'critical']),
summary: z.string(),
category: z.string(),
evidence: z.string().optional(),
})
)
.withExecute(async ({ priority, summary, category, evidence }) => {
const ticket = await ticketing.create({
priority,
summary,
category,
evidence,
source: 'ai-triage',
})
return { ticketId: ticket.id, status: 'escalated' }
})
.build()
// Tool: Check order status
const checkOrder = IgniterAgentTool
.create('check_order')
.withDescription('Look up an order by ID and return its status')
.withInput(z.object({ orderId: z.string() }))
.withExecute(async ({ orderId }) => {
const order = await orders.findById(orderId)
if (!order) return { found: false }
return {
found: true,
status: order.status,
items: order.items.length,
estimatedDelivery: order.estimatedDelivery,
}
})
.build()
// Build the support toolset
const supportToolset = IgniterAgentToolset
.create('support')
.addTool(searchKB)
.addTool(createEscalation)
.addTool(checkOrder)
.build()
// Memory for tracking support history
const memory = IgniterAgentInMemoryAdapter.create({
namespace: 'support',
maxChats: 1000,
})
// The triage agent
const triageAgent = IgniterAgent
.create('support-triage')
.withModel(openai('gpt-4o'))
.withPrompt(
IgniterAgentPrompt.create(`
You are a Tier 1 support triage agent for {{company}}.
Your process:
1. Greet the customer warmly
2. Understand their issue
3. Search the knowledge base for solutions
4. If unresolved, escalate with clear evidence
5. For order issues, always check the order first
Tone: {{tone}}
`)
)
.addToolset(supportToolset)
.withMemory({
provider: memory,
working: { enabled: true, scope: 'ticket' },
history: { enabled: true, limit: 200 },
chats: { enabled: true },
})
.onToolCallStart((agent, tool) => {
console.log(`[Support] Starting: ${tool}`)
})
.build()
await triageAgent.start()
// Handle a customer request
const result = await triageAgent.generate({
chatId: 'ticket_0042',
userId: 'cust_789',
context: {
company: 'ShopFast',
tone: 'empathetic and solution-oriented',
},
message: {
role: 'user',
content: "Hi, I haven't received my order #ORD-5591. It was supposed to arrive yesterday.",
},
})
```
***
## 2. Code Review Assistant
An agent with source code analysis tools for automated code review.
```typescript
// Tool: Run linter
const runLinter = IgniterAgentTool
.create('lint')
.withDescription('Run ESLint on the provided code and return issues')
.withInput(
z.object({
code: z.string().describe('Source code to lint'),
filepath: z.string().describe('Virtual filepath for rule matching'),
})
)
.withExecute(async ({ code, filepath }) => {
const results = await eslint.lintText(code, { filePath: filepath })
return results.map(r => ({
file: filepath,
line: r.line,
column: r.column,
rule: r.ruleId,
message: r.message,
severity: r.severity === 2 ? 'error' : 'warning',
fix: r.fix ? 'available' : null,
}))
})
.build()
// Tool: Run type check
const typeCheck = IgniterAgentTool
.create('typecheck')
.withDescription('Run TypeScript type checking on code')
.withInput(
z.object({
code: z.string(),
filename: z.string().default('temp.ts'),
})
)
.withExecute(async ({ code, filename }) => {
const diagnostics = await ts.createProgram([filename], {
noEmit: true,
strict: true,
})
return diagnostics.map(d => ({
line: d.line,
message: d.messageText,
code: d.code,
}))
})
.build()
// Tool: Suggest improvements
const suggestRefactor = IgniterAgentTool
.create('suggest')
.withDescription('Suggest improvements based on code patterns')
.withInput(z.object({ code: z.string(), context: z.string().optional() }))
.withExecute(async ({ code, context }) => {
// Pattern matching for common anti-patterns
const patterns = [
{ regex: /\.then\(.*\.catch\(/g, suggestion: 'Use async/await instead of .then chains' },
{ regex: /var\s+/g, suggestion: 'Use const or let instead of var' },
{ regex: /console\.log/g, suggestion: 'Consider using a proper logger' },
]
return patterns
.filter(p => p.regex.test(code))
.map(p => ({ pattern: p.regex.source, suggestion: p.suggestion }))
})
.build()
const reviewToolset = IgniterAgentToolset
.create('review')
.addTool(runLinter)
.addTool(typeCheck)
.addTool(suggestRefactor)
.build()
const codeReviewer = IgniterAgent
.create('code-reviewer')
.withModel(openai('gpt-4o'))
.withPrompt(
IgniterAgentPrompt.create(`
You are a senior TypeScript code reviewer.
For each review:
1. Run the linter to check for style issues
2. Run the type checker for type safety
3. Identify improvement patterns
4. Provide actionable, specific feedback
Always reference exact line numbers and suggest concrete fixes.
`)
)
.addToolset(reviewToolset)
.build()
await codeReviewer.start()
```
***
## 3. Data Analyst Agent
An agent that can query databases and generate reports.
```typescript
// Tool: Execute SQL query
const queryDB = IgniterAgentTool
.create('query')
.withDescription('Execute a read-only SQL query on the analytics database')
.withInput(
z.object({
sql: z.string().describe('SELECT query'),
params: z.array(z.unknown()).optional(),
maxRows: z.number().default(100).describe('Maximum rows to return'),
})
)
.withExecute(async ({ sql, params, maxRows }) => {
const result = await analyticsDB.query({
text: sql,
values: params,
rowMode: 'array',
maxRows,
})
return {
columns: result.fields.map(f => f.name),
rows: result.rows,
rowCount: result.rowCount,
}
})
.build()
// Tool: Generate chart config
const generateChart = IgniterAgentTool
.create('chart')
.withDescription('Generate a chart configuration for data visualization')
.withInput(
z.object({
chartType: z.enum(['bar', 'line', 'pie', 'scatter']),
title: z.string(),
xAxis: z.string(),
yAxis: z.string(),
data: z.array(z.record(z.unknown())),
})
)
.withExecute(async (config) => {
return { chartConfig: config, renderable: true }
})
.build()
const analyticsToolset = IgniterAgentToolset
.create('analytics')
.addTool(queryDB)
.addTool(generateChart)
.build()
const dataAnalyst = IgniterAgent
.create('data-analyst')
.withModel(openai('gpt-4o'))
.withPrompt(
IgniterAgentPrompt.create(`
You are a data analyst. When given a question:
1. Formulate a SQL query to answer it
2. Execute the query
3. Interpret the results in plain language
4. Suggest a chart if the data warrants it
`)
)
.addToolset(analyticsToolset)
.build()
await dataAnalyst.start()
const report = await dataAnalyst.generate({
chatId: 'report_012',
userId: 'analyst_001',
context: {},
message: {
role: 'user',
content: 'Show me monthly revenue by product category for Q1 2026.',
},
})
```
***
## 4. E-Commerce Concierge
A shopping assistant with product search, cart management, and recommendations.
```typescript
const searchProducts = IgniterAgentTool
.create('search_products')
.withDescription('Search the product catalog')
.withInput(
z.object({
query: z.string(),
category: z.string().optional(),
maxPrice: z.number().optional(),
inStock: z.boolean().default(true),
limit: z.number().default(5),
})
)
.withExecute(async (filters) => {
return catalog.search(filters)
})
.build()
const addToCart = IgniterAgentTool
.create('add_to_cart')
.withDescription('Add a product to the shopping cart')
.withInput(
z.object({
productId: z.string(),
quantity: z.number().min(1).max(10).default(1),
})
)
.withExecute(async ({ productId, quantity }) => {
return cart.add(currentUserId, productId, quantity)
})
.build()
const getRecommendations = IgniterAgentTool
.create('recommend')
.withDescription('Get personalized product recommendations')
.withInput(z.object({ basedOn: z.string().optional() }))
.withExecute(async ({ basedOn }) => {
return recommendations.forUser(currentUserId, { context: basedOn })
})
.build()
const shopToolset = IgniterAgentToolset
.create('shop')
.addTool(searchProducts)
.addTool(addToCart)
.addTool(getRecommendations)
.build()
const concierge = IgniterAgent
.create('shopping-concierge')
.withModel(openai('gpt-4o'))
.withPrompt(
IgniterAgentPrompt.create(`
You are {{store}}'s personal shopping concierge.
Guidelines:
- Be helpful but never pushy
- Always show prices clearly
- Offer alternatives when items are out of stock
- Suggest complementary products
`)
)
.addToolset(shopToolset)
.withMemory({
provider: IgniterAgentInMemoryAdapter.create({ namespace: 'shop' }),
working: { enabled: true, scope: 'session' },
history: { enabled: true, limit: 200 },
chats: { enabled: true },
})
.build()
```
***
## 5. Multi-Agent Workflow Orchestrator
A system that routes complex tasks through a chain of specialized agents.
```typescript
// Manager with 3 specialized agents
const orchestrator = IgniterAgentManager
.create()
.addAgent('researcher', researchAgent)
.addAgent('writer', writerAgent)
.addAgent('reviewer', reviewerAgent)
.onAgentStart((name) => console.log(`✅ ${name} ready`))
.onToolCallStart((agent, tool) => console.log(`[${agent}] → ${tool}`))
.build()
await orchestrator.startAll()
// Orchestrate a research → write → review pipeline
async function runPipeline(topic: string) {
// Phase 1: Research
const researcher = orchestrator.get('researcher')
const research = await researcher.generate({
chatId: `pipeline_${Date.now()}`,
userId: 'system',
context: { topic },
message: {
role: 'user',
content: `Research the topic: ${topic}. Gather key facts, data points, and perspectives.`,
},
})
// Phase 2: Write based on research
const writer = orchestrator.get('writer')
const draft = await writer.generate({
chatId: `pipeline_${Date.now()}`,
userId: 'system',
context: { research: research.content, topic },
message: {
role: 'user',
content: `Write a comprehensive article on "${topic}" using this research:\n${research.content}`,
},
})
// Phase 3: Review the draft
const reviewer = orchestrator.get('reviewer')
const review = await reviewer.generate({
chatId: `pipeline_${Date.now()}`,
userId: 'system',
context: { draft: draft.content },
message: {
role: 'user',
content: `Review this draft for accuracy, clarity, and completeness:\n${draft.content}`,
},
})
return { research, draft, review }
}
```
***
## 6. DevOps Incident Responder
An agent that helps diagnose and resolve production incidents.
```typescript
const checkLogs = IgniterAgentTool
.create('check_logs')
.withDescription('Query application logs for error patterns')
.withInput(
z.object({
service: z.string(),
severity: z.enum(['error', 'warn']).default('error'),
timeframe: z.string().default('15m'),
limit: z.number().default(20),
})
)
.withExecute(async ({ service, severity, timeframe, limit }) => {
return logAggregator.query({ service, severity, timeframe, limit })
})
.build()
const checkMetrics = IgniterAgentTool
.create('check_metrics')
.withDescription('Query service metrics (CPU, memory, latency, error rate)')
.withInput(z.object({ service: z.string(), metric: z.string().optional() }))
.withExecute(async ({ service, metric }) => {
return metricsService.query(service, metric)
})
.build()
const runDiagnostic = IgniterAgentTool
.create('diagnose')
.withDescription('Run a diagnostic check on a service')
.withInput(z.object({ service: z.string(), check: z.string() }))
.withExecute(async ({ service, check }) => {
return diagnostics.run(service, check)
})
.build()
const devopsToolset = IgniterAgentToolset
.create('devops')
.addTool(checkLogs)
.addTool(checkMetrics)
.addTool(runDiagnostic)
.build()
const incidentResponder = IgniterAgent
.create('incident-responder')
.withModel(openai('gpt-4o'))
.withPrompt(
IgniterAgentPrompt.create(`
You are an on-call DevOps engineer responding to a production incident.
Protocol:
1. Check logs for error patterns
2. Check service metrics for anomalies
3. Run targeted diagnostics
4. Provide a root cause analysis and remediation steps
5. If you cannot determine the cause, recommend escalation
Be concise and actionable. Every second matters during an incident.
`)
)
.addToolset(devopsToolset)
.onToolCallStart((agent, tool, input) => {
incidentLog.info(`Running ${tool} with:`, input)
})
.build()
```
---
# Tools & Toolsets
> Create type-safe AI tools with Zod validation, group them into toolsets, and understand automatic error handling and result normalization.
URL: https://igniterjs.com/docs/agents/tools-toolsets
## Tools & Toolsets
Tools are the bridge between your AI agent and your application logic. Every tool has a name, description, typed input schema, and an execution handler. Toolsets group related tools by domain.
***
## Creating a Tool
### Tool Builder API
The `IgniterAgentTool` builder requires four things: a **name**, **description**, **input schema** (Zod), and **execute handler**.
```typescript
import { IgniterAgentTool } from '@igniter-js/agents'
import { z } from 'zod'
const greetTool = IgniterAgentTool
.create('greet')
.withDescription('Greet a user by name')
.withInput(
z.object({
name: z.string().describe('The user name'),
language: z.enum(['en', 'pt', 'es']).default('en'),
})
)
.withExecute(async ({ name, language }) => {
const greetings = { en: 'Hello', pt: 'Olá', es: 'Hola' }
return `${greetings[language]}, ${name}!`
})
.build()
```
The **description** is critical — it's what the AI model uses to decide when to invoke your tool. Be specific about what the tool does and when to use it.
### Tool Builder Methods
| Method | Required | Description |
| ------------------------ | -------- | --------------------------------------------- |
| `.create(name)` | ✅ | Unique tool name within a toolset |
| `.withDescription(text)` | ✅ | Human-readable description shown to the AI |
| `.withInput(zodSchema)` | ✅ | Zod schema defining input parameters |
| `.withOutput(zodSchema)` | No | Zod schema for output validation |
| `.withExecute(handler)` | ✅ | Async function that performs the tool's logic |
| `.build()` | ✅ | Returns the built tool definition |
### Execute Handler Signature
```typescript
type ExecuteHandler = (
params: TInput,
options: IgniterAgentToolExecuteOptions
) => Promise
```
The `params` object is **fully typed** — inferred from your Zod schema. The `options` object provides runtime context:
```typescript
interface IgniterAgentToolExecuteOptions {
toolCallId: string
messages: ModelMessage[]
abortSignal?: AbortSignal
}
```
### Type Inference in Action
Every tool builder change updates the TypeScript types:
```typescript
const tool = IgniterAgentTool
.create('search') // IgniterAgentToolBuilder<'search', unknown, unknown>
.withInput(z.object({ // ...<'search', { query: string, limit: number }, unknown>
query: z.string(),
limit: z.number().default(10),
}))
.withExecute(async (params) => {
// params is typed: { query: string; limit: number }
return { results: await search(params.query, params.limit) }
})
// ...<'search', ..., { results: string[] }>
.build()
```
***
## Creating a Toolset
Toolsets group related tools under a namespace. When registered with an agent, each tool is prefixed with the toolset name: `{toolset}_{tool}`.
```typescript
import { IgniterAgentToolset } from '@igniter-js/agents'
const utilsToolset = IgniterAgentToolset
.create('utils')
.addTool(greetTool)
.addTool(formatDateTool)
.addTool(validateEmailTool)
.build()
console.log(utilsToolset.name) // 'utils'
console.log(utilsToolset.type) // 'custom'
console.log(utilsToolset.status) // 'connected'
```
### Toolset Builder Methods
| Method | Description |
| ----------------- | ------------------------------------------------ |
| `.create(name)` | Creates a new toolset builder with a unique name |
| `.withName(name)` | Renames the toolset during building |
| `.addTool(tool)` | Adds a built tool to the toolset |
| `.build()` | Returns the built toolset |
| `.getName()` | Returns the current toolset name |
| `.getTools()` | Returns the current tools object |
| `.getToolCount()` | Returns the number of tools |
***
## Automatic Error Handling
Tools added to a toolset are **automatically wrapped** with error handling. The result is normalized into a consistent format:
```typescript
// Success result
{ success: true, data: }
// Error result
{ success: false, error: "" }
```
This means you don't need try/catch in every tool — the framework handles it:
```typescript
const riskyTool = IgniterAgentTool
.create('risky')
.withDescription('A tool that might fail')
.withInput(z.object({}))
.withExecute(async () => {
throw new Error('Something went wrong') // Caught automatically
})
.build()
// Will return { success: false, error: "Something went wrong" }
// The error is also logged via console.error
```
***
## Multiple Toolsets
Register multiple toolsets on a single agent — each creates a separate namespace:
```typescript
const githubToolset = IgniterAgentToolset
.create('github')
.addTool(createIssueTool)
.addTool(listReposTool)
.build()
const dockerToolset = IgniterAgentToolset
.create('docker')
.addTool(buildImageTool)
.addTool(deployContainerTool)
.build()
const agent = IgniterAgent
.create('devops-bot')
.withModel(openai('gpt-4o'))
.addToolset(githubToolset) // Tools: github_createIssue, github_listRepos
.addToolset(dockerToolset) // Tools: docker_buildImage, docker_deployContainer
.build()
```
Tool names must be unique **within** a toolset but can overlap **across** toolsets. The toolset prefix prevents collisions.
***
## Pattern: Reusable Tool Libraries
Extract common tools into shared modules for reuse across agents:
```typescript
// lib/tools/database.ts
import { IgniterAgentTool } from '@igniter-js/agents'
import { z } from 'zod'
export const queryDatabase = IgniterAgentTool
.create('query')
.withDescription('Execute a read-only SQL query')
.withInput(
z.object({
sql: z.string().describe('SQL SELECT statement'),
params: z.array(z.unknown()).optional(),
})
)
.withExecute(async ({ sql, params }) => {
return db.query(sql, params)
})
.build()
export const databaseToolset = IgniterAgentToolset
.create('db')
.addTool(queryDatabase)
.build()
```
```typescript
// In any agent:
import { databaseToolset } from './lib/tools/database'
const agent = IgniterAgent
.create('analyst')
.withModel(openai('gpt-4o'))
.addToolset(databaseToolset)
.build()
```
***
## Next Steps
* [Prompts](/docs/agents/prompts) — Template-based system instructions
* [MCP Integration](/docs/agents/mcp-integration) — External tool providers via MCP
* [Best Practices](/docs/agents/best-practices) — Do's and Don'ts for tool design
---
# Troubleshooting
> Common errors, their causes, and solutions when working with @igniter-js/agents. Debugging tips for tools, MCP connections, memory, and multi-agent setups.
URL: https://igniterjs.com/docs/agents/troubleshooting
## Troubleshooting
Common issues and their solutions when building agents with `@igniter-js/agents`.
***
## Agent Start Failures
### Error: `AGENT_NOT_INITIALIZED`
```
IgniterAgentError: Agent 'support' is not registered
code: AGENT_NOT_INITIALIZED
```
**Cause:** You're trying to access or start an agent that wasn't registered with the manager.
**Solution:** Register the agent with the manager before accessing it:
```typescript
const manager = IgniterAgentManager.create().build()
// Register first
manager.register('support', supportAgent)
// Then start
await manager.start('support')
```
### Error: `INVALID_CONFIG`
```
IgniterAgentConfigError: Tool name is required and must be a non-empty string
code: INVALID_CONFIG
field: name
```
**Cause:** A required configuration field is missing or invalid.
**Solution:** Check the `field` in the error metadata to identify what's missing:
```typescript
// Missing name
const tool = IgniterAgentTool.create('') // ❌ Empty string
// Missing description
const tool = IgniterAgentTool.create('myTool').build() // ❌ No description set
// Missing input schema
const tool = IgniterAgentTool
.create('myTool')
.withDescription('...')
.build() // ❌ No input schema
```
***
## MCP Connection Issues
### Error: MCP connection hangs or times out
**Cause:** The MCP server process isn't starting or the remote server is unreachable.
**Solutions:**
1. **Test the MCP command manually:**
```bash
npx -y @modelcontextprotocol/server-filesystem /tmp
# Should start and wait for input
```
2. **Add verbose debugging:**
```typescript
const mcpClient = IgniterAgentMCPClient
.create('filesystem')
.withType('stdio')
.withCommand('npx')
.withArgs(['-y', '@modelcontextprotocol/server-filesystem', '/tmp'])
.withEnv({ DEBUG: 'mcp:*' })
.build()
```
3. **Handle connection errors gracefully:**
```typescript
const agent = IgniterAgent.create('bot')
.addMCP(mcpClient)
.onMCPError((name, mcp, error) => {
console.error(`MCP '${mcp}' failed:`, error.message)
})
.build()
try {
await agent.start()
} catch (error) {
if (error.message.includes('ECONNREFUSED')) {
console.error('MCP server is not running. Start it first.')
}
}
```
### Error: MCP tools not available
**Cause:** MCP tools are only discovered at `start()` time.
**Solution:** Always call `agent.start()` before `agent.generate()`:
```typescript
const agent = IgniterAgent.create('bot')
.addMCP(filesystemMCP)
.build()
await agent.start() // ← Required: discovers MCP tools
const result = await agent.generate({...}) // ← MCP tools now available
```
***
## Memory Issues
### Error: `MEMORY_PROVIDER_ERROR`
```
IgniterAgentMemoryError: saveMessage is not supported by the provider
code: MEMORY_PROVIDER_ERROR
```
**Cause:** You're using a memory operation that the provider doesn't support.
**Solution:** Check the provider's supported methods. Not all providers implement every method. The in-memory adapter supports all methods; custom adapters may only implement a subset.
```typescript
// Check if method exists before calling
if (agent.memory) {
try {
await agent.memory.saveMessage({...})
} catch (error) {
if (error.code === 'MEMORY_PROVIDER_ERROR') {
// Method not supported — log and continue
console.warn('saveMessage not supported by this adapter')
}
}
}
```
### Issue: Memory lost on restart
**Cause:** Using `IgniterAgentInMemoryAdapter` in production.
**Solution:** Switch to `IgniterAgentJSONFileAdapter` for single-machine deployments or build a custom adapter for shared storage:
```typescript
// For single-machine production
import { IgniterAgentJSONFileAdapter } from '@igniter-js/agents'
const adapter = IgniterAgentJSONFileAdapter.create({
filePath: './data/agent-memory.json',
})
// For multi-server: build a Redis adapter (see Memory docs)
```
***
## Tool Execution Issues
### Error: Tool returns `{ success: false, error: "..." }`
**Cause:** The tool threw an error during execution. This is normal — the framework wraps all tool calls with error handling.
**Solution:** The error is automatically captured. The AI will see the error response and can handle it accordingly:
```typescript
const tool = IgniterAgentTool
.create('api_call')
.withDescription('...')
.withInput(z.object({ endpoint: z.string() }))
.withExecute(async ({ endpoint }) => {
const response = await fetch(endpoint)
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`)
}
return response.json()
})
.build()
// The AI will see:
// { success: false, error: "API returned 500: Internal Server Error" }
// And can respond to the user appropriately
```
### Error: Tool input validation fails
**Cause:** The Zod schema rejected the input values.
**Solution:** Check that your tool's Zod schema matches the expected input:
```typescript
// Schema expects 'userId' but code passes 'user_id'
.withInput(z.object({ userId: z.string() }))
// ^^ camelCase
// Calling with wrong key:
.withExecute(async ({ user_id }) => { // ❌ Should be userId
```
***
## TypeScript Issues
### Error: Type inference lost on builder
**Cause:** Breaking the type chain by assigning intermediate builders to loosely-typed variables.
**Solution:** Keep the builder chain intact or use explicit type annotations:
```typescript
// ❌ Loses type inference
let builder = IgniterAgent.create('bot')
builder = builder.withModel(openai('gpt-4o')) // Type narrowed incorrectly
// ✅ Chain directly
const agent = IgniterAgent
.create('bot')
.withModel(openai('gpt-4o'))
.build()
// ✅ Use explicit type if you must break the chain
const builder: ReturnType> =
IgniterAgent.create('bot')
```
### Error: `Property 'x' does not exist` on context
**Cause:** Context schema not set or mismatched.
**Solution:** Define a context schema for type-safe context:
```typescript
const agent = IgniterAgent.create('bot')
.withContextSchema(z.object({ company: z.string() }))
.build()
// Now context is typed:
await agent.generate({
chatId: 'chat_001',
userId: 'user_001',
context: { company: 'Acme' }, // ✅ Typed
message: { role: 'user', content: '...' },
})
```
***
## Logger/Debugging
### Enabling detailed logs
```typescript
import { IgniterLogger } from '@igniter-js/common'
const logger = IgniterLogger.create({ level: 'debug' })
const agent = IgniterAgent.create('bot')
.withLogger(logger)
.build()
// Now see: debug → IgniterAgent.start started { agent: 'bot', ... }
// success → IgniterAgent.start success { durationMs: 342 }
// error → IgniterAgent.mcp.connect failed Error(...)
```
### Debugging tool calls
```typescript
const agent = IgniterAgent.create('bot')
.onToolCallStart((agent, tool, input) => {
console.log(`[DEBUG] ${agent} → ${tool}`, input)
})
.onToolCallEnd((agent, tool, output) => {
console.log(`[DEBUG] ${agent} ← ${tool}`, output)
})
.onToolCallError((agent, tool, error) => {
console.error(`[DEBUG] ${agent} ✕ ${tool}`, error)
})
.build()
```
***
## Browser Import Protection
### Error: `@igniter-js/agents is server-only`
**Cause:** Importing agents in a browser/Vite/Next.js client component.
**Solution:** Agents is server-only. Use it only in:
* API routes (`app/api/**/route.ts` in Next.js)
* Server actions
* `getServerSideProps` / server components
* CLI tools
* Background workers
* Express/Fastify route handlers
```typescript
// ✅ OK: API route
// app/api/chat/route.ts
import { IgniterAgent } from '@igniter-js/agents'
// ❌ Wrong: Client component
// components/Chat.tsx
import { IgniterAgent } from '@igniter-js/agents' // Will throw!
```
***
## Quick Diagnosis Checklist
* [ ] Is `agent.start()` called before `agent.generate()`?
* [ ] Are MCP servers reachable (test manually)?
* [ ] Is the model provider API key set as an environment variable?
* [ ] Are tool names unique within each toolset?
* [ ] Are tool descriptions specific and actionable?
* [ ] Is context schema set if using typed context?
* [ ] Is the memory adapter appropriate for the deployment (in-memory vs persistent)?
* [ ] Are Node.js >= 22 or Bun >= 1.0 in use?
* [ ] Is `@ai-sdk/openai` (or your provider) installed?
* [ ] Is `zod` installed (peer dependency)?
---
# Platform Adapters
> Configure and use Telegram, WhatsApp, and Discord adapters. Capability comparison, configuration reference, and platform-specific features.
URL: https://igniterjs.com/docs/bots/adapters
## Platform Adapters
Adapters translate between platform-specific APIs and the normalized `BotContext`. This page covers the three first-party adapters in detail.
***
## Telegram Adapter
The Telegram adapter supports the **full Bot API surface** — webhooks, long polling, commands, inline keyboards, and all content types.
### Configuration
```typescript
import { telegram } from '@igniter-js/bot';
const tg = telegram({
token: '123456:ABC-DEF...',
handle: '@your_bot', // Optional: overrides global handle
webhook: {
url: 'https://your-domain.com/api/bot/telegram',
secret: 'my-webhook-secret', // Optional: validates webhook authenticity
dropPendingUpdates: true, // Clear pending updates on restart (default: true)
},
});
```
| Parameter | Type | Required | Default | Description |
| ---------------------------- | --------- | -------- | ----------------------------- | ----------------------------------- |
| `token` | `string` | ✅ | `TELEGRAM_TOKEN` env | Bot API token from @BotFather |
| `handle` | `string` | ❌ | Global bot handle | Bot @username for mention detection |
| `webhook.url` | `string` | ❌ | `TELEGRAM_WEBHOOK_URL` env | Public HTTPS endpoint |
| `webhook.secret` | `string` | ❌ | `TELEGRAM_WEBHOOK_SECRET` env | Validates webhook authenticity |
| `webhook.dropPendingUpdates` | `boolean` | ❌ | `true` | Clear pending updates on init |
### Initialization
During `.start()`, the Telegram adapter:
1. Deletes any existing webhook
2. Registers bot commands via `setMyCommands`
3. Sets the new webhook with the configured URL and secret
If no `webhook.url` is provided, the adapter skips webhook setup and logs a warning — you can use long polling instead.
### Inline Keyboards
Telegram supports both inline keyboards (within messages) and reply keyboards (below input):
```typescript
// Inline keyboard (buttons inside the message)
async handle(ctx) {
await ctx.replyWithButtons('Choose an option:', [
[
{ id: 'buy', label: '🛒 Buy Now', action: 'url', data: { url: 'https://shop.com' } },
{ id: 'info', label: 'ℹ️ More Info', action: 'callback', data: 'more_info' },
],
[
{ id: 'share', label: '📤 Share', action: 'share', data: '' },
],
]);
}
```
### Send Methods
All content types are supported:
```typescript
// Text
await ctx.reply('Hello!');
// Image
await ctx.replyWithImage('https://example.com/photo.jpg', 'Check this out!');
// Document
await ctx.replyWithDocument(file, 'report.pdf');
// Location
await ctx.bot.send({
provider: 'telegram',
channel: ctx.channel.id,
content: {
type: 'location',
latitude: 40.7128,
longitude: -74.0060,
name: 'New York City',
},
});
// Poll
await ctx.bot.send({
provider: 'telegram',
channel: ctx.channel.id,
content: {
type: 'poll',
question: 'What is your favorite language?',
options: ['TypeScript', 'Rust', 'Go', 'Python'],
isAnonymous: true,
},
});
// Interactive with inline keyboard
await ctx.replyWithButtons('Confirm?', [
{ id: 'yes', label: '✅ Yes', action: 'callback', data: 'confirm_yes' },
{ id: 'no', label: '❌ No', action: 'callback', data: 'confirm_no' },
]);
```
### Actions
```typescript
// Edit message
await ctx.editMessage!('12345', { type: 'text', content: 'Updated!' });
// Delete message
await ctx.deleteMessage!('12345');
// Typing indicator
await ctx.sendTyping!();
```
Telegram does **not** support message reactions via the Bot API, so `ctx.react()` is not available.
***
## WhatsApp Adapter
The WhatsApp adapter works with the **Meta WhatsApp Cloud API**. It supports text, media, interactive buttons, and location/contact sharing.
### Configuration
```typescript
import { whatsapp } from '@igniter-js/bot';
const wa = whatsapp({
token: 'EAAx...', // Cloud API access token
phone: '1234567890', // Phone number ID
});
```
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | --------------------------------- |
| `token` | `string` | ✅ | WhatsApp Cloud API access token |
| `phone` | `string` | ✅ | WhatsApp Business phone number ID |
WhatsApp webhook setup is managed through the **Meta Developer Dashboard**, not the adapter's `init()` method. The `init()` call is a no-op that logs a confirmation message.
### Interactive Buttons (max 3)
WhatsApp limits interactive messages to 3 buttons:
```typescript
async handle(ctx) {
await ctx.replyWithButtons('Confirm your order:', [
{ id: 'confirm', label: '✅ Confirm', action: 'callback', data: 'order_confirm' },
{ id: 'cancel', label: '❌ Cancel', action: 'callback', data: 'order_cancel' },
{ id: 'help', label: '🆘 Help', action: 'callback', data: 'order_help' },
]);
}
```
The adapter automatically converts buttons to the WhatsApp interactive message format.
### Media Handling
WhatsApp requires media URLs. The adapter handles File objects by converting them to data URLs:
```typescript
// URL (preferred for production)
await ctx.replyWithImage('https://cdn.example.com/product.jpg', 'New arrival!');
// File object (converted to data URL — works for small files)
const file = new File([buffer], 'receipt.pdf', { type: 'application/pdf' });
await ctx.replyWithDocument(file, 'Your receipt');
```
### Platform Limitations
WhatsApp does **not** support:
* Editing or deleting messages via the Cloud API
* Polls
* Traditional slash commands (commands are parsed from message text by the framework)
* Message pinning
***
## Discord Adapter
The Discord adapter uses the **Interactions API** for slash commands, buttons, and select menus. It includes signature verification for webhook security.
### Configuration
```typescript
import { discord } from '@igniter-js/bot';
const dc = discord({
token: 'MTIz...', // Bot token
applicationId: '123456789012345678', // Application ID
publicKey: 'abc123...', // Public key for signature verification
});
```
| Parameter | Type | Required | Description |
| --------------- | -------- | -------- | -------------------------------------------------------- |
| `token` | `string` | ✅ | Discord bot token |
| `applicationId` | `string` | ❌ | Application ID (required for slash command registration) |
| `publicKey` | `string` | ❌ | Public key for Ed25519 signature verification |
### Signature Verification
The Discord adapter validates interaction signatures using Ed25519. If `publicKey` is configured, every incoming interaction is verified before processing. Requests with invalid signatures receive a `401 Unauthorized` response.
### Slash Commands
Commands are automatically registered as Discord slash commands during `init()`:
```typescript
// This command becomes a Discord slash command
builder.addCommand('ping', {
name: 'ping',
aliases: [],
description: 'Check latency', // Used as slash command description
help: 'Use /ping to check bot responsiveness',
async handle(ctx) {
await ctx.reply('🏓 Pong!');
},
});
```
### Message Components (Buttons & Select Menus)
Discord supports up to **5 buttons per row** and **5 rows per message**:
```typescript
async handle(ctx) {
await ctx.replyWithButtons('Select an action:', [
[
{ id: 'primary', label: 'Primary', action: 'callback', data: 'action_primary' },
{ id: 'secondary', label: 'Secondary', action: 'callback', data: 'action_secondary' },
],
[
{ id: 'docs', label: '📚 Documentation', action: 'url', data: { url: 'https://docs.example.com' } },
],
]);
}
```
### Thread Support
Discord supports threads natively. The adapter detects thread context and routes messages accordingly.
### Embeds
For image URLs, the adapter automatically creates Discord embeds:
```typescript
await ctx.replyWithImage('https://example.com/photo.jpg', 'A beautiful photo');
// Creates an embed with the image, not a plain URL
```
***
## Adapter Client
Every adapter exposes a pre-configured HTTP client for making direct API calls:
```typescript
const bot = IgniterBot.create()
.addAdapter('telegram', tg)
.build();
// Get the adapter's client
const tgAdapter = bot.getAdapter('telegram');
const client = tgAdapter?.client;
if (client) {
// Direct Telegram API calls
const me = await client.get('/getMe');
const chat = await client.post('/getChat', { chat_id: '123456' });
// Raw Discord API calls
const guild = await client.get('/guilds/123456789');
}
```
The client is pre-configured with the platform's base URL, authentication headers, and content type negotiation. All methods return typed responses.
***
## Custom Adapters
You can build adapters for any chat platform. See the `Bot.adapter()` static method:
```typescript
const myAdapter = Bot.adapter({
name: 'my-platform',
parameters: z.object({
apiKey: z.string(),
endpoint: z.string().url(),
}),
capabilities: {
content: { text: true, image: false, /* ... */ },
actions: { edit: false, delete: false, /* ... */ },
features: { webhooks: true, /* ... */ },
limits: { maxMessageLength: 1000, maxFileSize: 10 * 1024 * 1024, maxButtonsPerMessage: 3 },
},
init: async ({ config, commands, logger }) => {
// Setup webhook, register commands, etc.
},
handle: async ({ request, config, logger }) => {
// Parse request → return BotContext or null
const body = await request.json();
return {
event: 'message',
provider: 'my-platform',
channel: { id: body.chat_id, name: 'Chat', isGroup: false },
message: {
author: { id: body.user_id, name: body.user_name, username: body.user_handle },
content: { type: 'text', content: body.text, raw: body.text },
isMentioned: body.text.includes('@mybot'),
},
};
},
sendText: async ({ channel, text, config }) => {
// Send text message to platform API
await fetch(config.endpoint + '/messages', {
method: 'POST',
headers: { Authorization: `Bearer ${config.apiKey}` },
body: JSON.stringify({ chat_id: channel, text }),
});
},
});
```
Custom adapters automatically integrate with the entire middleware, plugin, session, and event system — no extra wiring needed.
***
## Next Steps
---
# API Reference
> Complete API reference for @igniter-js/bot. Builder methods, Bot class, adapters, middlewares, plugins, types, and error codes.
URL: https://igniterjs.com/docs/bots/api-reference
## API Reference
This page documents every public API surface of `@igniter-js/bot`. All methods, types, and exports are listed with their signatures and descriptions.
***
## IgniterBotBuilder
The main entry point for constructing bots.
### Static Methods
#### `IgniterBot.create()`
Creates a new builder instance.
```typescript
static create(): IgniterBotBuilder
```
### Identity Methods
#### `withHandle(handle: string): this`
Sets the bot handle (e.g., `@mybot`). Auto-generates `id` and `name` from the handle if not set explicitly.
```typescript
withHandle(handle: string): this
```
#### `withId(id: string): this`
Sets a unique bot identifier. Optional — derived from handle if not provided.
```typescript
withId(id: string): this
```
#### `withName(name: string): this`
Sets a human-readable display name. Optional — derived from handle if not provided.
```typescript
withName(name: string): this
```
### Configuration Methods
#### `withLogger(logger: BotLogger): this`
Configures a structured logger for the bot and all adapters.
```typescript
withLogger(logger: BotLogger): this
```
The `BotLogger` interface:
```typescript
interface BotLogger {
debug?: (...args: any[]) => void
info?: (...args: any[]) => void
warn?: (...args: any[]) => void
error?: (...args: any[]) => void
}
```
#### `withSessionStore(store: BotSessionStore): this`
Configures session storage for conversational state.
```typescript
withSessionStore(store: BotSessionStore): this
```
#### `withOptions(options: BotOptions): this`
Sets advanced bot options.
```typescript
withOptions(options: BotOptions): this
interface BotOptions {
timeout?: number // Request timeout in ms
retries?: number // Retry attempts for failed operations
autoRegisterCommands?: boolean // Auto-register with platforms
errorHandler?: (error: BotError, context?: BotContext) => void | Promise
}
```
### Adapter Methods
#### `addAdapter(key: string, adapter: IBotAdapter): IgniterBotBuilder`
Adds a single platform adapter. Returns a new builder with updated generic types.
```typescript
addAdapter>(
key: K,
adapter: A
): IgniterBotBuilder, TCommands, TContext>
```
#### `addAdapters(adapters: Record): IgniterBotBuilder`
Adds multiple adapters at once.
```typescript
addAdapters>>(
adapters: A
): IgniterBotBuilder
```
### Command Methods
#### `addCommand(name: string, command: BotCommand): IgniterBotBuilder`
Registers a command. Returns a new builder with updated types.
```typescript
addCommand>(
name: K,
command: C
): IgniterBotBuilder, TContext>
```
#### `addCommands(commands: Record): IgniterBotBuilder`
Registers multiple commands at once.
```typescript
addCommands>>(
commands: C
): IgniterBotBuilder
```
#### `addCommandGroup(prefix: string, commands: Record): IgniterBotBuilder`
Registers prefixed commands (e.g., `admin_ban`, `admin_kick`).
```typescript
addCommandGroup>>(
prefix: string,
commands: C
): IgniterBotBuilder
```
### Middleware Methods
#### `addMiddleware(middleware: Middleware): IgniterBotBuilder`
Adds a middleware to the pipeline.
```typescript
addMiddleware(
middleware: Middleware
): IgniterBotBuilder>
```
#### `addMiddlewares(middlewares: Middleware[]): this`
Adds multiple middlewares at once.
```typescript
addMiddlewares(middlewares: Middleware[]): this
```
### Plugin Methods
#### `usePlugin(plugin: BotPlugin): this`
Loads a plugin, registering all its commands, middlewares, adapters, and hooks.
```typescript
usePlugin(plugin: BotPlugin): this
```
### Event Listener Methods
#### `onMessage(handler: BotEventHandler): this`
Registers a message event listener.
```typescript
onMessage(handler: BotEventHandler): this
type BotEventHandler = (ctx: TContext) => Promise | void
```
#### `onError(handler: BotErrorHandler): this`
Registers an error event listener.
```typescript
onError(handler: BotErrorHandler): this
type BotErrorHandler = (
ctx: TContext & { error: BotError },
) => Promise | void
```
#### `onCommand(handler: BotEventHandler): this`
Registers a command event listener (called for all commands).
```typescript
onCommand(handler: BotEventHandler): this
```
#### `onStart(handler: BotStartHandler): this`
Registers a startup hook.
```typescript
onStart(handler: BotStartHandler): this
type BotStartHandler = () => Promise | void
```
### Build Method
#### `build(): Bot`
Validates configuration and returns the immutable Bot instance.
```typescript
build(): Bot[], TCommands>
```
Throws if:
* No adapters are registered
* No handle is configured (neither global nor in any adapter)
***
## Bot Class
The runtime bot instance. Returned by `builder.build()`.
### Properties
| Property | Type | Description |
| ----------- | --------------------- | --------------------- |
| `id` | `string` | Unique bot identifier |
| `name` | `string` | Display name |
| `botHandle` | `string \| undefined` | Global handle |
### Methods
#### `start(): Promise`
Initializes all adapters (registers webhooks, commands, etc.) and runs startup hooks.
```typescript
async start(): Promise
```
#### `handle(provider: string, request: Request): Promise`
Routes an incoming request to the appropriate adapter.
```typescript
async handle(provider: string, request: Request): Promise
```
Throws `BotError` with code `PROVIDER_NOT_FOUND` if the adapter isn't registered.
#### `send(params: BotSendParams): Promise`
Sends an outbound message through a specific adapter.
```typescript
async send(params: BotSendParams): Promise
type BotSendParams = {
provider: string
channel: string
content: BotOutboundContent
options?: BotSendOptions
config: TConfig
}
```
Throws `BotError` with code `CONTENT_TYPE_NOT_SUPPORTED` if the adapter doesn't support the content type.
#### `on(event: BotEvent, callback: Function): void`
Registers a runtime event listener.
```typescript
on(event: BotEvent, callback: (ctx: BotContext) => Promise): void
type BotEvent = 'start' | 'message' | 'error'
```
#### `registerCommand(name: string, command: BotCommand): this`
Dynamically registers a command at runtime.
```typescript
registerCommand(name: string, command: BotCommand): this
```
#### `registerAdapter(key: string, adapter: IBotAdapter): this`
Dynamically registers an adapter at runtime.
```typescript
registerAdapter>(key: K, adapter: A): this
```
#### `use(middleware: Middleware): this`
Dynamically adds a middleware at runtime.
```typescript
use(mw: Middleware): this
```
#### `onPreProcess(hook: Function): this`
Registers a hook that runs before the middleware pipeline (for session loading, context enrichment).
```typescript
onPreProcess(hook: (ctx: BotContext) => Promise | void): this
```
#### `onPostProcess(hook: Function): this`
Registers a hook that runs after successful processing.
```typescript
onPostProcess(hook: (ctx: BotContext) => Promise | void): this
```
#### `emit(event: BotEvent, ctx: BotContext): Promise`
Manually emits an event to registered listeners.
```typescript
async emit(event: BotEvent, ctx: BotContext): Promise
```
#### `getAdapter(provider: string): IBotAdapter | undefined`
Returns the adapter for a specific provider.
```typescript
getAdapter(provider: string): IBotAdapter | undefined
```
#### `getAdapters(): Record`
Returns all registered adapters.
```typescript
getAdapters(): Record>
```
### Static Methods
#### `Bot.adapter(config): AdapterFactory`
Creates a typed adapter factory. Used internally by built-in adapters and for custom adapters.
```typescript
static adapter>(config: {
name: string
parameters: TConfig
capabilities: BotAdapterCapabilities
verify?: (params: AdapterVerifyParams) => Promise
init: (params: AdapterInitParams) => Promise
handle: (params: AdapterHandleParams) => Promise | null>
sendTyping?: (params: AdapterSendTypingParams) => Promise
client?: (config, logger?) => AdapterClient
sendText?: (params: AdapterSendTextParams) => Promise
sendImage?: (params: AdapterSendImageParams) => Promise
// ... all send methods, editMessage, deleteMessage
}): (config?: Partial>) => IBotAdapter
```
#### `Bot.command(command): BotCommand`
Validates and returns a command definition.
```typescript
static command(
command: BotCommand
): BotCommand
```
#### `Bot.middleware(middleware): Middleware`
Validates and returns a middleware function.
```typescript
static middleware(
middleware: Middleware
): Middleware
```
#### `Bot.create(config): Bot`
Internal factory. **Deprecated:** Use `IgniterBot` builder instead.
```typescript
static create(config: {
id: string
name: string
handle?: string
adapters: TAdapters
middlewares?: TMiddlewares
commands?: TCommands
on?: Partial Promise>>
logger?: BotLogger
sessionStore?: BotSessionStore
}): Bot
```
***
## BotCommand Interface
```typescript
interface BotCommand {
name: string
aliases: string[]
description: string
help: string
args?: ZodType
handle: (ctx: TContext, params: TArgs) => Promise
subcommands?: Record, 'name' | 'aliases'>>
}
```
***
## BotContext Interface
```typescript
interface BotContext {
event: BotEvent // 'start' | 'message' | 'error'
provider: string // 'telegram' | 'whatsapp' | 'discord'
bot: {
id: string
name: string
send: (params: Omit, 'config'>) => Promise
getAdapter?: (provider: string) => IBotAdapter | undefined
getAdapters?: () => Record
}
channel: {
id: string
name: string
isGroup: boolean
}
message: {
id?: string
content?: BotContent
attachments?: BotAttachmentContent[]
author: {
id: string
name: string
username: string
}
isMentioned: boolean
}
session: BotSessionHelper
reply(content: BotOutboundContent | string, options?: BotSendOptions): Promise
replyWithButtons(text: string, buttons: BotButton[], options?: BotSendOptions): Promise
replyWithImage(image: string | File, caption?: string, options?: BotSendOptions): Promise
replyWithDocument(file: File, caption?: string, options?: BotSendOptions): Promise
editMessage?(messageId: string, content: BotOutboundContent): Promise
deleteMessage?(messageId: string): Promise
react?(emoji: string, messageId?: string): Promise
sendTyping?(): Promise
}
```
***
## Content Types
### BotOutboundContent (Send)
```typescript
type BotOutboundContent =
| BotTextContent // { type: 'text', content: string }
| BotImageContent // { type: 'image', content: string, file?: File, caption?: string }
| BotVideoContent // { type: 'video', content: string, file?: File, caption?: string }
| BotAudioContent // { type: 'audio', content: string, file?: File }
| BotDocumentContent // { type: 'document', content: string, file: File, filename?: string, caption?: string }
| BotStickerContent // { type: 'sticker', content: string, file?: File }
| BotLocationContent // { type: 'location', latitude: number, longitude: number, name?: string, address?: string }
| BotContactContent // { type: 'contact', phoneNumber: string, firstName: string, lastName?: string }
| BotPollContent // { type: 'poll', question: string, options: string[], ... }
| BotInteractiveContent // { type: 'interactive', text: string, buttons?: BotButton[], inlineKeyboard?: BotInlineKeyboardRow[] }
| BotReplyContent // { type: 'reply', messageId: string, content: BotOutboundContent }
```
### BotSendOptions
```typescript
interface BotSendOptions {
replyToMessageId?: string
disableWebPagePreview?: boolean
disableNotification?: boolean
parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2'
protectContent?: boolean
autoFormat?: boolean
}
```
***
## BotError
```typescript
class BotError extends Error {
code: BotErrorCode
meta?: Record
constructor(code: BotErrorCode, message?: string, meta?: Record)
}
type BotErrorCode =
| 'CLIENT_NOT_PROVIDED'
| 'PROVIDER_NOT_FOUND'
| 'COMMAND_NOT_FOUND'
| 'INVALID_COMMAND_PARAMETERS'
| 'ADAPTER_HANDLE_RETURNED_NULL'
| 'CONTENT_TYPE_NOT_SUPPORTED'
| 'INVALID_CONTENT'
```
***
## Session Interfaces
### BotSessionStore
```typescript
interface BotSessionStore {
get(userId: string, channelId: string): Promise
set(userId: string, channelId: string, session: BotSession): Promise
delete(userId: string, channelId: string): Promise
clear(userId: string): Promise
}
```
### BotSessionHelper
```typescript
interface BotSessionHelper extends BotSession {
save(): Promise
delete(): Promise
update(data: Partial>): Promise
}
```
***
## Plugin Interface
```typescript
interface BotPlugin {
name: string
version: string
description?: string
commands?: Record
middlewares?: Middleware[]
adapters?: Record>
hooks?: {
onStart?: () => Promise | void
onMessage?: (ctx: BotContext) => Promise | void
onError?: (ctx: BotContext & { error: BotError }) => Promise | void
onStop?: () => Promise | void
}
config?: Record
}
```
***
## Exports
### Main Exports
```typescript
export { IgniterBot, IgniterBotBuilder } from './builder'
export { Bot, BotError, BotErrorCodes } from './bot.provider'
export { telegram } from './adapters/telegram'
export { whatsapp } from './adapters/whatsapp'
export { discord } from './adapters/discord'
export { nextRouteHandlerAdapter } from './adapters/nextjs'
export { tanstackStartRouteHandlerAdapter } from './adapters/tanstack-start'
export { adapters } from './index' // Convenience: { telegram, whatsapp, discord }
export { VERSION } from './index'
// Middlewares
export { authMiddleware, authPresets, roleMiddleware } from './middlewares/auth'
export { rateLimitMiddleware, rateLimitPresets, memoryRateLimitStore } from './middlewares/rate-limit'
export { loggingMiddleware, loggingPresets, commandLoggingMiddleware } from './middlewares/logging'
// Plugins
export { analyticsPlugin } from './plugins/analytics'
// Stores
export { memoryStore, MemorySessionStore } from './stores/memory'
```
***
## Next Steps
---
# Best Practices
> Production patterns, security tips, and anti-patterns for building bots with @igniter-js/bot. Keep your code maintainable, secure, and performant.
URL: https://igniterjs.com/docs/bots/best-practices
## Best Practices
This page captures patterns that work well (and pitfalls to avoid) when building production bots with `@igniter-js/bot`.
***
## Architecture & Organization
### ✅ Do: Keep Bot Configuration in a Single File
```typescript
// lib/bot.ts — Single source of truth for your bot
import { IgniterBot, telegram, whatsapp, memoryStore } from '@igniter-js/bot';
import { commands } from './commands';
import { middlewares } from './middlewares';
import { plugins } from './plugins';
export const bot = IgniterBot.create()
.withHandle('@mybot')
.withSessionStore(memoryStore())
.addAdapters({ telegram: telegram({ token: env.TELEGRAM_TOKEN }) })
.addCommands(commands)
.addMiddlewares(middlewares)
.build();
```
### ❌ Don't: Scatter Bot Configuration Across Multiple Files
Avoid building different parts of the bot in separate modules and merging them later — the builder pattern is designed for one clear configuration path.
### ✅ Do: Extract Commands into Separate Files
```typescript
// commands/start.ts
import { Bot } from '@igniter-js/bot';
export const startCommand = Bot.command({
name: 'start',
aliases: ['hello'],
description: 'Greets the user',
help: 'Use /start to receive a welcome',
async handle(ctx) {
await ctx.reply('Welcome!');
},
});
// commands/index.ts
export { startCommand } from './start';
export { helpCommand } from './help';
export { searchCommand } from './search';
// lib/bot.ts
import * as commands from './commands';
builder.addCommands(commands);
```
### ✅ Do: Isolate Business Logic from Command Handlers
```typescript
// services/search.service.ts — Pure business logic, testable
export async function searchCatalog(query: string, category?: string) {
// Database queries, API calls, etc.
return results;
}
// commands/search.ts — Thin handler, delegates to service
import { searchCatalog } from '../services/search.service';
export const searchCommand = Bot.command({
name: 'search',
aliases: ['find'],
description: 'Search the catalog',
help: 'Use /search <query>',
async handle(ctx) {
const results = await searchCatalog(ctx.args.query);
await ctx.reply(formatResults(results)); // Separate formatting
},
});
```
***
## Middleware Ordering
### ✅ Do: Order Middlewares by Criticality
```
1. Logging (outside timing)
2. Rate limiting (protect resources)
3. Authentication (block unauthorized)
4. Context enrichment (load user data)
5. Business logic middlewares
```
### ❌ Don't: Place Auth After Business Logic
```
// BAD: Rate limiting won't apply to unauthorized users
builder
.addMiddleware(authMiddleware({ ... })) // 1st
.addMiddleware(rateLimitMiddleware({ ... })) // 2nd — useless if auth blocks
// GOOD: Rate limit first
builder
.addMiddleware(rateLimitMiddleware({ ... })) // 1st: always runs
.addMiddleware(authMiddleware({ ... })) // 2nd
```
***
## Sessions
### ✅ Do: Use Session Update for Partial Changes
```typescript
// GOOD: Merge — other session data is preserved
await ctx.session.update({ step: 'checkout' });
// BAD: Direct mutation might not save
ctx.session.data.step = 'checkout'; // May not persist
```
### ✅ Do: Clear Sessions When Flows Complete
```typescript
async handle(ctx) {
if (ctx.session.data.step === 'complete') {
await saveData(ctx.session.data);
await ctx.session.delete(); // Clean up
await ctx.reply('All done! 🎉');
}
}
```
### ❌ Don't: Store Large Objects in Sessions
```typescript
// BAD: Sessions are for conversational state, not data storage
await ctx.session.update({ allProducts: hugeProductArray });
// GOOD: Store IDs, fetch data when needed
await ctx.session.update({ lastSearchQuery: query });
```
***
## Error Handling
### ✅ Do: Always Have a Global Error Handler
```typescript
builder.onError(async (ctx) => {
console.error(`[${ctx.provider}] Error for user ${ctx.message.author.id}:`, ctx.error);
// Don't reveal internal details to users
await ctx.reply('⚠️ Something went wrong. Our team has been notified.');
});
```
### ✅ Do: Use try/catch in Individual Handlers for Recoverable Errors
```typescript
async handle(ctx) {
try {
const data = await externalApi.fetch();
await ctx.reply(formatData(data));
} catch (error) {
await ctx.reply('⚠️ Could not fetch data. Please try again.');
// Don't re-throw — it was handled gracefully
}
}
```
### ❌ Don't: Expose Internal Error Details to Users
```typescript
// BAD: Leaks internal structure
await ctx.reply(`Error: ${error.message}\nStack: ${error.stack}`);
// GOOD: User-friendly message
await ctx.reply('Something went wrong. Try again or contact support.');
```
***
## Security
### ✅ Do: Validate All User Input
```typescript
// Zod validation at the command level
args: z.object({
userId: z.string().regex(/^[a-zA-Z0-9_-]+$/), // Sanitized
amount: z.number().int().positive().max(10000),
})
```
### ✅ Do: Use Environment Variables for Secrets
```typescript
// GOOD
telegram({ token: process.env.TELEGRAM_TOKEN! })
// BAD: Never hardcode tokens
telegram({ token: '123456:ABC' })
```
### ✅ Do: Verify Webhook Signatures
```typescript
// Discord adapter does this automatically if publicKey is set
discord({
token: '...',
publicKey: process.env.DISCORD_PUBLIC_KEY!,
})
```
### ❌ Don't: Trust `ctx.message.author.id` Without Verification
The adapter parses this from the platform — it's trustworthy for platform IDs, but don't use it directly for authorization without additional checks (database lookup, subscription status).
***
## Performance
### ✅ Do: Initialize Bot at Module Level
```typescript
// GOOD: Created once on import, reused across requests
export const bot = IgniterBot.create() /* ... */ .build();
// BAD: Created per-request (serverless cold start only)
export function getBot() {
return IgniterBot.create() /* ... */ .build();
}
```
### ✅ Do: Batch Database Queries in Middleware
```typescript
// Enrich context once, use everywhere
builder.addMiddleware(async (ctx, next) => {
const [user, preferences, permissions] = await Promise.all([
db.users.findById(ctx.message.author.id),
db.preferences.get(ctx.message.author.id),
db.permissions.get(ctx.message.author.id),
]);
await next();
return { user, preferences, permissions };
});
```
### ❌ Don't: Block the Middleware Pipeline with Slow Operations
```typescript
// BAD: 500ms delay on every message
builder.addMiddleware(async (ctx, next) => {
await heavySyncOperation();
await next();
});
// GOOD: Fire-and-forget or queue
builder.addMiddleware(async (ctx, next) => {
// Don't await non-critical operations
analytics.track('message.received', { ... });
await next();
});
```
***
## Testing
### ✅ Do: Test Commands in Isolation
```typescript
import { Bot } from '@igniter-js/bot';
test('start command returns welcome message', async () => {
const ctx = createMockContext({
provider: 'telegram',
author: { id: 'user_1', name: 'Test', username: 'test' },
});
let replyText = '';
ctx.reply = async (text) => { replyText = text; };
await startCommand.handle(ctx, {});
expect(replyText).toContain('Welcome');
});
```
### ✅ Do: Use Memory Store in Tests
```typescript
const bot = IgniterBot.create()
.withSessionStore(memoryStore())
.addAdapter('telegram', mockTelegramAdapter)
.build();
```
***
## Production Checklist
* [ ] **Environment variables** for all tokens and secrets
* [ ] **Global error handler** via `onError()`
* [ ] **Rate limiting** configured for your expected traffic
* [ ] **Persistent session store** (Redis, Prisma) — not memory
* [ ] **Webhook signature verification** enabled (Discord)
* [ ] **Logging middleware** with `includeContent: false` (privacy)
* [ ] **Bot initialized at module level** (not per-request)
* [ ] **Commands extracted** into separate files for maintainability
* [ ] **Business logic isolated** from command handlers
* [ ] **Webhook URLs configured** in platform dashboards
***
## Next Steps
---
# Commands
> Master the command system — aliases, subcommands, Zod argument validation, command groups, and auto-registration across platforms.
URL: https://igniterjs.com/docs/bots/commands
## Commands
Commands are the primary way users interact with your bot. The framework provides a full-featured command system with aliases, subcommands, Zod validation, command groups, and automatic platform registration.
***
## Basic Command
Every command requires: `name`, `aliases`, `description`, `help`, and a `handle` function:
```typescript
builder.addCommand('start', {
name: 'start',
aliases: ['hello', 'hi'],
description: 'Greets the user',
help: 'Use /start to receive a welcome message',
async handle(ctx) {
await ctx.reply(`👋 Welcome, ${ctx.message.author.name}!`);
},
});
```
| Field | Type | Required | Description |
| ------------- | ------------------------------ | -------- | ----------------------------------------------------------- |
| `name` | `string` | ✅ | Command name without slash. Must not contain `/` or spaces. |
| `aliases` | `string[]` | ✅ | Alternative names. Each must follow the same rules. |
| `description` | `string` | ✅ | Short description (used for platform registration). |
| `help` | `string` | ✅ | Detailed help shown on invalid usage. |
| `args` | `ZodType` | ❌ | Zod schema for validating and typing arguments. |
| `handle` | `(ctx, args) => Promise` | ✅ | The handler function. |
| `subcommands` | `Record` | ❌ | Nested subcommands. |
Commands are validated at registration time using `Bot.command()`. Names with slashes, spaces, or missing required fields throw immediately.
***
## Aliases
Aliases let users trigger the same command with different names. All aliases are converted to lowercase and indexed for O(1) lookup:
```typescript
builder.addCommand('help', {
name: 'help',
aliases: ['commands', '?', 'h'], // /help, /commands, /?, /h all work
description: 'Show available commands',
help: 'Use /help to see available commands',
async handle(ctx) { /* ... */ },
});
```
***
## Argument Validation (Zod)
Use the `args` field to validate and type command arguments:
```typescript
import { z } from 'zod';
builder.addCommand('search', {
name: 'search',
aliases: ['find', 's'],
description: 'Search the catalog',
help: 'Use /search <query> [category] to find items',
args: z.object({
query: z.string().min(2).describe('Search query (min 2 chars)'),
category: z.enum(['books', 'movies', 'music']).optional(),
}),
async handle(ctx, args) {
// args is fully typed: { query: string; category?: 'books' | 'movies' | 'music' }
const results = await searchCatalog(args.query, args.category);
if (results.length === 0) {
await ctx.reply(`No results found for "${args.query}".`);
return;
}
await ctx.reply(
`🔍 Found **${results.length}** result(s) for "${args.query}":\n\n` +
results.map((r, i) => `${i + 1}. ${r.title}`).join('\n'),
);
},
});
```
When a user provides invalid arguments, the framework returns a helpful error message automatically:
```
/invalid usage: search <query> [category]
Use /search <query> [category] to find items
```
The `args` schema uses `z.object()`. Nested objects, arrays, unions, and refinements are all supported — anything Zod can validate works here.
***
## Subcommands
Subcommands create nested command hierarchies. Define them inline or reference externally defined commands:
```typescript
builder.addCommand('admin', {
name: 'admin',
aliases: ['adm'],
description: 'Admin commands',
help: 'Use /admin <subcommand> for admin operations',
subcommands: {
ban: {
name: 'ban',
aliases: ['block'],
description: 'Ban a user',
help: 'Use /admin ban <user_id>',
args: z.object({ userId: z.string() }),
async handle(ctx, args) {
await banUser(args.userId);
await ctx.reply(`🚫 User ${args.userId} has been banned.`);
},
},
kick: {
name: 'kick',
aliases: ['remove'],
description: 'Kick a user',
help: 'Use /admin kick <user_id>',
args: z.object({ userId: z.string() }),
async handle(ctx, args) {
await kickUser(args.userId);
await ctx.reply(`👢 User ${args.userId} has been kicked.`);
},
},
stats: {
name: 'stats',
aliases: ['info'],
description: 'Show admin stats',
help: 'Use /admin stats',
async handle(ctx) {
const stats = await getAdminStats();
await ctx.reply(`📊 **Admin Stats**\n\n${JSON.stringify(stats, null, 2)}`);
},
},
},
// The parent command itself has a handler for bare /admin
async handle(ctx) {
await ctx.reply(
'🔧 **Admin Commands:**\n\n' +
'/admin ban <user_id> — Ban a user\n' +
'/admin kick <user_id> — Kick a user\n' +
'/admin stats — Show statistics',
);
},
});
```
Subcommands don't need their own `name` or `aliases` — those are inherited from the parent command context.
***
## Command Groups
Group related commands with a common prefix using `.addCommandGroup()`:
```typescript
builder.addCommandGroup('admin', {
ban: {
name: 'ban',
aliases: ['block'],
description: 'Ban a user',
help: 'Use /admin_ban <user_id>',
async handle(ctx) { /* ... */ },
},
kick: {
name: 'kick',
aliases: ['remove'],
description: 'Kick a user',
help: 'Use /admin_kick <user_id>',
async handle(ctx) { /* ... */ },
},
stats: {
name: 'stats',
aliases: [],
description: 'Admin statistics',
help: 'Use /admin_stats for statistics',
async handle(ctx) { /* ... */ },
},
});
// Results in commands: /admin_ban, /admin_kick, /admin_stats
```
Unlike subcommands, command groups create **flat, prefixed commands**. This is useful when platforms don't support nested command structures natively.
***
## Multiple Command Registration
Register several commands at once:
```typescript
builder.addCommands({
start: { name: 'start', aliases: ['hello'], /* ... */ },
help: { name: 'help', aliases: ['?'], /* ... */ },
about: { name: 'about', aliases: ['info'], /* ... */ },
contact: { name: 'contact', aliases: ['support'], /* ... */ },
});
```
***
## Dynamic Registration at Runtime
Commands can be added after the bot is built:
```typescript
const bot = builder.build();
// Later, dynamically register a new command
bot.registerCommand('maintenance', {
name: 'maintenance',
aliases: ['maint'],
description: 'Toggle maintenance mode',
help: 'Use /maintenance on|off',
async handle(ctx) {
const isMaintenance = await toggleMaintenance();
await ctx.reply(`Maintenance mode: ${isMaintenance ? 'ON' : 'OFF'}`);
},
});
```
This is useful for plugin systems, hot-reload during development, or commands that depend on runtime state.
***
## Platform Auto-Registration
Commands are automatically registered with platforms that support it:
* **Telegram** — Commands are set via `setMyCommands` API during `init()`
* **Discord** — Slash commands are registered via the Applications API during `init()`
* **WhatsApp** — Commands are handled entirely by the framework (no native command system)
Discord slash commands may take up to 1 hour to propagate globally after registration. For development, register them to a specific guild for instant updates.
***
## Creating Commands in Separate Files
Use `Bot.command()` to create commands in separate files with validation:
```typescript
// commands/start.ts
import { Bot } from '@igniter-js/bot';
export const startCommand = Bot.command({
name: 'start',
aliases: ['hello', 'hi'],
description: 'Greets the user',
help: 'Use /start to receive a welcome message',
async handle(ctx) {
await ctx.reply('👋 Welcome!');
},
});
// lib/bot.ts
import { startCommand } from './commands/start';
const bot = IgniterBot.create()
.addCommand('start', startCommand)
.build();
```
`Bot.command()` validates the command structure at creation time, catching errors early.
***
## Best Practices
* ✅ **Use descriptive names** — `search` is better than `s`. Aliases handle shortcuts.
* ✅ **Always provide help text** — Users will invoke `/command --help` style usage.
* ✅ **Validate with Zod** — Even simple commands benefit from typed arguments.
* ✅ **Organize related commands with groups** — Keeps your bot configuration readable.
* ✅ **Keep handlers focused** — Extract business logic into separate functions for testability.
* ❌ **Don't create too many top-level commands** — Use subcommands or groups for organization.
* ❌ **Don't hardcode platform-specific logic** — Use `ctx.provider` to check the current platform only when truly needed.
***
## Next Steps
---
# Core Concepts
> Master the builder pattern, adapters, commands, context, middlewares, and plugins that power @igniter-js/bot.
URL: https://igniterjs.com/docs/bots/core-concepts
## Core Concepts
This page explains the architectural pillars of `@igniter-js/bot`. Understanding these concepts lets you build complex, production-grade bots with confidence.
***
## The Builder Pattern
The `IgniterBot` builder is the **only recommended way** to construct bots. It provides a fluent, type-safe API where each method narrows the generic type parameters, giving you precise autocomplete at every step.
### Builder Lifecycle
```
IgniterBot.create()
→ .withHandle() // Set bot identity
→ .withSessionStore() // Configure session storage
→ .withLogger() // Structured logging
→ .withOptions() // Timeouts, retries, etc.
→ .addAdapters() // Platform adapters (Telegram, WhatsApp, Discord)
→ .addCommands() // Register commands
→ .addMiddlewares() // Middleware pipeline
→ .usePlugin() // Load plugins
→ .onMessage() // Event listeners
→ .onError() // Error handling
→ .onStart() // Startup hooks
→ .onCommand() // Command execution hooks
→ .build() // Materialize into immutable Bot instance
```
After `.build()`, the `Bot` instance is **immutable**. All configuration happens through the builder. The bot instance can still dynamically register commands, adapters, and middlewares at runtime using `registerCommand()`, `registerAdapter()`, and `use()`.
### Identity: Handle, ID, and Name
The `withHandle()` method is the primary identity setter. If you don't explicitly set `id` and `name`, they're derived automatically:
```typescript
IgniterBot.create()
.withHandle('@ecommerce_bot')
// → id: 'ecommerce_bot'
// → name: 'Ecommerce Bot'
.build();
```
You can override them explicitly:
```typescript
IgniterBot.create()
.withHandle('@ecommerce_bot')
.withId('prod-ecommerce-v2')
.withName('E-Commerce Assistant')
.build();
```
The handle is used for **mention detection** in group chats. If a global handle is set, all adapters use it. Each adapter can also override the handle in its config.
### Type-Safe Generics
The builder tracks your configuration in its generic parameters:
```typescript
const b1 = IgniterBot.create()
.addAdapter('telegram', tgAdapter)
// b1: IgniterBotBuilder<{ telegram: TelegramAdapter }, {}, BotContext>
const b2 = b1.addCommand('start', startCmd)
// b2: IgniterBotBuilder<{ telegram: ... }, { start: BotCommand }, BotContext>
const b3 = b2.addMiddleware(authMiddleware)
// b3: IgniterBotBuilder<{ telegram: ... }, { start: ... }, BotContext & AuthContextAdditions>
```
This means TypeScript knows exactly which adapters and commands are available throughout the entire builder chain.
***
## Adapters
Adapters are the bridge between the bot framework and platform-specific APIs. Each adapter:
1. **Parses** incoming requests into a normalized `BotContext`
2. **Declares** its capabilities (supported content types, actions, features, limits)
3. **Provides** send methods for each supported content type
4. **Initializes** by registering webhooks, commands, etc.
5. **Verifies** webhook signatures when applicable
### How Adapters Work
```
HTTP Request → Adapter.verify() (optional signature check)
→ Adapter.handle() → BotContext
→ Bot engine (middlewares, command routing, event listeners)
→ Handler calls ctx.reply() / ctx.sendTyping() / etc.
→ Adapter.sendText() / sendImage() / etc.
→ Platform API
```
### Declared Capabilities
Every adapter declares its capabilities so the framework can validate operations at runtime:
```typescript
const capabilities = {
content: {
text: true, // Can send plain text
image: true, // Can send images
video: true, // Can send video
audio: true, // Can send audio
document: true, // Can send files
sticker: true, // Can send stickers
location: true, // Can share location
contact: true, // Can share contacts
poll: true, // Can create polls
interactive: true, // Can send buttons/keyboards
},
actions: {
edit: true, // Can edit messages
delete: true, // Can delete messages
react: false, // Can add reactions
pin: true, // Can pin messages
thread: false, // Has thread support
},
features: {
webhooks: true, // Supports webhooks
longPolling: true, // Supports long polling
commands: true, // Has native command support
mentions: true, // Supports @mentions
groups: true, // Supports group chats
channels: true, // Supports channels
users: true, // Has user management
files: true, // Supports file handling
},
limits: {
maxMessageLength: 4096,
maxFileSize: 50 * 1024 * 1024, // 50MB
maxButtonsPerMessage: 8,
},
}
```
If you try to send a poll on WhatsApp (which doesn't support polls), the framework throws a `BotError` with code `CONTENT_TYPE_NOT_SUPPORTED`.
***
## BotContext
The `BotContext` is the normalized representation of every incoming interaction. It's passed to every middleware, command handler, and event listener.
```typescript
interface BotContext {
// Event type: 'start' | 'message' | 'error'
event: BotEvent
// Platform identifier: 'telegram' | 'whatsapp' | 'discord'
provider: string
// Bot instance and send capability
bot: {
id: string
name: string
send: (params) => Promise
getAdapter?: (provider: string) => IBotAdapter | undefined
getAdapters?: () => Record
}
// Channel / chat information
channel: {
id: string
name: string
isGroup: boolean
}
// Message details
message: {
id?: string
content?: BotContent
attachments?: BotAttachmentContent[]
author: {
id: string
name: string
username: string
}
isMentioned: boolean
}
// Session helper for conversational state
session: BotSessionHelper
// Convenience reply methods
reply(content: string | BotOutboundContent, options?: BotSendOptions): Promise
replyWithButtons(text: string, buttons: BotButton[], options?: BotSendOptions): Promise
replyWithImage(image: string | File, caption?: string, options?: BotSendOptions): Promise
replyWithDocument(file: File, caption?: string, options?: BotSendOptions): Promise
editMessage?(messageId: string, content: BotOutboundContent): Promise
deleteMessage?(messageId: string): Promise
react?(emoji: string, messageId?: string): Promise
sendTyping?(): Promise
}
```
Methods like `editMessage`, `deleteMessage`, `react`, and `sendTyping` are optional — they're only available when the current adapter supports those actions.
***
## The Middleware Pipeline
Middlewares execute in order for every incoming message. Each middleware receives the context and a `next()` function. Calling `next()` passes control to the next middleware. **Not** calling `next()` stops the pipeline.
```mermaid
graph LR
A[Incoming Message] --> M1[Auth Middleware]
M1 --> M2[Rate Limit Middleware]
M2 --> M3[Logging Middleware]
M3 --> M4[Custom Middleware]
M4 --> H[Command Handler / Event Listener]
```
### Middleware Signature
```typescript
type Middleware = (
ctx: TContextIn,
next: () => Promise,
) => Promise>
```
A middleware can:
1. **Enrich the context** by returning an object — merged into the context for downstream handlers
2. **Block the request** by not calling `next()`
3. **Perform side effects** before and after `next()`
4. **Handle errors** with try/catch around `next()`
```typescript
// Context enrichment
builder.addMiddleware(async (ctx, next) => {
const user = await db.users.findById(ctx.message.author.id);
await next();
return { user }; // available in downstream context
});
// Request blocking
builder.addMiddleware(async (ctx, next) => {
if (ctx.message.author.id === 'blocked_user') {
await ctx.reply('You are blocked.');
return; // No next() → pipeline stops
}
await next();
});
// Timing middleware
builder.addMiddleware(async (ctx, next) => {
const start = Date.now();
await next();
console.log(`Request took ${Date.now() - start}ms`);
});
```
***
## Plugins
Plugins package commands, middlewares, adapters, and lifecycle hooks into reusable, shareable modules.
```typescript
interface BotPlugin {
name: string
version: string
description?: string
commands?: Record
middlewares?: Middleware[]
adapters?: Record
hooks?: {
onStart?: () => Promise | void
onMessage?: (ctx: BotContext) => Promise | void
onError?: (ctx: BotContext & { error: BotError }) => Promise | void
onStop?: () => Promise | void
}
config?: Record
}
```
When a plugin is loaded via `.usePlugin()`, the builder automatically merges its commands, middlewares, adapters, and hooks into the bot configuration. This means you can build a plugin once and reuse it across multiple bots.
***
## Sessions
Sessions persist conversational state across messages. The framework provides:
* A `BotSessionStore` interface for pluggable storage backends
* A built-in `MemorySessionStore` for development
* A `BotSessionHelper` attached to `ctx.session` for easy access in handlers
```typescript
// Session shape
interface BotSession {
userId: string
channelId: string
data: Record // Your application state
createdAt: Date
updatedAt: Date
expiresAt?: Date // Auto-cleanup on expiration
}
// Helper methods on ctx.session
interface BotSessionHelper extends BotSession {
save(): Promise // Persist changes
delete(): Promise // Remove session
update(data: Partial>): Promise // Merge data
}
```
***
## Event System
The bot emits events that you can listen to:
| Event | Trigger | Context Fields |
| --------- | ------------------------------- | -------------------------------- |
| `message` | Every incoming message | Full `BotContext` |
| `start` | Bot initialization (`.start()`) | No message data |
| `error` | Any handler throws | `BotContext` + `error: BotError` |
```typescript
// Via builder
builder.onMessage(async (ctx) => { /* ... */ })
builder.onError(async (ctx) => { /* ctx.error */ })
builder.onStart(async () => { /* ... */ })
// Via Bot instance
bot.on('message', async (ctx) => { /* ... */ })
bot.on('error', async (ctx) => { /* ... */ })
```
***
## Error Handling
The framework uses a custom `BotError` class with error codes:
```typescript
const BotErrorCodes = {
CLIENT_NOT_PROVIDED: 'CLIENT_NOT_PROVIDED',
PROVIDER_NOT_FOUND: 'PROVIDER_NOT_FOUND',
COMMAND_NOT_FOUND: 'COMMAND_NOT_FOUND',
INVALID_COMMAND_PARAMETERS: 'INVALID_COMMAND_PARAMETERS',
ADAPTER_HANDLE_RETURNED_NULL: 'ADAPTER_HANDLE_RETURNED_NULL',
CONTENT_TYPE_NOT_SUPPORTED: 'CONTENT_TYPE_NOT_SUPPORTED',
INVALID_CONTENT: 'INVALID_CONTENT',
}
class BotError extends Error {
code: BotErrorCode
meta?: Record
}
```
Errors can be caught in middleware, event listeners, or the global error handler:
```typescript
builder.onError(async (ctx) => {
const botError = ctx.error as BotError;
console.error(`[${botError.code}] ${botError.message}`, botError.meta);
await ctx.reply('Something went wrong. Our team has been notified.');
});
```
***
## Next Steps
---
# Framework Integration
> Integrate @igniter-js/bot with Next.js, TanStack Start, Express, Fastify, Hono, and any Node.js web framework. Handle webhooks with minimal boilerplate.
URL: https://igniterjs.com/docs/bots/framework-integration
## Framework Integration
`@igniter-js/bot` is framework-agnostic at its core — adapters work with the standard `Request`/`Response` API. This page covers integration patterns for popular frameworks.
***
## Next.js App Router
The `nextRouteHandlerAdapter` provides a ready-to-use route handler.
### Route Setup
```typescript
// app/api/bot/[adapter]/[botId]/route.ts
import { nextRouteHandlerAdapter } from '@igniter-js/bot';
import { bot } from '@/lib/bot';
export const { GET, POST } = nextRouteHandlerAdapter({
assistant_bot: bot,
});
```
**Dynamic segments:**
* `[adapter]` — Platform: `telegram`, `whatsapp`, `discord`
* `[botId]` — Bot ID (derived from handle: `assistant_bot`)
### Webhook URL
Set your webhook URL to:
```
https://your-domain.com/api/bot/telegram/assistant_bot
```
The `GET` handler handles webhook verification (Telegram setup, Discord signature checks). The `POST` handler processes incoming messages.
### Multiple Bots
```typescript
import { nextRouteHandlerAdapter } from '@igniter-js/bot';
import { supportBot, salesBot, internalBot } from '@/lib/bots';
export const { GET, POST } = nextRouteHandlerAdapter({
support_bot: supportBot,
sales_bot: salesBot,
internal_bot: internalBot,
});
```
### Local Development
Use a tunneling service to expose your local server:
```bash
# ngrok
ngrok http 3000
# localtunnel
npx localtunnel --port 3000 --subdomain my-bot-dev
```
Then set your Telegram webhook to: `https://my-bot-dev.loca.lt/api/bot/telegram/assistant_bot`
***
## TanStack Start
The `tanstackStartRouteHandlerAdapter` follows the same pattern:
```typescript
// app/routes/api/bot/$adapter/$botId.ts
import { tanstackStartRouteHandlerAdapter } from '@igniter-js/bot';
import { bot } from '@/lib/bot';
export const { GET, POST } = tanstackStartRouteHandlerAdapter({
assistant_bot: bot,
});
```
***
## Express
Bot works with Express through the standard `Request`/`Response` pattern:
```typescript
import express from 'express';
import { bot } from './lib/bot';
const app = express();
// Convert Express req to Web Request
function toWebRequest(req: express.Request): Request {
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
return new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record),
body: req.method === 'POST' ? JSON.stringify(req.body) : undefined,
});
}
// Bot webhook handler
app.all('/api/bot/:adapter', async (req, res) => {
try {
const webRequest = toWebRequest(req);
const webResponse = await bot.handle(req.params.adapter, webRequest);
// Forward status and headers
res.status(webResponse.status);
webResponse.headers.forEach((value, key) => {
res.setHeader(key, value);
});
// Forward body
const body = await webResponse.text();
res.send(body);
} catch (error) {
console.error('Bot handler error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.listen(3000, () => {
console.log('Bot server running on http://localhost:3000');
});
```
### Express with Multiple Bots
```typescript
import { supportBot, salesBot } from './lib/bots';
const bots = { support: supportBot, sales: salesBot };
app.all('/api/bot/:botId/:adapter', async (req, res) => {
const bot = bots[req.params.botId];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const webRequest = toWebRequest(req);
const webResponse = await bot.handle(req.params.adapter, webRequest);
res.status(webResponse.status);
webResponse.headers.forEach((value, key) => res.setHeader(key, value));
const body = await webResponse.text();
res.send(body);
});
```
***
## Fastify
```typescript
import Fastify from 'fastify';
import { bot } from './lib/bot';
const fastify = Fastify({ logger: true });
// Bot webhook handler
fastify.all('/api/bot/:adapter', async (request, reply) => {
const url = `${request.protocol}://${request.hostname}${request.url}`;
const webRequest = new Request(url, {
method: request.method,
headers: request.headers as HeadersInit,
body: request.method === 'POST' ? JSON.stringify(request.body) : undefined,
});
const webResponse = await bot.handle(
(request.params as any).adapter,
webRequest,
);
reply.status(webResponse.status);
webResponse.headers.forEach((value, key) => {
reply.header(key, value);
});
const body = await webResponse.text();
reply.send(body);
});
await fastify.listen({ port: 3000 });
```
***
## Hono
Hono's native `Request`/`Response` API makes integration trivial:
```typescript
import { Hono } from 'hono';
import { bot } from './lib/bot';
const app = new Hono();
app.all('/api/bot/:adapter', async (c) => {
const webResponse = await bot.handle(c.req.param('adapter'), c.req.raw);
return webResponse;
});
export default app;
```
***
## Bun.serve / Node.js HTTP
For minimal Node.js or Bun servers:
```typescript
// Bun
import { bot } from './lib/bot';
Bun.serve({
port: 3000,
async fetch(request) {
const url = new URL(request.url);
const adapter = url.pathname.split('/')[3]; // /api/bot/{adapter}
if (!adapter) return new Response('Not Found', { status: 404 });
return bot.handle(adapter, request);
},
});
```
```typescript
// Node.js http module
import { createServer } from 'http';
import { bot } from './lib/bot';
const server = createServer(async (req, res) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
const adapter = url.pathname.split('/')[3];
if (!adapter) {
res.writeHead(404);
res.end('Not Found');
return;
}
const webRequest = new Request(url.toString(), {
method: req.method,
headers: req.headers as HeadersInit,
body: req.method === 'POST' ? await readBody(req) : undefined,
});
const webResponse = await bot.handle(adapter, webRequest);
res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers));
const body = await webResponse.text();
res.end(body);
});
server.listen(3000);
function readBody(req: IncomingMessage): Promise {
return new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => resolve(body));
});
}
```
***
## Proactive Messaging (Push Notifications)
To send messages proactively (not in response to a user message), use `bot.send()`:
```typescript
// Send a notification to a user on WhatsApp
await bot.send({
provider: 'whatsapp',
channel: '5511999999999', // User's phone number
content: {
type: 'interactive',
text: '📦 Your order #12345 has shipped!',
buttons: [
{ id: 'track', label: '📍 Track Order', action: 'callback', data: 'track_12345' },
{ id: 'support', label: '🆘 Support', action: 'callback', data: 'support_12345' },
],
},
});
// Send to Telegram
await bot.send({
provider: 'telegram',
channel: '123456789', // Telegram chat ID
content: { type: 'text', content: '🔔 Reminder: Your appointment is tomorrow at 2 PM.' },
});
// Send an image on Discord
await bot.send({
provider: 'discord',
channel: '789012345', // Discord channel ID
content: {
type: 'image',
content: 'https://cdn.example.com/chart.png',
caption: '📊 Weekly Analytics Report',
},
});
```
For proactive messaging, you need to store the user's platform-specific channel ID (Telegram `chat_id`, WhatsApp phone number, Discord `channel_id`). This is available in `ctx.channel.id` during normal interactions.
***
## Serverless / Edge
For serverless environments (Vercel, Cloudflare Workers, AWS Lambda):
### Vercel Edge Functions
```typescript
// app/api/bot/[adapter]/route.ts
import { bot } from '@/lib/bot';
export async function GET(request: Request, { params }: { params: { adapter: string } }) {
return bot.handle(params.adapter, request);
}
export async function POST(request: Request, { params }: { params: { adapter: string } }) {
return bot.handle(params.adapter, request);
}
```
### Cloudflare Workers
```typescript
import { bot } from './lib/bot';
export default {
async fetch(request: Request): Promise {
const url = new URL(request.url);
const adapter = url.pathname.split('/')[3];
if (!adapter) {
return new Response('Not Found', { status: 404 });
}
return bot.handle(adapter, request);
},
};
```
Serverless environments have cold starts. Initialize your bot **outside** the handler function (at module level) to reuse the instance across invocations.
***
## Environment Variables in Production
In production, configure your webhook URLs through the platform dashboards:
| Platform | Where to Set |
| -------- | -------------------------------------------------------- |
| Telegram | `setWebhook` API (called automatically by `bot.start()`) |
| WhatsApp | Meta Developer Dashboard → Webhook Configuration |
| Discord | Discord Developer Portal → Interactions Endpoint URL |
***
## Next Steps
---
# Introduction
> Universal bot framework for Node.js. Build bots once, deploy to Telegram, WhatsApp, Discord, and more — with a fluent builder API, middleware pipeline, session management, and plugin system.
URL: https://igniterjs.com/docs/bots
## Overview
`@igniter-js/bot` is a **multi-platform bot framework** for Node.js and Bun. It lets you build conversational bots that run on Telegram, WhatsApp, and Discord simultaneously — sharing commands, middlewares, and session state across all platforms.
You define your bot once using the **builder pattern**, add adapters for each platform, and get a unified API for handling messages, commands, and interactive content — with full TypeScript autocomplete everywhere.
Bot works with **any platform that can receive HTTP requests**. The adapter pattern means you can write a custom adapter for any chat platform in under 100 lines, and it automatically works with the entire middleware, plugin, and session system.
### Key Features
* **Multi-Platform** — Telegram, WhatsApp, Discord, and custom adapters. One codebase, every chat platform.
* **Fluent Builder API** — `IgniterBot.create().withHandle().addAdapters().addCommand().build()` — fully typed at every step.
* **Command System** — Slash commands with aliases, subcommands, Zod argument validation, and automatic platform registration.
* **Middleware Pipeline** — `auth`, `rateLimit`, `logging` built-in. Extend with custom middleware for any use case.
* **Plugin Architecture** — Package commands, middlewares, adapters, and hooks into reusable plugins.
* **Session Management** — Persistent conversational state with pluggable storage (memory, Redis, Prisma).
* **Rich Content Types** — Send text, images, video, audio, documents, stickers, location, contacts, polls, and interactive buttons.
* **Type-Safe Context** — `BotContext` gives you typed access to message, channel, author, session, and reply helpers.
* **Framework Integration** — Native handlers for Next.js App Router and TanStack Start. Express/Fastify compatible via standard `Request`/`Response`.
* **Adapter Client** — Every adapter exposes a pre-configured HTTP client for making direct API calls when needed.
***
## Architecture
Bot follows the **adapter pattern** — the same pattern used across the Igniter.js ecosystem. The core handles commands, middlewares, sessions, and event routing. Adapters translate between platform-specific APIs and the normalized `BotContext`.
```mermaid
graph TD
A[Incoming Request] --> B[Platform Adapter]
B --> C[Adapter.handle]
C --> D{Normalized BotContext}
D --> E[Session Loader]
E --> F[Pre-Process Hooks]
F --> G[Middleware Pipeline]
G --> H[Command Router]
H --> I[Command Handler]
G --> J[Event Listeners]
I --> K[Reply via Adapter]
J --> K
K --> L[Platform API]
```
Every incoming message flows through: **adapter parsing → session loading → pre-process hooks → middleware chain → command routing → handler execution → reply dispatch**.
***
## Quick Start
Get a cross-platform bot running in under three minutes.
### Install
```bash
npm install @igniter-js/bot zod
```
```bash
pnpm add @igniter-js/bot zod
```
```bash
yarn add @igniter-js/bot zod
```
```bash
bun add @igniter-js/bot zod
```
### Create Your Bot
```typescript
import { IgniterBot, telegram, whatsapp, discord, memoryStore } from '@igniter-js/bot';
const bot = IgniterBot
.create()
.withHandle('@my_awesome_bot')
.withSessionStore(memoryStore())
.addAdapters({
telegram: telegram({ token: process.env.TELEGRAM_TOKEN! }),
whatsapp: whatsapp({
token: process.env.WHATSAPP_TOKEN!,
phone: process.env.WHATSAPP_PHONE!,
}),
discord: discord({
token: process.env.DISCORD_TOKEN!,
applicationId: process.env.DISCORD_APP_ID!,
publicKey: process.env.DISCORD_PUBLIC_KEY!,
}),
})
.addCommand('start', {
name: 'start',
aliases: ['hello', 'hi'],
description: 'Greets the user',
help: 'Use /start to receive a welcome message',
async handle(ctx) {
await ctx.reply(
`👋 Hello, ${ctx.message.author.name}! I'm running on ${ctx.provider}.`,
);
},
})
.addCommand('ping', {
name: 'ping',
aliases: [],
description: 'Check bot responsiveness',
help: 'Use /ping to check if the bot is alive',
async handle(ctx) {
const start = Date.now();
await ctx.reply('Pong! 🏓');
},
})
.build();
await bot.start();
```
✅ **The bot is now running and ready to accept requests.**
### Wire Up a Route (Next.js)
```typescript
// app/api/bot/[adapter]/route.ts
import { nextRouteHandlerAdapter } from '@igniter-js/bot';
import { bot } from '@/lib/bot';
export const { GET, POST } = nextRouteHandlerAdapter({
'my_awesome_bot': bot,
});
```
Now your bot responds to Telegram webhooks, WhatsApp Cloud API, and Discord interactions — all through a single route.
***
## Core Concepts
The `IgniterBot` builder is the recommended way to construct bots. It provides a fluent, type-safe API where each method narrows the generic types:
```typescript
const bot = IgniterBot
.create() // Start building
.withHandle('@bot') // Set global handle
.withSessionStore(store) // Configure session storage
.withLogger(pino()) // Structured logging
.addAdapter('telegram', tg) // Add a platform adapter
.addCommand('start', cmd) // Register a command
.addMiddleware(authMw) // Add middleware
.usePlugin(analytics) // Load a plugin
.onMessage(messageHandler) // Listen to events
.onError(errorHandler) // Handle errors
.onStart(startupHook) // Run on initialization
.build() // Materialize the Bot instance
```
After `.build()`, the bot is immutable. All configuration happens through the builder.
Adapters translate between platform-specific APIs and the normalized `BotContext`. Each adapter declares its **capabilities** (supported content types, actions, features, and limits) so the framework can validate operations at runtime.
**First-party adapters:**
* **Telegram** — Full support: webhooks, long polling, commands, inline keyboards, all content types
* **WhatsApp** — Cloud API: text, media, interactive buttons, contacts, location
* **Discord** — Interactions API: slash commands, buttons, embeds, thread support
Each adapter provides typed configuration via Zod schemas and supports environment variable defaults.
Commands are the primary interaction pattern. They support:
* **Aliases** — `/start`, `/hello`, `/hi` all pointing to the same handler
* **Subcommands** — Nested command structures (e.g., `/admin ban`, `/admin kick`)
* **Zod Validation** — Type-safe argument parsing with automatic error messages
* **Automatic Registration** — Commands are auto-registered with platforms that support it (Telegram bot commands, Discord slash commands)
* **Command Groups** — Prefix commands logically: `builder.addCommandGroup('admin', { ... })`
```typescript
builder.addCommand('search', {
name: 'search',
aliases: ['find', 's'],
description: 'Search for items',
help: 'Use /search <query> to find items',
args: z.object({ query: z.string().min(2) }),
async handle(ctx, args) {
const results = await searchDatabase(args.query);
await ctx.reply(`Found ${results.length} results.`);
},
})
```
Middlewares execute in sequence for every incoming message. They can enrich the context, block requests, or perform side effects:
```typescript
// Custom middleware
builder.addMiddleware(async (ctx, next) => {
console.log(`[${ctx.provider}] ${ctx.message.author.name}: ${ctx.message.content}`);
const start = Date.now();
await next(); // Continue to the next middleware or handler
console.log(`Completed in ${Date.now() - start}ms`);
})
```
**Built-in middlewares:**
* `authMiddleware` — User/channel allowlists, blocklists, role-based access
* `rateLimitMiddleware` — Configurable rate limiting with memory or Redis store
* `loggingMiddleware` — Structured logging with presets (minimal, standard, verbose, production)
Plugins package commands, middlewares, adapters, and lifecycle hooks into reusable modules:
```typescript
// Built-in analytics plugin
builder.usePlugin(analyticsPlugin({
trackEvent: async (event, props) => {
await posthog.capture(event, props);
},
trackMessages: true,
trackCommands: true,
}))
```
Plugins can also register their own commands (e.g., `/stats` from the analytics plugin), middlewares, and adapters — all composed automatically into the bot.
Sessions persist conversational state across messages. The framework provides a `BotSessionStore` interface with a built-in in-memory store and support for custom backends:
```typescript
// In-memory (development)
builder.withSessionStore(memoryStore())
// Redis (production)
builder.withSessionStore(redisStore(redisClient))
```
Access sessions in handlers via `ctx.session`:
```typescript
async handle(ctx) {
await ctx.session.update({ step: 'awaiting_name' });
const currentStep = ctx.session.data.step;
}
```
***
## Real-World Use Cases
Bot powers conversational applications across many domains:
| Use Case | Description |
| ------------------------- | ----------------------------------------------------------------------------- |
| **Customer Support** | Route inquiries across Telegram, WhatsApp, and Discord with a single codebase |
| **E-commerce Bots** | Product search, order tracking, cart management with interactive buttons |
| **Community Management** | Moderation commands, welcome messages, polls, and announcements |
| **SaaS Onboarding** | Step-by-step guided setup flows with persistent session state |
| **Notification Services** | Push alerts to users on their preferred platform via proactive messaging |
| **Internal Tools** | DevOps bots for deployment status, incident alerts, and team coordination |
***
## Next Steps
---
# Installation
> Install and configure @igniter-js/bot for your runtime. Choose the right adapters and set up your first bot in minutes.
URL: https://igniterjs.com/docs/bots/installation
## Installation
`@igniter-js/bot` works on **Node.js 18+** and **Bun 1.0+**. It requires `zod` as a peer dependency for adapter parameter validation.
```bash
npm install @igniter-js/bot zod
```
```bash
pnpm add @igniter-js/bot zod
```
```bash
yarn add @igniter-js/bot zod
```
```bash
bun add @igniter-js/bot zod
```
### Runtime Requirements
| Runtime | Version | Notes |
| ------------ | ------- | ----------------------------------------------- |
| Node.js | 18+ | Full support for all adapters |
| Bun | 1.0+ | Native `File` API support for media uploads |
| Edge Runtime | Limited | Only adapters that don't use Node-specific APIs |
***
## Choosing Adapters
Bot comes with three first-party adapters. Install only the platforms you need — the package is tree-shakeable.
### Telegram
**Best for:** General-purpose bots, group management, channel automation
```typescript
import { telegram } from '@igniter-js/bot';
const tgAdapter = telegram({
token: process.env.TELEGRAM_TOKEN!,
handle: '@your_bot', // Optional: overrides global handle
webhook: {
url: 'https://your-domain.com/api/bot/telegram',
secret: process.env.TELEGRAM_WEBHOOK_SECRET!, // Optional: validates webhook authenticity
dropPendingUpdates: true, // Clear pending updates on restart (default: true)
},
});
```
**Environment variables supported:**
* `TELEGRAM_TOKEN` — Bot API token (from @BotFather)
* `TELEGRAM_WEBHOOK_URL` — Public HTTPS endpoint for webhook
* `TELEGRAM_WEBHOOK_SECRET` — Secret token for webhook verification
### WhatsApp (Cloud API)
**Best for:** Customer support, e-commerce, notification bots
```typescript
import { whatsapp } from '@igniter-js/bot';
const waAdapter = whatsapp({
token: process.env.WHATSAPP_TOKEN!,
phone: process.env.WHATSAPP_PHONE!, // Phone number ID from Meta Business
});
```
WhatsApp Cloud API requires a Meta Business account and a verified phone number. The `init` method is a no-op — webhook setup is managed through the Meta Developer dashboard.
### Discord
**Best for:** Community servers, gaming, developer communities
```typescript
import { discord } from '@igniter-js/bot';
const dcAdapter = discord({
token: process.env.DISCORD_TOKEN!,
applicationId: process.env.DISCORD_APP_ID!,
publicKey: process.env.DISCORD_PUBLIC_KEY!, // Required for interaction verification
});
```
Discord requires interaction signature verification. Always provide `publicKey` in production, or the adapter will log a warning on every request.
### Adapter Capability Comparison
| Feature | Telegram | WhatsApp | Discord |
| ------------------- | ---------------- | ----------- | ------------- |
| Text messages | ✅ | ✅ | ✅ |
| Images | ✅ | ✅ | ✅ |
| Video | ✅ | ✅ | ✅ |
| Audio | ✅ | ✅ | ✅ |
| Documents | ✅ | ✅ | ✅ |
| Stickers | ✅ | ✅ | ❌ |
| Location | ✅ | ✅ | ❌ |
| Contacts | ✅ | ✅ | ❌ |
| Polls | ✅ | ❌ | ❌ |
| Interactive buttons | ✅ | ✅ (max 3) | ✅ (max 5/row) |
| Edit messages | ✅ | ❌ | ✅ |
| Delete messages | ✅ | ❌ | ✅ |
| Reactions | ❌ | ✅ | ✅ |
| Threads | ❌ | ❌ | ✅ |
| Webhooks | ✅ | ✅ | ✅ |
| Long polling | ✅ | ❌ | ❌ |
| Slash commands | ❌ (bot commands) | ❌ | ✅ |
| Groups | ✅ | ✅ | ✅ (servers) |
| Max message length | 4,096 chars | 4,096 chars | 2,000 chars |
| Max file size | 50 MB | 100 MB | 25 MB |
***
## Framework Integration Packages
### Next.js App Router
The `nextRouteHandlerAdapter` creates a ready-to-use route handler:
```typescript
// app/api/bot/[adapter]/[botId]/route.ts
import { nextRouteHandlerAdapter } from '@igniter-js/bot';
export const { GET, POST } = nextRouteHandlerAdapter(bots);
```
The adapter expects dynamic route segments `[adapter]` and `[botId]`. It automatically handles webhook verification (GET) and message processing (POST).
### TanStack Start
The `tanstackStartRouteHandlerAdapter` follows the same pattern:
```typescript
// app/routes/api/bot/$adapter/$botId.ts
import { tanstackStartRouteHandlerAdapter } from '@igniter-js/bot';
export const { GET, POST } = tanstackStartRouteHandlerAdapter(bots);
```
### Express / Fastify / Hono / Any HTTP Framework
Bot adapters work with any framework through the standard `Request`/`Response` API:
```typescript
// Express example
app.post('/api/bot/:adapter', async (req, res) => {
const webResponse = await bot.handle(req.params.adapter, toWebRequest(req));
res.status(webResponse.status);
// Forward headers and body...
});
```
See the [Framework Integration](/docs/bots/framework-integration) guide for complete examples with Express, Fastify, Hono, and more.
***
## Environment Variables Reference
Bot adapters read from environment variables as defaults. You only need to pass config values explicitly when the env vars differ.
| Variable | Used By | Description |
| ------------------------- | -------- | ------------------------------------- |
| `TELEGRAM_TOKEN` | Telegram | Bot API token |
| `TELEGRAM_WEBHOOK_URL` | Telegram | Webhook endpoint URL |
| `TELEGRAM_WEBHOOK_SECRET` | Telegram | Webhook secret token |
| `WHATSAPP_TOKEN` | WhatsApp | Cloud API access token |
| `WHATSAPP_PHONE` | WhatsApp | Phone number ID |
| `DISCORD_TOKEN` | Discord | Bot token |
| `DISCORD_APP_ID` | Discord | Application ID |
| `DISCORD_PUBLIC_KEY` | Discord | Public key for signature verification |
***
## Next Steps
---
# Middlewares
> Secure, rate-limit, and monitor your bot with built-in middlewares. Create custom middleware for authentication, logging, and request enrichment.
URL: https://igniterjs.com/docs/bots/middlewares
## Middlewares
Middlewares execute in sequence for every incoming message. They can **enrich the context**, **block requests**, or **perform side effects** like logging and analytics. The framework ships with three production-ready middlewares and a flexible API for custom ones.
***
## Middleware Contract
```typescript
type Middleware = (
ctx: TContextIn,
next: () => Promise,
) => Promise>;
```
| Action | How |
| ------------------ | -------------------------------------------------------------- |
| **Pass control** | Call `await next()` |
| **Block request** | Don't call `next()` — pipeline stops here |
| **Enrich context** | Return an object — merged into context for downstream handlers |
| **Side effects** | Do work before and/or after `next()` |
| **Error handling** | Wrap `next()` in try/catch |
***
## Built-in Middlewares
### Auth Middleware
Controls access based on user IDs, channel IDs, custom logic, or roles.
```typescript
import { authMiddleware, authPresets, roleMiddleware } from '@igniter-js/bot';
```
#### Basic Usage
```typescript
// Allow only specific users
builder.addMiddleware(authMiddleware({
allowedUsers: ['user_123', 'user_456'],
unauthorizedMessage: '⛔ You are not authorized.',
}));
// Block specific users
builder.addMiddleware(authMiddleware({
blockedUsers: ['spammer_789'],
}));
// Allow only in specific channels
builder.addMiddleware(authMiddleware({
allowedChannels: ['channel_support', 'channel_vip'],
}));
```
#### Custom Authorization
```typescript
builder.addMiddleware(authMiddleware({
checkFn: async (ctx) => {
const user = await database.getUser(ctx.message.author.id);
return user?.plan === 'premium'; // Only premium users
},
unauthorizedMessage: (ctx) => `Upgrade to premium, ${ctx.message.author.name}!`,
skip: (ctx) => ctx.message.content?.type === 'command' && ctx.message.content.command === 'help',
onUnauthorized: async (ctx) => {
await analytics.track('unauthorized_access', { userId: ctx.message.author.id });
},
}));
```
| Option | Type | Description |
| --------------------- | -------------------------------------- | ---------------------------------- |
| `allowedUsers` | `string[]` | Only these user IDs can access |
| `allowedChannels` | `string[]` | Only these channel IDs are active |
| `blockedUsers` | `string[]` | These user IDs are blocked |
| `blockedChannels` | `string[]` | These channel IDs are blocked |
| `checkFn` | `(ctx) => boolean \| Promise` | Custom authorization logic |
| `unauthorizedMessage` | `string \| (ctx) => string` | Message sent on denial |
| `skip` | `(ctx) => boolean \| Promise` | Skip auth under certain conditions |
| `onUnauthorized` | `(ctx) => void \| Promise` | Custom handler for denied access |
#### Role-Based Auth
```typescript
builder.addMiddleware(roleMiddleware({
getRoles: async (userId) => {
const user = await db.users.findById(userId);
return user?.roles ?? [];
},
requiredRoles: ['admin', 'moderator'],
unauthorizedMessage: '🔒 Admins and moderators only.',
}));
```
#### Pre-built Presets
```typescript
// Admin-only bot
builder.addMiddleware(authPresets.adminsOnly(['admin_001', 'admin_002']));
// Private chat only
builder.addMiddleware(authPresets.privateOnly());
// Group chat only
builder.addMiddleware(authPresets.groupsOnly());
// Whitelist
builder.addMiddleware(authPresets.whitelist(['user_a', 'user_b']));
// Blacklist
builder.addMiddleware(authPresets.blacklist(['spammer_x']));
```
***
### Rate Limit Middleware
Prevents abuse by limiting the number of requests per user within a time window.
```typescript
import { rateLimitMiddleware, rateLimitPresets, memoryRateLimitStore } from '@igniter-js/bot';
```
#### Basic Usage
```typescript
builder.addMiddleware(rateLimitMiddleware({
maxRequests: 10,
windowMs: 60_000, // 1 minute
message: '⚠️ Too many requests. Please wait a minute.',
}));
```
| Option | Type | Default | Description |
| ---------------- | -------------------------------------------- | ---------------------- | ---------------------------- |
| `maxRequests` | `number` | Required | Max requests in the window |
| `windowMs` | `number` | Required | Time window in milliseconds |
| `store` | `RateLimitStore` | `MemoryRateLimitStore` | Storage backend |
| `keyGenerator` | `(ctx) => string` | `provider:userId` | Custom key for rate limiting |
| `message` | `string \| (ctx, retryAfter) => string` | Default message | Response on limit exceeded |
| `skip` | `(ctx) => boolean \| Promise` | — | Skip rate limiting |
| `onLimitReached` | `(ctx, retryAfter) => void \| Promise` | — | Custom handler |
#### Custom Key Generator
Rate limit by a combination of provider, user, and command:
```typescript
builder.addMiddleware(rateLimitMiddleware({
maxRequests: 3,
windowMs: 10_000, // 10 seconds
keyGenerator: (ctx) => {
const cmd = ctx.message.content?.type === 'command'
? ctx.message.content.command
: 'message';
return `${ctx.provider}:${ctx.message.author.id}:${cmd}`;
},
message: (ctx, retryAfter) =>
`Command rate limit reached. Try again in ${retryAfter}s.`,
}));
```
#### Redis Store
For production, use a shared store like Redis:
```typescript
import { RateLimitStore } from '@igniter-js/bot';
class RedisRateLimitStore implements RateLimitStore {
constructor(private redis: Redis) {}
async get(key: string): Promise {
return parseInt(await this.redis.get(key) || '0');
}
async increment(key: string, windowMs: number): Promise {
const count = await this.redis.incr(key);
await this.redis.pexpire(key, windowMs);
return count;
}
async reset(key: string): Promise {
await this.redis.del(key);
}
}
builder.addMiddleware(rateLimitMiddleware({
maxRequests: 20,
windowMs: 60_000,
store: new RedisRateLimitStore(redisClient),
}));
```
#### Pre-built Presets
```typescript
// Strict: 5 req/min
builder.addMiddleware(rateLimitPresets.strict());
// Moderate: 10 req/min
builder.addMiddleware(rateLimitPresets.moderate());
// Lenient: 20 req/min
builder.addMiddleware(rateLimitPresets.lenient());
// Per-command: 3 req/10s per command
builder.addMiddleware(rateLimitPresets.perCommand());
```
***
### Logging Middleware
Structured logging for messages, commands, errors, and performance metrics.
```typescript
import { loggingMiddleware, loggingPresets, commandLoggingMiddleware } from '@igniter-js/bot';
```
#### Basic Usage
```typescript
builder.addMiddleware(loggingMiddleware({
logMessages: true,
logCommands: true,
logErrors: true,
logMetrics: true,
includeUserInfo: true,
includeContent: false, // Security: don't log message content
}));
```
| Option | Type | Default | Description |
| ----------------- | ------------------------------ | -------------- | --------------------------------------- |
| `logger` | `BotLogger` | `console` | Logger instance (Pino, Winston, etc.) |
| `logMessages` | `boolean` | `true` | Log incoming messages |
| `logCommands` | `boolean` | `true` | Log command executions |
| `logErrors` | `boolean` | `true` | Log errors |
| `logMetrics` | `boolean` | `true` | Log execution time |
| `includeUserInfo` | `boolean` | `true` | Include user id/username |
| `includeContent` | `boolean` | `false` | Include message content (security risk) |
| `formatter` | `(ctx, event, data) => string` | Default format | Custom log format |
| `skip` | `(ctx) => boolean` | — | Skip logging conditions |
#### With Structured Logger
```typescript
import pino from 'pino';
const logger = pino({ level: 'info' });
builder.addMiddleware(loggingMiddleware({
logger,
logMessages: true,
logMetrics: true,
includeUserInfo: true,
includeContent: false,
}));
```
The middleware calls `logger.info()`, `logger.debug()`, `logger.warn()`, and `logger.error()` — compatible with any logger implementing the `BotLogger` interface.
#### Pre-built Presets
```typescript
// Minimal: only errors
builder.addMiddleware(loggingPresets.minimal());
// Standard: messages, commands, errors
builder.addMiddleware(loggingPresets.standard());
// Verbose: everything including metrics and content
builder.addMiddleware(loggingPresets.verbose());
// Production: standard logging without PII
builder.addMiddleware(loggingPresets.production());
// Debug: full JSON output for troubleshooting
builder.addMiddleware(loggingPresets.debug());
```
#### Command-Specific Logging
For detailed per-command logs with parameters:
```typescript
builder.addMiddleware(commandLoggingMiddleware({
logger: pino(),
includeParams: true,
}));
// Output:
// [COMMAND] /search by @username
// [PARAMS] ["typescript books"]
// [SUCCESS] /search completed in 45ms
```
***
## Custom Middleware
### Context Enrichment
Add properties to the context for downstream handlers:
```typescript
builder.addMiddleware(async (ctx, next) => {
const user = await db.users.findById(ctx.message.author.id);
await next();
return { user }; // ctx.user now available in all subsequent handlers
});
```
### Timing & Performance
```typescript
builder.addMiddleware(async (ctx, next) => {
const start = performance.now();
try {
await next();
} finally {
const ms = performance.now() - start;
metrics.histogram('bot.request.duration', ms, {
provider: ctx.provider,
command: ctx.message.content?.type === 'command' ? ctx.message.content.command : 'message',
});
}
});
```
### Feature Flags
```typescript
builder.addMiddleware(async (ctx, next) => {
const flags = await featureFlags.getForUser(ctx.message.author.id);
if (!flags.newSearchEnabled && ctx.message.content?.type === 'command' && ctx.message.content.command === 'search') {
await ctx.reply('The new search feature is not available for your account yet.');
return; // Block pipeline
}
await next();
});
```
### Error Boundary
```typescript
builder.addMiddleware(async (ctx, next) => {
try {
await next();
} catch (error) {
await errorTracking.captureException(error, {
userId: ctx.message.author.id,
provider: ctx.provider,
command: ctx.message.content?.type === 'command' ? ctx.message.content.command : undefined,
});
await ctx.reply('⚠️ An unexpected error occurred. Our team has been notified.');
// Don't re-throw — gracefully handled
}
});
```
***
## Middleware Ordering Matters
The order you add middlewares determines the execution order:
```
1. Logging middleware → Logs before/after everything
2. Rate limit middleware → Check limits early
3. Auth middleware → Block unauthorized users
4. Custom enrichment → Load user/profile data
5. Command handler → Execute the command
```
```typescript
builder
.addMiddleware(loggingPresets.production()) // 1st: log everything
.addMiddleware(rateLimitPresets.moderate()) // 2nd: check rate limits
.addMiddleware(authPresets.whitelist(['...'])) // 3rd: auth check
.addMiddleware(async (ctx, next) => { // 4th: enrich context
const user = await loadUser(ctx.message.author.id);
await next();
return { user };
});
```
Place **rate limiting before auth** so that rate limits apply to unauthorized users too — otherwise attackers can bypass rate limits by triggering auth failures.
***
## Next Steps
---
# Plugins
> Extend your bot with modular plugins. Package commands, middlewares, adapters, and lifecycle hooks into reusable, shareable modules.
URL: https://igniterjs.com/docs/bots/plugins
## Plugins
Plugins are the **modular extension system** for `@igniter-js/bot`. A plugin packages commands, middlewares, adapters, and lifecycle hooks into a single, reusable module that can be shared across bots and projects.
***
## Plugin Contract
```typescript
interface BotPlugin {
name: string // Unique plugin name
version: string // Semver version
description?: string // What the plugin does
commands?: Record // Commands to register
middlewares?: Middleware[] // Middlewares to add
adapters?: Record> // Adapters to register
hooks?: {
onStart?: () => Promise | void
onMessage?: (ctx: BotContext) => Promise | void
onError?: (ctx: BotContext & { error: BotError }) => Promise | void
onStop?: () => Promise | void
}
config?: Record // Plugin-specific configuration
}
```
When a plugin is loaded via `.usePlugin()`, the builder automatically:
1. Registers all plugin commands
2. Appends all plugin middlewares to the pipeline
3. Registers all plugin adapters
4. Attaches all lifecycle hooks to the bot
***
## Built-in Plugin: Analytics
The `analyticsPlugin` tracks bot usage metrics and provides a `/stats` command.
### Installation
```typescript
import { analyticsPlugin } from '@igniter-js/bot';
```
### Basic Usage
```typescript
const bot = IgniterBot.create()
.usePlugin(analyticsPlugin())
.build();
```
This enables basic console-logged analytics and the `/stats` command:
```
/stats
📊 Bot Statistics
Messages: 1,247
Commands: 342
Errors: 3
Unique Users: 89
```
### With External Analytics Service
```typescript
import { analyticsPlugin } from '@igniter-js/bot';
import { PostHog } from 'posthog-node';
const posthog = new PostHog(process.env.POSTHOG_API_KEY!);
const bot = IgniterBot.create()
.usePlugin(analyticsPlugin({
trackEvent: async (event, properties) => {
await posthog.capture({
distinctId: properties.userId || 'anonymous',
event,
properties,
});
},
trackMessages: true,
trackCommands: true,
trackErrors: true,
includeUserInfo: true,
}))
.build();
```
| Option | Type | Default | Description |
| ----------------- | ----------------------------------------- | ------------- | ---------------------------- |
| `trackEvent` | `(event, props) => void \| Promise` | `console.log` | Where to send events |
| `trackMessages` | `boolean` | `true` | Track message events |
| `trackCommands` | `boolean` | `true` | Track command events |
| `trackErrors` | `boolean` | `true` | Track error events |
| `includeUserInfo` | `boolean` | `true` | Include user ID and username |
### Events Tracked
| Event | Properties | When |
| ------------- | --------------------------------------------------------------- | ---------------------------- |
| `bot.started` | `timestamp` | Bot initialization |
| `bot.message` | `provider, contentType, isGroup, isMentioned, userId, username` | Every incoming message |
| `bot.command` | Same as message + `command, params` | Every command execution |
| `bot.error` | `provider, errorMessage, errorCode, userId` | Every error caught |
| `bot.request` | `provider, event, duration, isGroup` | After every request (timing) |
***
## Creating Custom Plugins
### Example: Welcome Plugin
A plugin that sends a welcome message to new users:
```typescript
// plugins/welcome.ts
import type { BotPlugin } from '@igniter-js/bot';
export function welcomePlugin(options?: {
message?: string;
trackNewUsers?: boolean;
}): BotPlugin {
const { message = 'Welcome to the bot! 👋', trackNewUsers = true } = options || {};
const seenUsers = new Set();
return {
name: 'welcome',
version: '1.0.0',
description: 'Sends a welcome message to new users',
middlewares: [
async (ctx, next) => {
const userId = ctx.message.author.id;
if (!seenUsers.has(userId)) {
seenUsers.add(userId);
await ctx.reply(message);
if (trackNewUsers) {
console.log(`New user: ${ctx.message.author.username} on ${ctx.provider}`);
}
}
await next();
},
],
};
}
```
Use it:
```typescript
import { welcomePlugin } from './plugins/welcome';
const bot = IgniterBot.create()
.usePlugin(welcomePlugin({
message: '🎉 Welcome to our community! Type /help to get started.',
trackNewUsers: true,
}))
.build();
```
### Example: Scheduled Tasks Plugin
A plugin that runs periodic tasks:
```typescript
// plugins/scheduler.ts
import type { BotPlugin } from '@igniter-js/bot';
export function schedulerPlugin(options: {
intervalMs: number;
task: () => Promise;
}): BotPlugin {
return {
name: 'scheduler',
version: '1.0.0',
description: 'Runs periodic background tasks',
hooks: {
onStart: () => {
setInterval(async () => {
try {
await options.task();
} catch (error) {
console.error('[scheduler] Task failed:', error);
}
}, options.intervalMs);
},
},
};
}
```
### Example: Multi-Tenant Plugin
A plugin that adds tenant-aware middleware and a tenant command:
```typescript
// plugins/multi-tenant.ts
import type { BotPlugin, BotContext } from '@igniter-js/bot';
export function multiTenantPlugin(options: {
getTenant: (userId: string) => Promise;
}): BotPlugin {
return {
name: 'multi-tenant',
version: '1.0.0',
description: 'Adds multi-tenant context to bot interactions',
middlewares: [
async (ctx, next) => {
const tenantId = await options.getTenant(ctx.message.author.id);
await next();
return { tenantId }; // Available in downstream handlers
},
],
commands: {
tenant: {
name: 'tenant',
aliases: ['workspace'],
description: 'Show current tenant',
help: 'Use /tenant to see your current workspace',
async handle(ctx) {
const tenantId = (ctx as any).tenantId || 'unknown';
await ctx.reply(`🏢 Current workspace: **${tenantId}**`);
},
},
},
};
}
```
***
## Plugin Factories
Plugins can be factories that accept configuration:
```typescript
type BotPluginFactory = (config?: TConfig) => BotPlugin;
// Usage
const plugin = myPlugin({ apiKey: '...', environment: 'production' });
builder.usePlugin(plugin);
```
***
## Composing Multiple Plugins
Plugins are loaded in order and their middlewares are appended to the pipeline:
```typescript
const bot = IgniterBot.create()
.usePlugin(welcomePlugin({ message: 'Hello!' })) // 1st
.usePlugin(analyticsPlugin({ trackMessages: true })) // 2nd
.usePlugin(multiTenantPlugin({ getTenant })) // 3rd
.build();
// Middleware order: welcome → analytics → multi-tenant → your custom middlewares → commands
```
Plugin middlewares are appended **before** `.build()` returns, but **after** any middlewares already added via `.addMiddleware()`. If order matters, add your middlewares after loading plugins.
***
## Next Steps
---
# Quick Start
> Build a production-ready multi-platform bot in 3 minutes. Covers Telegram, WhatsApp, and Discord with a complete example.
URL: https://igniterjs.com/docs/bots/quick-start
## Quick Start
This guide walks you through building a **multi-platform bot** that responds to commands on Telegram, WhatsApp, and Discord — all from the same codebase. By the end, you'll have a bot with commands, middleware, and session management running in production.
This guide assumes you have a Next.js project. The same patterns work with Express, Fastify, Hono, or any Node.js framework — see [Framework Integration](/docs/bots/framework-integration).
***
## Step 1: Install Dependencies
```bash
npm install @igniter-js/bot zod
```
```bash
pnpm add @igniter-js/bot zod
```
```bash
yarn add @igniter-js/bot zod
```
```bash
bun add @igniter-js/bot zod
```
***
## Step 2: Set Up Environment Variables
Create a `.env.local` file:
```bash
# Telegram
TELEGRAM_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
# WhatsApp (Cloud API)
WHATSAPP_TOKEN=EAAx...
WHATSAPP_PHONE=1234567890
# Discord
DISCORD_TOKEN=MTIz...
DISCORD_APP_ID=123456789012345678
DISCORD_PUBLIC_KEY=abc123...
```
Never commit `.env.local` to version control. Add it to `.gitignore`.
***
## Step 3: Create Your Bot
Create `lib/bot.ts`:
```typescript
import {
IgniterBot,
telegram,
whatsapp,
discord,
memoryStore,
loggingMiddleware,
rateLimitMiddleware,
analyticsPlugin,
} from '@igniter-js/bot';
export const bot = IgniterBot
.create()
// Identity — auto-generates id and name from handle
.withHandle('@assistant_bot')
// Session storage for conversational state
.withSessionStore(memoryStore())
// Structured logging (Pino, Winston, or console)
.withLogger({
info: (...args) => console.log('[bot]', ...args),
error: (...args) => console.error('[bot]', ...args),
debug: (...args) => console.debug('[bot]', ...args),
warn: (...args) => console.warn('[bot]', ...args),
})
// All platforms at once
.addAdapters({
telegram: telegram({ token: process.env.TELEGRAM_TOKEN! }),
whatsapp: whatsapp({
token: process.env.WHATSAPP_TOKEN!,
phone: process.env.WHATSAPP_PHONE!,
}),
discord: discord({
token: process.env.DISCORD_TOKEN!,
applicationId: process.env.DISCORD_APP_ID!,
publicKey: process.env.DISCORD_PUBLIC_KEY!,
}),
})
// Middleware — runs for every message
.addMiddlewares([
loggingMiddleware({
logMessages: true,
logCommands: true,
logMetrics: true,
includeUserInfo: true,
}),
rateLimitMiddleware({
maxRequests: 10,
windowMs: 60_000, // 1 minute
message: '⚠️ Please slow down. Try again in a minute.',
}),
])
// Commands
.addCommand('start', {
name: 'start',
aliases: ['hello', 'hi'],
description: 'Start the bot',
help: 'Use /start to begin interacting with the bot',
async handle(ctx) {
await ctx.reply(
`👋 Welcome, ${ctx.message.author.name}!\n\n` +
`I'm running on **${ctx.provider}**.\n\n` +
`Type /help to see what I can do.`,
);
},
})
.addCommand('help', {
name: 'help',
aliases: ['commands', '?'],
description: 'Show available commands',
help: 'Use /help to see the list of commands',
async handle(ctx) {
await ctx.reply(
`📋 **Available Commands**\n\n` +
`/start — Start the bot\n` +
`/help — Show this message\n` +
`/echo <text> — Repeat your message\n` +
`/ping — Check bot responsiveness\n` +
`/image — Send a sample image\n` +
`/buttons — Show interactive buttons\n` +
`/session — Test session persistence`,
);
},
})
.addCommand('echo', {
name: 'echo',
aliases: ['say', 'repeat'],
description: 'Echo back your message',
help: 'Use /echo <text> to repeat your message',
async handle(ctx) {
const message = ctx.message.content;
const text = message?.type === 'command'
? message.params.join(' ')
: 'Nothing to echo!';
if (!text.trim()) {
await ctx.reply('Please provide some text. Example: `/echo Hello World`');
return;
}
await ctx.reply(`🔊 ${text}`);
},
})
.addCommand('ping', {
name: 'ping',
aliases: [],
description: 'Check bot latency',
help: 'Use /ping to check if the bot is responsive',
async handle(ctx) {
const start = Date.now();
await ctx.reply(`🏓 Pong! \`${Date.now() - start}ms\``);
},
})
.addCommand('image', {
name: 'image',
aliases: ['pic', 'photo'],
description: 'Send a sample image',
help: 'Use /image to receive a sample image',
async handle(ctx) {
await ctx.replyWithImage(
'https://placekitten.com/400/300',
'Here is a random kitten! 🐱',
);
},
})
.addCommand('buttons', {
name: 'buttons',
aliases: [],
description: 'Show interactive buttons',
help: 'Use /buttons to see interactive elements',
async handle(ctx) {
await ctx.replyWithButtons('Choose an option:', [
{ id: 'btn_1', label: 'Option A', action: 'callback', data: 'selected_a' },
{ id: 'btn_2', label: 'Option B', action: 'callback', data: 'selected_b' },
{ id: 'btn_3', label: 'Visit Website', action: 'url', data: { url: 'https://example.com' } },
]);
},
})
.addCommand('session', {
name: 'session',
aliases: [],
description: 'Test session persistence',
help: 'Use /session to see your conversation state',
async handle(ctx) {
const count = (ctx.session.data.visitCount || 0) + 1;
await ctx.session.update({ visitCount: count });
await ctx.reply(
`📊 You've used the /session command **${count}** time(s) in this chat.`,
);
},
})
// Event handlers
.onMessage(async (ctx) => {
// Called for every non-command message
console.log(`[message] ${ctx.provider} | ${ctx.message.author.name}`);
})
.onError(async (ctx) => {
// Called when an error occurs in any handler
console.error(`[error]`, ctx.error);
await ctx.reply('⚠️ Something went wrong. Please try again.');
})
.onStart(async () => {
console.log('✅ Bot started successfully!');
})
// Analytics plugin — tracks usage metrics
.usePlugin(analyticsPlugin({
trackMessages: true,
trackCommands: true,
trackErrors: true,
}))
// Build it!
.build();
// Start adapters (registers webhooks, commands, etc.)
await bot.start();
```
✅ **Your bot is now initialized.** The `.start()` call registers webhooks (Telegram), syncs commands (Discord), and runs startup hooks.
***
## Step 4: Create the API Route
Create a route that bridges incoming webhook requests to your bot:
### Next.js App Router
```typescript
// app/api/bot/[adapter]/[botId]/route.ts
import { nextRouteHandlerAdapter } from '@igniter-js/bot';
import { bot } from '@/lib/bot';
export const { GET, POST } = nextRouteHandlerAdapter({
assistant_bot: bot,
});
```
The adapter expects these dynamic segments:
* `[adapter]` — `telegram`, `whatsapp`, or `discord`
* `[botId]` — matches the bot's `id` (derived from handle: `assistant_bot`)
### Test Locally
For local development with Telegram, use a tunneling service:
```bash
# Install ngrok or localtunnel
npx localtunnel --port 3000 --subdomain my-bot-dev
# Then set your Telegram webhook URL to:
# https://my-bot-dev.loca.lt/api/bot/telegram/assistant_bot
```
***
## Step 5: Verify Everything Works
### Test on Telegram
Open your bot on Telegram and type:
```
/start
/help
/ping
/echo Hello from Igniter!
/image
/buttons
```
Each command should respond immediately with the appropriate reply.
### Test Session Persistence
Type `/session` multiple times — the counter should increment:
```
/session → "You've used the /session command 1 time(s)"
/session → "You've used the /session command 2 time(s)"
```
### Test Rate Limiting
Send more than 10 messages in one minute — you should see:
```
⚠️ Please slow down. Try again in a minute.
```
***
## What's Next?
Your bot is running with:
* ✅ **3 platforms** (Telegram, WhatsApp, Discord)
* ✅ **6 commands** with help text and aliases
* ✅ **Logging middleware** with structured output
* ✅ **Rate limiting** with configurable limits
* ✅ **Session persistence** with in-memory store
* ✅ **Analytics tracking** for usage metrics
* ✅ **Error handling** with graceful recovery
---
# Real-World Examples
> Production-grade bot examples with complete code. Customer support, e-commerce, community management, onboarding flows, and more.
URL: https://igniterjs.com/docs/bots/real-world
## Real-World Examples
Complete, production-ready bot scenarios showing how `@igniter-js/bot` solves real problems. Each example is self-contained and can be adapted for your use case.
***
## 1. Customer Support Bot (Multi-Platform)
A support bot that routes inquiries across Telegram, WhatsApp, and Discord with session tracking and human escalation.
### Features
* Multi-platform (Telegram + WhatsApp + Discord)
* Ticket creation with session tracking
* FAQ self-service
* Escalation to human agents
* Proactive status updates
```typescript
// lib/bot.ts
import { IgniterBot, telegram, whatsapp, discord, memoryStore } from '@igniter-js/bot';
import { supportCommands } from './commands/support';
import { supportMiddleware } from './middleware/support';
export const supportBot = IgniterBot.create()
.withHandle('@support_bot')
.withSessionStore(memoryStore())
.addAdapters({
telegram: telegram({ token: process.env.TELEGRAM_TOKEN! }),
whatsapp: whatsapp({ token: process.env.WHATSAPP_TOKEN!, phone: process.env.WHATSAPP_PHONE! }),
discord: discord({ token: process.env.DISCORD_TOKEN!, applicationId: process.env.DISCORD_APP_ID! }),
})
.addMiddlewares([supportMiddleware])
.addCommands(supportCommands)
.onError(async (ctx) => {
await ctx.reply('Our support system encountered an error. A human agent will assist you shortly.');
})
.build();
await supportBot.start();
```
```typescript
// commands/support.ts
export const supportCommands = {
help: Bot.command({
name: 'help',
aliases: ['support', 'faq'],
description: 'Get help',
help: 'Use /help to see support options',
async handle(ctx) {
await ctx.replyWithButtons('How can we help?', [
{ id: 'faq_order', label: '📦 Order Issues', action: 'callback', data: 'faq_order' },
{ id: 'faq_account', label: '👤 Account Help', action: 'callback', data: 'faq_account' },
{ id: 'faq_billing', label: '💳 Billing', action: 'callback', data: 'faq_billing' },
]);
},
}),
ticket: Bot.command({
name: 'ticket',
aliases: ['report', 'issue'],
description: 'Create a support ticket',
help: 'Use /ticket <description> to report an issue',
async handle(ctx) {
const ticketId = await createSupportTicket({
userId: ctx.message.author.id,
platform: ctx.provider,
description: ctx.args.description,
});
await ctx.session.update({ activeTicket: ticketId });
await ctx.reply(
`✅ Ticket **#${ticketId}** created!\n\n` +
`We'll respond within 24 hours. Reply to this message to add more details.`,
);
},
}),
status: Bot.command({
name: 'status',
aliases: ['check'],
description: 'Check ticket status',
help: 'Use /status to check your open tickets',
async handle(ctx) {
const tickets = await getOpenTickets(ctx.message.author.id);
if (tickets.length === 0) {
await ctx.reply('You have no open tickets.');
return;
}
const statusList = tickets.map(t =>
`**#${t.id}** — ${t.status} — ${t.subject}`
).join('\n');
await ctx.reply(`📋 **Your Open Tickets:**\n\n${statusList}`);
},
}),
};
```
```typescript
// middleware/support.ts
export const supportMiddleware = async (ctx, next) => {
// If there's an active ticket, append messages to it
const activeTicket = ctx.session.data.activeTicket;
if (activeTicket && ctx.message.content?.type === 'text') {
await appendToTicket(activeTicket, {
author: ctx.message.author.name,
content: ctx.message.content.content,
platform: ctx.provider,
});
await ctx.reply('✅ Message added to your ticket.');
return; // Don't process as a command
}
await next();
};
```
***
## 2. E-Commerce Bot
A shopping bot with product search, cart management, and order tracking across platforms.
### Features
* Product catalog search
* Shopping cart with session persistence
* Order placement and tracking
* Interactive product browsing with buttons
* Multi-currency pricing
```typescript
// commands/shop.ts
import { z } from 'zod';
import { Bot } from '@igniter-js/bot';
export const shopCommands = {
search: Bot.command({
name: 'search',
aliases: ['find', 'browse'],
description: 'Search products',
help: 'Use /search <query> to find products',
args: z.object({ query: z.string().min(2) }),
async handle(ctx, args) {
await ctx.sendTyping!();
const products = await searchProducts(args.query);
if (products.length === 0) {
await ctx.reply(`No products found for "${args.query}".`);
return;
}
await ctx.session.update({ searchResults: products.slice(0, 10) });
await ctx.replyWithButtons(
`🔍 **${products.length}** results for "${args.query}":\n\n` +
products.slice(0, 5).map((p, i) => `${i + 1}. **${p.name}** — $${p.price}`).join('\n'),
products.slice(0, 5).map((p, i) => ({
id: `add_${p.id}`,
label: `🛒 Add ${p.name}`,
action: 'callback' as const,
data: p.id,
})),
);
},
}),
cart: Bot.command({
name: 'cart',
aliases: ['basket'],
description: 'View your cart',
help: 'Use /cart to see items in your cart',
async handle(ctx) {
const cart = ctx.session.data.cart || [];
if (cart.length === 0) {
await ctx.reply('🛒 Your cart is empty. Use /search to find products.');
return;
}
const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
await ctx.replyWithButtons(
`🛒 **Your Cart** (${cart.length} items)\n\n` +
cart.map((item, i) =>
`${i + 1}. ${item.name} ×${item.quantity} — $${item.price * item.quantity}`
).join('\n') +
`\n\n**Total: $${total.toFixed(2)}**`,
[
{ id: 'checkout', label: '💳 Checkout', action: 'callback', data: 'checkout' },
{ id: 'clear', label: '🗑 Clear Cart', action: 'callback', data: 'clear_cart' },
],
);
},
}),
checkout: Bot.command({
name: 'checkout',
aliases: ['buy', 'order'],
description: 'Place your order',
help: 'Use /checkout to complete your purchase',
async handle(ctx) {
const cart = ctx.session.data.cart || [];
if (cart.length === 0) {
await ctx.reply('Your cart is empty. Add items first with /search.');
return;
}
const order = await createOrder({
userId: ctx.message.author.id,
items: cart,
platform: ctx.provider,
});
await ctx.session.update({ cart: [], lastOrder: order.id });
await ctx.reply(
`✅ Order **#${order.id}** placed!\n\n` +
`Total: $${order.total.toFixed(2)}\n` +
`Track with: /track ${order.id}`,
);
},
}),
track: Bot.command({
name: 'track',
aliases: ['order'],
description: 'Track an order',
help: 'Use /track <order_id> to check status',
args: z.object({ orderId: z.string() }),
async handle(ctx, args) {
const order = await getOrder(args.orderId);
if (!order) {
await ctx.reply('Order not found. Check the ID and try again.');
return;
}
const statusEmoji = {
processing: '📦',
shipped: '🚚',
delivered: '✅',
cancelled: '❌',
};
await ctx.reply(
`${statusEmoji[order.status]} Order **#${order.id}**\n\n` +
`Status: **${order.status.toUpperCase()}**\n` +
`Items: ${order.itemCount}\n` +
`Placed: ${order.createdAt.toLocaleDateString()}`,
);
},
}),
};
```
***
## 3. Community Moderation Bot
A Discord-first moderation bot with admin commands, welcome messages, and automated moderation.
### Features
* Admin command group with role-based access
* Welcome messages for new members
* Message logging
* Auto-moderation (banned words, spam detection)
* `/stats` for server analytics
```typescript
// lib/bot.ts
import { IgniterBot, discord, authMiddleware, roleMiddleware } from '@igniter-js/bot';
const bot = IgniterBot.create()
.withHandle('@mod_bot')
.addAdapter('discord', discord({
token: process.env.DISCORD_TOKEN!,
applicationId: process.env.DISCORD_APP_ID!,
publicKey: process.env.DISCORD_PUBLIC_KEY!,
}))
.addMiddleware(roleMiddleware({
getRoles: async (userId) => {
const member = await discordApi.getGuildMember(process.env.GUILD_ID!, userId);
return member.roles;
},
requiredRoles: ['admin', 'moderator'],
unauthorizedMessage: '🔒 This command requires admin or moderator role.',
}))
.addCommandGroup('mod', {
ban: {
name: 'ban',
aliases: [],
description: 'Ban a user',
help: 'Use /mod_ban <user_id> [reason]',
async handle(ctx) {
await banUser(ctx.args.userId, ctx.args.reason);
await ctx.reply(`🚫 User ${ctx.args.userId} banned. Reason: ${ctx.args.reason || 'No reason provided'}`);
},
},
kick: {
name: 'kick',
aliases: [],
description: 'Kick a user',
help: 'Use /mod_kick <user_id>',
async handle(ctx) {
await kickUser(ctx.args.userId);
await ctx.reply(`👢 User ${ctx.args.userId} kicked.`);
},
},
warn: {
name: 'warn',
aliases: [],
description: 'Warn a user',
help: 'Use /mod_warn <user_id> <reason>',
async handle(ctx) {
const warning = await addWarning(ctx.args.userId, ctx.args.reason);
await ctx.reply(`⚠️ User ${ctx.args.userId} warned (${warning.count}/3). Reason: ${ctx.args.reason}`);
},
},
clear: {
name: 'clear',
aliases: ['purge'],
description: 'Clear messages',
help: 'Use /mod_clear <count>',
async handle(ctx) {
await clearMessages(ctx.channel.id, ctx.args.count);
await ctx.reply(`🧹 Cleared ${ctx.args.count} messages.`);
},
},
})
.build();
```
***
## 4. SaaS Onboarding Bot
A step-by-step onboarding bot that guides new users through setup.
### Features
* Multi-step guided flow
* Persistent session progress
* Input validation at each step
* Skip and reset capabilities
* Progress tracking
```typescript
// commands/onboarding.ts
export const onboardingCommands = {
start: Bot.command({
name: 'start',
aliases: ['onboard', 'setup'],
description: 'Start onboarding',
help: 'Use /start to begin setting up your account',
async handle(ctx) {
await ctx.session.update({
step: 1,
data: { startedAt: new Date().toISOString() },
});
await ctx.reply(
'🚀 **Welcome to Acme SaaS!**\n\n' +
'I\'ll guide you through setup in 4 quick steps.\n\n' +
'**Step 1/4:** What is your company name?',
);
},
}),
skip: Bot.command({
name: 'skip',
aliases: [],
description: 'Skip current step',
help: 'Use /skip to skip the current onboarding step',
async handle(ctx) {
const step = ctx.session.data.step;
if (!step) {
await ctx.reply('No onboarding in progress. Use /start to begin.');
return;
}
await ctx.session.update({ step: step + 1 });
await sendStepPrompt(ctx, step + 1);
},
}),
reset: Bot.command({
name: 'reset',
aliases: ['restart'],
description: 'Restart onboarding',
help: 'Use /reset to restart the onboarding process',
async handle(ctx) {
await ctx.session.delete();
await ctx.reply('🔄 Onboarding reset. Use /start to begin again.');
},
}),
};
// middleware/onboarding-flow.ts
export const onboardingFlowMiddleware = async (ctx, next) => {
const step = ctx.session.data.step;
// Only intercept non-command text messages during onboarding
if (!step || ctx.message.content?.type === 'command') {
return next();
}
const text = ctx.message.content?.type === 'text' ? ctx.message.content.content : '';
switch (step) {
case 1: // Company name
if (!text.trim()) {
await ctx.reply('Please enter a valid company name.');
return;
}
await ctx.session.update({
step: 2,
data: { ...ctx.session.data, companyName: text },
});
await ctx.reply(
'✅ Company: **' + text + '**\n\n' +
'**Step 2/4:** What industry are you in?\n\n' +
'Options: Tech, Retail, Healthcare, Finance, Education, Other',
);
return;
case 2: // Industry
const validIndustries = ['tech', 'retail', 'healthcare', 'finance', 'education', 'other'];
if (!validIndustries.includes(text.toLowerCase())) {
await ctx.reply('Please choose from: Tech, Retail, Healthcare, Finance, Education, Other');
return;
}
await ctx.session.update({
step: 3,
data: { ...ctx.session.data, industry: text },
});
await ctx.reply(
'✅ Industry: **' + text + '**\n\n' +
'**Step 3/4:** How many team members will use Acme?',
);
return;
case 3: // Team size
const size = parseInt(text);
if (isNaN(size) || size < 1) {
await ctx.reply('Please enter a valid number (e.g., 5, 25, 100).');
return;
}
await ctx.session.update({
step: 4,
data: { ...ctx.session.data, teamSize: size },
});
await ctx.replyWithButtons(
'✅ Team size: **' + size + '** members\n\n' +
'**Step 4/4:** Ready to complete setup?\n\n' +
`Company: **${ctx.session.data.companyName}**\n` +
`Industry: **${ctx.session.data.industry}**\n` +
`Team: **${ctx.session.data.teamSize}** members`,
[
{ id: 'confirm', label: '✅ Complete Setup', action: 'callback', data: 'confirm_onboarding' },
{ id: 'restart', label: '🔄 Start Over', action: 'callback', data: 'restart_onboarding' },
],
);
return;
case 4:
await ctx.reply('Please use the buttons above to confirm or restart.');
return;
}
};
async function sendStepPrompt(ctx, step) {
const prompts = {
1: '**Step 1/4:** What is your company name?',
2: '**Step 2/4:** What industry are you in?',
3: '**Step 3/4:** How many team members?',
4: '**Step 4/4:** Confirm your setup with the buttons below.',
};
await ctx.reply(prompts[step] || 'Onboarding complete!');
}
```
***
## 5. Notification Service Bot
A bot that sends proactive notifications to users on their preferred platform.
### Features
* User preference management (preferred platform)
* Proactive push notifications
* Scheduled reminders
* Multi-channel delivery
* Notification templates
```typescript
// services/notifications.ts
import { bot } from '../lib/bot';
export async function sendOrderUpdate(userId: string, orderId: string, status: string) {
const user = await getUserPreferences(userId);
if (!user) return;
const messages = {
confirmed: '✅ Your order has been confirmed!',
shipped: '🚚 Your order is on the way!',
delivered: '📦 Your order has been delivered!',
cancelled: '❌ Your order has been cancelled.',
};
await bot.send({
provider: user.preferredPlatform,
channel: user.platformChannelId,
content: {
type: 'interactive',
text: `${messages[status]}\n\nOrder: **#${orderId}**`,
buttons: [
{ id: 'track', label: '📍 Track', action: 'callback', data: `track_${orderId}` },
{ id: 'support', label: '🆘 Support', action: 'callback', data: `support_${orderId}` },
],
},
});
}
export async function sendDailyReminder(userId: string) {
const user = await getUserPreferences(userId);
if (!user) return;
await bot.send({
provider: user.preferredPlatform,
channel: user.platformChannelId,
content: {
type: 'text',
content: `☀️ Good morning, ${user.name}! Don't forget to check your tasks for today.`,
},
});
}
// Cron job (using node-cron or similar)
import cron from 'node-cron';
cron.schedule('0 9 * * *', async () => {
const users = await getUsersWithReminders();
for (const user of users) {
await sendDailyReminder(user.id);
}
});
```
***
## Architecture Pattern: Multi-Tenant Bot
A pattern for serving multiple organizations from a single bot instance:
```typescript
// lib/bot.ts
const bot = IgniterBot.create()
.withHandle('@platform_bot')
.addAdapters({
telegram: telegram({ token: process.env.TELEGRAM_TOKEN! }),
whatsapp: whatsapp({ token: process.env.WHATSAPP_TOKEN!, phone: process.env.WHATSAPP_PHONE! }),
})
.addMiddleware(async (ctx, next) => {
// Resolve tenant from user mapping
const tenant = await tenantResolver.resolve(ctx.message.author.id, ctx.provider);
if (!tenant) {
await ctx.reply('Your account is not associated with any organization.');
return;
}
await next();
return { tenant };
})
.addMiddleware(async (ctx, next) => {
// Switch database context
const db = await getTenantDatabase(ctx.tenant.id);
await next();
return { db };
})
.build();
```
***
## Next Steps
---
# Sessions
> Persist conversational state across messages. Use in-memory storage for development, or plug in Redis, Prisma, or any custom backend for production.
URL: https://igniterjs.com/docs/bots/sessions
## Sessions
Sessions persist conversational state across multiple messages. They're essential for **multi-step workflows**, **form filling**, **shopping carts**, **onboarding flows**, and any interaction that spans more than one message.
***
## Architecture
```
Incoming Message
↓
Session Loader (auto-loads from store)
↓
Middleware Pipeline
↓
Command Handler (reads/writes ctx.session)
↓
Session Auto-Save (persists changes after handler completes)
```
Sessions are keyed by `userId:channelId` — each user has a separate session per chat.
***
## Quick Start
```typescript
import { IgniterBot, memoryStore } from '@igniter-js/bot';
const bot = IgniterBot.create()
.withSessionStore(memoryStore())
// ... adapters, commands, etc.
.build();
```
Now every handler has access to `ctx.session`:
```typescript
builder.addCommand('start', {
name: 'start',
aliases: [],
description: 'Start onboarding',
help: 'Use /start to begin',
async handle(ctx) {
await ctx.session.update({ step: 'awaiting_name' });
await ctx.reply('Welcome! What is your name?');
},
});
```
***
## The BotSession Interface
```typescript
interface BotSession {
userId: string // Unique user identifier
channelId: string // Chat/channel identifier
data: Record // Your application state
createdAt: Date // Session creation timestamp
updatedAt: Date // Last update timestamp
expiresAt?: Date // Auto-cleanup on expiration
}
```
***
## BotSessionHelper (ctx.session)
The `ctx.session` object extends `BotSession` with convenience methods:
```typescript
interface BotSessionHelper extends BotSession {
save(): Promise
delete(): Promise
update(data: Partial>): Promise
}
```
| Method | Description |
| ------------------------------------ | ----------------------------------------- |
| `ctx.session.data` | Read current session state |
| `ctx.session.update({ key: value })` | Merge data into the session and auto-save |
| `ctx.session.save()` | Explicitly persist the session |
| `ctx.session.delete()` | Remove the session from the store |
***
## Multi-Step Workflow Example
A complete onboarding flow using sessions:
```typescript
builder.addCommand('onboard', {
name: 'onboard',
aliases: ['setup'],
description: 'Start onboarding',
help: 'Use /onboard to begin setup',
async handle(ctx) {
await ctx.session.update({ step: 'name', data: {} });
await ctx.reply('Step 1/3: What is your name?');
},
});
builder.addMiddleware(async (ctx, next) => {
const step = ctx.session.data.step;
if (!step || ctx.message.content?.type === 'command') {
return next();
}
const text = ctx.message.content?.type === 'text'
? ctx.message.content.content
: '';
switch (step) {
case 'name':
await ctx.session.update({
step: 'email',
data: { ...ctx.session.data, name: text },
});
await ctx.reply(`Nice to meet you, ${text}! Step 2/3: What is your email?`);
return; // Don't process commands during onboarding
case 'email':
await ctx.session.update({
step: 'confirm',
data: { ...ctx.session.data, email: text },
});
const data = ctx.session.data;
await ctx.reply(
`Step 3/3: Please confirm:\n\n` +
`Name: **${data.name}**\n` +
`Email: **${text}**\n\n` +
`Type /confirm to save or /cancel to discard.`,
);
return;
default:
return next();
}
});
builder.addCommand('confirm', {
name: 'confirm',
aliases: ['yes', 'ok'],
description: 'Confirm onboarding data',
help: 'Use /confirm to save your information',
async handle(ctx) {
if (ctx.session.data.step !== 'confirm') {
await ctx.reply('No pending confirmation. Use /onboard to start.');
return;
}
await saveUser(ctx.session.data);
await ctx.session.delete();
await ctx.reply('✅ Your profile has been saved! Welcome aboard.');
},
});
```
***
## Memory Session Store
The built-in `MemorySessionStore` is suitable for **development and single-instance deployments**:
```typescript
import { memoryStore } from '@igniter-js/bot';
const store = memoryStore({
cleanupIntervalMs: 60_000, // Clean expired sessions every 60 seconds (default)
});
builder.withSessionStore(store);
```
Features:
* In-memory `Map` storage — fast but ephemeral
* Automatic expired session cleanup
* `store.size()` to check active session count
* `store.destroy()` to stop cleanup interval
Memory sessions are lost on process restart. For production, use a persistent store.
***
## Custom Session Stores
Implement the `BotSessionStore` interface to use any storage backend:
```typescript
interface BotSessionStore {
get(userId: string, channelId: string): Promise
set(userId: string, channelId: string, session: BotSession): Promise
delete(userId: string, channelId: string): Promise
clear(userId: string): Promise // Delete all sessions for a user
}
```
### Redis Example
```typescript
import type { BotSessionStore, BotSession } from '@igniter-js/bot';
import { Redis } from 'ioredis';
class RedisSessionStore implements BotSessionStore {
constructor(private redis: Redis, private prefix = 'bot:session:') {}
private key(userId: string, channelId: string): string {
return `${this.prefix}${userId}:${channelId}`;
}
async get(userId: string, channelId: string): Promise {
const raw = await this.redis.get(this.key(userId, channelId));
if (!raw) return null;
const session = JSON.parse(raw);
// Check expiration
if (session.expiresAt && new Date(session.expiresAt) < new Date()) {
await this.redis.del(this.key(userId, channelId));
return null;
}
return {
...session,
createdAt: new Date(session.createdAt),
updatedAt: new Date(session.updatedAt),
expiresAt: session.expiresAt ? new Date(session.expiresAt) : undefined,
};
}
async set(userId: string, channelId: string, session: BotSession): Promise {
const key = this.key(userId, channelId);
const ttl = session.expiresAt
? Math.ceil((session.expiresAt.getTime() - Date.now()) / 1000)
: 86_400; // Default: 24 hours
await this.redis.setex(key, ttl, JSON.stringify({
...session,
updatedAt: new Date(),
}));
}
async delete(userId: string, channelId: string): Promise {
await this.redis.del(this.key(userId, channelId));
}
async clear(userId: string): Promise {
const keys = await this.redis.keys(`${this.prefix}${userId}:*`);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
}
```
Usage:
```typescript
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
const store = new RedisSessionStore(redis, 'mybot:session:');
builder.withSessionStore(store);
```
### Prisma Example
```typescript
import type { BotSessionStore, BotSession } from '@igniter-js/bot';
import { PrismaClient } from '@prisma/client';
class PrismaSessionStore implements BotSessionStore {
constructor(private prisma: PrismaClient) {}
async get(userId: string, channelId: string): Promise {
const record = await this.prisma.botSession.findUnique({
where: { userId_channelId: { userId, channelId } },
});
if (!record) return null;
if (record.expiresAt && record.expiresAt < new Date()) {
await this.prisma.botSession.delete({ where: { id: record.id } });
return null;
}
return {
userId: record.userId,
channelId: record.channelId,
data: record.data as Record,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
expiresAt: record.expiresAt ?? undefined,
};
}
async set(userId: string, channelId: string, session: BotSession): Promise {
await this.prisma.botSession.upsert({
where: { userId_channelId: { userId, channelId } },
create: {
userId,
channelId,
data: session.data,
expiresAt: session.expiresAt,
},
update: {
data: session.data,
expiresAt: session.expiresAt,
updatedAt: new Date(),
},
});
}
async delete(userId: string, channelId: string): Promise {
await this.prisma.botSession.deleteMany({ where: { userId, channelId } });
}
async clear(userId: string): Promise {
await this.prisma.botSession.deleteMany({ where: { userId } });
}
}
```
***
## Session Expiration
Set `expiresAt` on sessions for automatic cleanup:
```typescript
async handle(ctx) {
await ctx.session.update({
step: 'checkout',
cart: items,
});
// Session expires in 30 minutes (for abandoned carts)
ctx.session.expiresAt = new Date(Date.now() + 30 * 60 * 1000);
await ctx.session.save();
}
```
The memory store includes a background cleanup interval. Custom stores should handle expiration in their `get()` method by checking `expiresAt`.
***
## Best Practices
* ✅ **Use `ctx.session.update()`** instead of modifying `ctx.session.data` directly — it auto-saves
* ✅ **Set expiration** for time-sensitive flows (checkout, verification codes)
* ✅ **Use a persistent store** (Redis, Prisma) in production — memory store loses data on restart
* ✅ **Keep session data small** — avoid storing large objects; use IDs and fetch from database
* ✅ **Clear sessions** when a flow completes (`ctx.session.delete()`)
* ❌ **Don't store sensitive data** in sessions without encryption
* ❌ **Don't use sessions as a database** — they're for temporary conversational state
* ❌ **Don't share session stores across unrelated bots** — use a prefix or separate Redis DB
***
## Next Steps
---
# Troubleshooting
> Diagnose and fix common issues with @igniter-js/bot. Error codes, debugging techniques, webhook problems, and platform-specific troubleshooting.
URL: https://igniterjs.com/docs/bots/troubleshooting
## Troubleshooting
Common issues and solutions when building bots with `@igniter-js/bot`.
***
## Build Errors
### "At least one adapter is required"
**Symptom:** `.build()` throws: `At least one adapter is required.`
**Cause:** No adapters were added before calling `.build()`.
**Fix:**
```typescript
// Before
const bot = IgniterBot.create().build(); // ❌
// After
const bot = IgniterBot.create()
.addAdapter('telegram', telegram({ token: '...' }))
.build(); // ✅
```
***
### "Bot handle is required"
**Symptom:** `.build()` throws: `Bot handle is required.`
**Cause:** Neither a global handle (`withHandle()`) nor a per-adapter handle was configured.
**Fix:**
```typescript
// Option 1: Global handle
IgniterBot.create().withHandle('@mybot')
// Option 2: Per-adapter handles
IgniterBot.create().addAdapter('telegram', telegram({
token: '...',
handle: '@mybot',
}))
```
***
## Runtime Errors
### PROVIDER\_NOT\_FOUND
**Symptom:** `BotError: Provider 'xxx' not found`
**Cause:** An incoming request targets an adapter that wasn't registered, or the route's dynamic segment doesn't match.
**Fix:**
1. Check that the adapter key in your route matches the key used in `addAdapter()`:
```typescript
// bot setup
.addAdapter('telegram', tg)
// route — must match
app.all('/api/bot/telegram', ...) // 'telegram' must match
```
2. If using `nextRouteHandlerAdapter`, ensure the `[adapter]` segment matches:
```
/api/bot/telegram/assistant_bot → adapter = 'telegram'
```
***
### CLIENT\_NOT\_PROVIDED
**Symptom:** `BotError: Telegram/WhatsApp/Discord client not provided`
**Cause:** The adapter's internal HTTP client initialization failed. This usually happens when configuration values are empty or invalid.
**Fix:**
1. Check that environment variables are set and accessible at runtime
2. Verify adapter configuration values are not empty strings:
```typescript
// Debug: log config (never log tokens in production!)
const tg = telegram({ token: process.env.TELEGRAM_TOKEN! });
console.log('Token exists:', !!process.env.TELEGRAM_TOKEN);
```
***
### CONTENT\_TYPE\_NOT\_SUPPORTED
**Symptom:** `BotError: Poll content not supported by adapter 'whatsapp'`
**Cause:** Attempting to send a content type that the adapter's capabilities don't include.
**Fix:**
1. Check the [adapter capability table](/docs/bots/adapters#adapter-capability-comparison)
2. Use conditional logic:
```typescript
async handle(ctx) {
if (ctx.provider === 'whatsapp') {
await ctx.reply('Polls are not available on WhatsApp. Reply with your choice: A, B, or C.');
} else {
await ctx.bot.send({
provider: ctx.provider,
channel: ctx.channel.id,
content: { type: 'poll', question: '...', options: ['A', 'B', 'C'] },
});
}
}
```
***
### INVALID\_COMMAND\_PARAMETERS
**Symptom:** Command handler receives unexpected arguments or resolves with an error.
**Cause:** The `args` Zod schema failed validation against user input.
**Fix:**
1. Check the Zod schema is correct:
```typescript
args: z.object({
query: z.string(), // Remember: strings without .optional() are required
})
```
2. Test your schema manually:
```typescript
import { z } from 'zod';
const schema = z.object({ query: z.string().min(2) });
schema.parse({ query: 'hi' }); // ✅ passes
schema.parse({}); // ❌ throws ZodError
```
3. Provide clear error messages in `help`:
```typescript
help: 'Use /search <query> [category: books|movies|music]',
```
***
## Webhook Issues
### Telegram: Webhook Not Receiving Updates
**Symptoms:** Bot is running but doesn't respond to messages.
**Checklist:**
1. **Verify webhook URL is publicly accessible:**
```bash
curl https://your-domain.com/api/bot/telegram/assistant_bot
```
2. **Check webhook status via Telegram API:**
```bash
curl "https://api.telegram.org/bot/getWebhookInfo"
```
Look for `"ok": true` and `"url": "https://..."` and no `last_error_message`.
3. **Ensure HTTPS certificate is valid** — Telegram requires HTTPS.
4. **Delete and re-set the webhook manually:**
```bash
curl "https://api.telegram.org/bot/deleteWebhook"
curl "https://api.telegram.org/bot/setWebhook?url=https://..."
```
5. **Check the `dropPendingUpdates` setting** — if `false`, old updates may be processed first, making it seem like new messages don't work.
***
### Discord: 401 Unauthorized on Interactions
**Symptoms:** Discord returns "401 Unauthorized" for bot interactions.
**Checklist:**
1. **Verify `publicKey` is correct** — copy it from the Discord Developer Portal (General Information).
2. **Enable `X-Signature-Ed25519` header forwarding** in your proxy/reverse proxy configuration.
3. **Check that the raw request body is being forwarded** — some middleware (body parsers) may modify the body before the adapter reads it.
***
### WhatsApp: Messages Not Being Delivered
**Symptoms:** Bot receives webhooks but outbound messages don't arrive.
**Checklist:**
1. **Verify the phone number ID format:**
```typescript
whatsapp({ phone: '1234567890' }) // Just the number, no + or spaces
```
2. **Check the message template status** — some message types require pre-approved templates.
3. **Verify the recipient has opted in** — WhatsApp requires users to message first.
4. **Check rate limits** — WhatsApp Cloud API has strict rate limits (80 messages/second for business accounts).
***
## Session Issues
### Sessions Not Persisting
**Symptoms:** `ctx.session.data` is always empty or reset to defaults.
**Checklist:**
1. **Use `ctx.session.update()` not direct mutation:**
```typescript
// ✅ Works
await ctx.session.update({ step: 'checkout' });
// ❌ May not persist
ctx.session.data.step = 'checkout';
```
2. **Check that the session store is configured:**
```typescript
// Must be set before .build()
builder.withSessionStore(memoryStore())
```
3. **If using memory store in production:** It doesn't persist across restarts. Use Redis or Prisma.
***
### Redis Sessions Timeout
**Symptoms:** Sessions expire too quickly in Redis.
**Checklist:**
1. **Check the TTL in your Redis store implementation** — the default is 24 hours if `expiresAt` is not set.
2. **Verify Redis connection is stable** — check for connection drops or timeouts.
3. **Check for Redis maxmemory eviction** — if `maxmemory-policy` is set to `allkeys-lru`, sessions may be evicted.
***
## Rate Limiting Issues
### Users Being Rate Limited Too Quickly
**Symptoms:** Legitimate users hit rate limits within seconds.
**Checklist:**
1. **Check your `windowMs` and `maxRequests`:**
```typescript
rateLimitMiddleware({
maxRequests: 10,
windowMs: 60_000, // 1 minute — not 1 second!
})
```
2. **Use the correct key generator:**
```typescript
// Per-user (default)
keyGenerator: (ctx) => `${ctx.provider}:${ctx.message.author.id}`
// Per-user-per-command (more granular)
keyGenerator: (ctx) => {
const cmd = ctx.message.content?.type === 'command' ? ctx.message.content.command : 'message';
return `${ctx.provider}:${ctx.message.author.id}:${cmd}`;
}
```
3. **Memory store is per-process** — if you have multiple instances, each has its own counter. Use Redis for shared state.
***
## TypeScript Issues
### "Property does not exist on type BotContext"
**Symptom:** TypeScript error when accessing enriched context properties.
**Fix:** Middleware enrichment flows through the builder's generic types:
```typescript
// The builder tracks middleware return types
const bot = IgniterBot.create()
.addMiddleware(async (ctx, next) => {
await next();
return { user: { id: '123' } }; // returned
})
.addCommand('profile', {
async handle(ctx) {
ctx.user.id; // ✅ TypeScript knows about this
},
})
.build();
```
If you're accessing enriched context in a separate file, cast as needed:
```typescript
type EnrichedContext = BotContext & { user: { id: string } };
async handle(ctx) {
const enriched = ctx as EnrichedContext;
console.log(enriched.user.id);
}
```
***
## Debugging Tips
### Enable Verbose Logging
```typescript
import { loggingPresets } from '@igniter-js/bot';
builder.addMiddleware(loggingPresets.debug());
```
This logs every message, command, error, and metric with full detail.
### Check Adapter Status
```typescript
console.log('Registered adapters:', Object.keys(bot.getAdapters()));
console.log('Telegram adapter:', bot.getAdapter('telegram'));
```
### Test Webhook Manually
```bash
# Simulate a Telegram message (use actual token and chat ID)
curl -X POST "https://your-domain.com/api/bot/telegram/assistant_bot" \
-H "Content-Type: application/json" \
-d '{"update_id": 1, "message": {"message_id": 1, "from": {"id": 123, "is_bot": false, "first_name": "Test"}, "chat": {"id": 123, "type": "private"}, "date": 1234567890, "text": "/start"}}'
```
### Inspect Session Data
```typescript
builder.addMiddleware(async (ctx, next) => {
console.log('Session data:', ctx.session.data);
await next();
});
```
***
## Still Stuck?
1. **Check the [API Reference](/docs/bots/api-reference)** for exact method signatures
2. **Review [Best Practices](/docs/bots/best-practices)** for common patterns
3. **Read the adapter source code** at `packages/bot/src/adapters/` — it's well-documented
4. **Enable debug logging** with `loggingPresets.debug()` for full request tracing
***
## Next Steps
---
# API Reference
> Complete API reference for @igniter-js/caller — all classes, methods, types, and configuration options.
URL: https://igniterjs.com/docs/caller/api-reference
## IgniterCaller
The main entrypoint for creating API client instances.
```typescript
import { IgniterCaller } from '@igniter-js/caller';
```
### `IgniterCaller.create()`
Creates a new `IgniterCallerBuilder` instance with an empty configuration.
```typescript
const api = IgniterCaller.create()
.withBaseUrl('https://api.example.com')
.build();
```
***
## IgniterCallerBuilder
Immutable builder for configuring the API client. Every `with*` method returns a new builder instance.
IgniterCallerBuilder<{}>',
description: 'Static factory. Creates a new builder with empty state.',
},
withBaseUrl: {
type: '(baseURL: string) => this',
description: 'Sets the base URL prefixed to all requests.',
},
withHeaders: {
type: '(headers: Record) => this',
description: 'Sets default headers merged into every request.',
},
withCookies: {
type: '(cookies: Record) => this',
description: 'Sets default cookies (serialized into the Cookie header).',
},
withLogger: {
type: '(logger: IgniterLogger) => this',
description: 'Attaches a logger instance for request lifecycle logging.',
},
withTelemetry: {
type: '(telemetry: IgniterTelemetryManager) => this',
description: 'Attaches a telemetry manager for observability events.',
},
withSchemas: {
type: '(schemas: SchemaMap | SchemaBuildResult, validation?: SchemaValidationOptions) => this',
description: 'Configures schema-based type inference and optional runtime validation. Accepts a raw schema map or a build result from IgniterCallerSchema.',
},
withRequestInterceptor: {
type: '(interceptor: (config: RequestOptions) => RequestOptions | Promise) => this',
description: 'Adds a request interceptor that can modify request options before execution.',
},
withResponseInterceptor: {
type: '(interceptor: (response: ApiResponse) => ApiResponse | Promise) => this',
description: 'Adds a response interceptor that can transform responses after execution.',
},
withStore: {
type: '(store: StoreAdapter, options?: StoreOptions) => this',
description: 'Configures a persistent store adapter for caching (e.g., Redis).',
},
withMock: {
type: '(config: { enabled: boolean; delay?: number; mock: MockManager }) => this',
description: 'Enables mock mode. Requests matching mock handlers skip real network calls.',
},
build: {
type: '() => IgniterCallerManager',
description: 'Finalizes configuration. Logs initialization details and returns the manager instance.',
},
}}
/>
***
## IgniterCallerManager
The runtime engine returned by `.build()`. Used to create and execute HTTP requests.
### Instance Methods
All HTTP method helpers accept an optional URL. When schemas are configured, providing a URL enables full type inference.
RequestBuilder',
description: 'Creates a GET request builder. Supports path params via :param syntax.',
},
post: {
type: '(url?: string) => RequestBuilder',
description: 'Creates a POST request builder.',
},
put: {
type: '(url?: string) => RequestBuilder',
description: 'Creates a PUT request builder (idempotent, replaces entire resource).',
},
patch: {
type: '(url?: string) => RequestBuilder',
description: 'Creates a PATCH request builder (partial update).',
},
delete: {
type: '(url?: string) => RequestBuilder',
description: 'Creates a DELETE request builder.',
},
head: {
type: '(url?: string) => RequestBuilder',
description: 'Creates a HEAD request builder (headers only, no body).',
},
request: {
type: '(options: DirectRequestOptions) => Promise>',
description: 'Executes a request directly using an options object. Convenience method for dynamic or programmatic requests.',
},
}}
/>
### Static Methods
void) => () => void',
description: 'Registers a global event listener for matching responses. Returns an unsubscribe function. Pattern can be a string or RegExp.',
},
batch: {
type: '(requests: Promise[]) => Promise',
description: 'Executes multiple requests in parallel using Promise.all semantics. Results preserve the input order.',
},
}}
/>
***
## IgniterCallerRequestBuilder
Fluent per-request API. Returned by `api.get()`, `api.post()`, etc. All methods return `this` for chaining.
### Request Configuration
this',
description: 'Sets the request URL or path (relative to base URL).',
},
body: {
type: '(data: unknown) => this',
description: 'Sets the request body. Objects/arrays are auto-serialized to JSON. For GET/HEAD, body is converted to query params.',
},
params: {
type: '(params: Record) => this',
description: 'Sets URL path parameters (replacing :param segments) and query string parameters. Extra params become query string.',
},
headers: {
type: '(headers: Record) => this',
description: 'Merges additional headers. Overrides defaults with the same name.',
},
timeout: {
type: '(ms: number) => this',
description: 'Sets request timeout in milliseconds. Default: 30000 (30s). Triggers AbortError on expiry.',
},
}}
/>
### Resilience
this',
description: 'Configures automatic retry on failure. maxAttempts is the number of retry attempts (total attempts = 1 + maxAttempts). Default backoff: linear. Default retryOnStatus: [408, 429, 500, 502, 503, 504].',
},
fallback: {
type: '(fn: () => T) => this',
description: 'Provides a fallback factory function. Called if all retry attempts fail. The result is returned as data, with no error.',
},
cache: {
type: '(strategy: RequestCache, key?: string) => this',
description: 'Sets the fetch RequestCache strategy (e.g., "default", "no-store", "reload", "force-cache"). Optional key for manual cache management.',
},
stale: {
type: '(milliseconds: number) => this',
description: 'Enables response caching for the specified duration. Subsequent requests within the stale time return cached data instead of making a network call.',
},
}}
/>
### Response Typing
(schema?: ZodSchema | StandardSchemaV1) => RequestBuilder',
description: 'Sets the expected response type for TypeScript inference. If a Zod/StandardSchema is passed, the response is validated at runtime (only for JSON/XML/CSV content types). If no schema is passed, it provides type inference only.',
},
}}
/>
### Execution
Promise>',
description: 'Executes the request. Runs through the full pipeline: cache check → interceptors → validation → fetch (with retry) → parsing → validation → interceptors → cache store → fallback.',
},
}}
/>
***
## IgniterCallerSchema
Path-first schema builder for defining type-safe API contracts with Zod (or any StandardSchemaV1 library).
IgniterCallerSchema<{}, {}>',
description: 'Creates a new empty schema builder.',
},
schema: {
type: '(key: string, schema: StandardSchemaV1, options?: { internal?: boolean }) => this',
description: 'Registers a reusable schema in the registry. Can be referenced by key in path definitions via path.ref(key).',
},
path: {
type: '(path: string, builder: (pathBuilder: SchemaPathBuilder) => SchemaPathBuilder) => this',
description: 'Defines a path with its HTTP methods. The builder callback receives a SchemaPathBuilder to define GET, POST, etc. Path must start with /.',
},
build: {
type: '() => SchemaBuildResult',
description: 'Builds the schema map. Returns an object with $Infer and get helpers for type extraction.',
},
}}
/>
### SchemaPathBuilder
Defines HTTP methods for a single path:
| Method | Signature | Description |
| ---------------- | ------------------------------------------------------------------------ | ------------------------------------------------ |
| `get(config)` | `(config: { request?, responses }) => this` | Defines GET endpoint |
| `post(config)` | `(config: { request?, responses }) => this` | Defines POST endpoint |
| `put(config)` | `(config: { request?, responses }) => this` | Defines PUT endpoint |
| `patch(config)` | `(config: { request?, responses }) => this` | Defines PATCH endpoint |
| `delete(config)` | `(config: { request?, responses }) => this` | Defines DELETE endpoint |
| `head(config)` | `(config: { request?, responses }) => this` | Defines HEAD endpoint |
| `ref(key)` | `(key: string) => { schema, array(), nullable(), optional(), record() }` | References a registered schema with Zod wrappers |
| `build()` | `() => TMethods` | Returns the accumulated method map |
***
## IgniterCallerMock
Builder for creating typed mock registries for testing.
IgniterCallerMockBuilder<{}>',
description: 'Creates a new mock builder.',
},
withSchemas: {
type: '(schemas: SchemaMap | SchemaBuildResult) => this',
description: 'Associates schemas for typed mock definitions. Mocks validate against schema types.',
},
mock: {
type: '(path: string, handlers: MockPathDefinition) => this',
description: 'Registers mock handlers for a path. Handlers can be static responses or dynamic functions receiving request context.',
},
build: {
type: '() => IgniterCallerMockManager',
description: 'Builds the mock manager instance.',
},
}}
/>
### Mock Handler Shapes
Static response:
```typescript
{ response: data; status?: number; headers?: Record; delayMs?: number }
```
Dynamic handler:
```typescript
(request: { method; path; url; headers; query; params; body?; ... }) => MockResponse
```
***
## MockCallerStoreAdapter
In-memory store adapter for testing. Implements the `IgniterCallerStoreAdapter` interface.
```typescript
import { MockCallerStoreAdapter } from '@igniter-js/caller/adapters';
```
MockCallerStoreAdapter',
description: 'Creates a new mock adapter with call tracking.',
},
client: {
type: 'Map',
description: 'Underlying in-memory store.',
},
calls: {
type: '{ get: number; set: number; delete: number; has: number }',
description: 'Call counters for assertions.',
},
history: {
type: '{ get: string[]; set: Array<{ key, value, options? }>; delete: string[]; has: string[] }',
description: 'Operation history for detailed assertions.',
},
clear: {
type: '() => void',
description: 'Clears all tracked state (store, calls, history).',
},
}}
/>
***
## Types
### `IgniterCallerApiResponse`
The standardized response envelope returned by all requests.
### `IgniterCallerError`
Custom error class extending `IgniterError` for all Caller failures.
boolean',
description: 'Static type guard. Returns true if the value is an IgniterCallerError.',
},
}}
/>
### Error Codes
| Code | Description |
| ------------------------------------------- | --------------------------------------- |
| `IGNITER_CALLER_HTTP_ERROR` | HTTP response status ≥ 400 |
| `IGNITER_CALLER_MOCK_HTTP_ERROR` | Mock handler returned an error response |
| `IGNITER_CALLER_TIMEOUT` | Request exceeded the configured timeout |
| `IGNITER_CALLER_REQUEST_VALIDATION_FAILED` | Request body failed schema validation |
| `IGNITER_CALLER_RESPONSE_VALIDATION_FAILED` | Response body failed schema validation |
| `IGNITER_CALLER_SCHEMA_DUPLICATE` | Duplicate schema path or method |
| `IGNITER_CALLER_SCHEMA_INVALID` | Invalid schema configuration |
| `IGNITER_CALLER_UNKNOWN_ERROR` | Unexpected catch-all error |
### `IgniterCallerStoreAdapter`
Interface for implementing persistent cache stores.
Promise',
description: 'Retrieves a cached value by key.',
},
set: {
type: '(key: string, value: any, options?: { ttl?: number }) => Promise',
description: 'Stores a cached value with optional TTL.',
},
delete: {
type: '(key: string) => Promise',
description: 'Removes a cached value.',
},
has: {
type: '(key: string) => Promise',
description: 'Checks if a key exists in the store.',
},
}}
/>
### `IgniterCallerRetryOptions`
### `IgniterCallerStoreOptions`
***
## Subpath Exports
| Import Path | Purpose |
| ------------------------------ | ------------------------------------------------------------------------- |
| `@igniter-js/caller` | Core client: IgniterCaller, IgniterCallerSchema, IgniterCallerMock, types |
| `@igniter-js/caller/client` | React integration: IgniterCallerProvider, useIgniterCaller, hooks |
| `@igniter-js/caller/telemetry` | Telemetry events: IgniterCallerTelemetryEvents |
| `@igniter-js/caller/adapters` | Store adapters: MockCallerStoreAdapter |
---
# CLI Integration
> Automate your API client setup with the Igniter CLI.
URL: https://igniterjs.com/docs/caller/cli
The Igniter CLI allows you to generate a fully typed Caller and its corresponding Zod schemas directly from an OpenAPI 3 specification. This is the fastest way to integrate third-party APIs or sync with your own backend.
## Generation Command
Use the `generate caller` command:
```bash
npx @igniter-js/cli generate caller --name facebook --url https://api.facebook.com/openapi.json
```
```bash
pnpm igniter generate caller --name facebook --url https://api.facebook.com/openapi.json
```
```bash
yarn igniter generate caller --name facebook --url https://api.facebook.com/openapi.json
```
```bash
bunx @igniter-js/cli generate caller --name facebook --url https://api.facebook.com/openapi.json
```
### Command Options
| Option | Description |
| ---------- | -------------------------------------------------------------- |
| `--name` | Prefix for the generated variables and file naming. |
| `--url` | Remote URL of the OpenAPI JSON/YAML. |
| `--path` | Local file path to the OpenAPI spec. |
| `--output` | Override output directory (default: `src/callers/`). |
## Generated Assets
The command generates two main files in your project:
1. **`schema.ts`**: Contains all Zod schemas derived from `components/schemas` and a path-first `IgniterCallerSchema` builder.
2. **`index.ts`**: Exports a pre-configured `IgniterCaller` instance.
### Using the Generated Caller
```ts
import { facebookCaller } from '@/callers/facebook'
import type { FacebookCallerSchemas } from '@/callers/facebook/schema'
// Making a request (URL is typed!)
const result = await facebookCaller.get('/me').execute()
// Type inference for responses
type MeResponse = ReturnType<
typeof FacebookCallerSchemas.$Infer.Response<'/me', 'GET', 200>
>
```
## Why use CLI Generation?
* **Zero Manual Typing**: Keep your TS types perfectly in sync with the API documentation.
* **Runtime Safety**: Automatic validation ensures the data you receive matches the code you wrote.
* **Speed**: Go from an URL to a production-ready client in seconds.
***
## Manual Schemas
If you don't have an OpenAPI spec, you can still define [Manual Schemas](/docs/caller/schemas) using the `IgniterCallerSchema` builder.
---
# Comparison
> How Caller compares to Axios, ky, ofetch, and native fetch. Understand the trade-offs and choose the right HTTP client for your project.
URL: https://igniterjs.com/docs/caller/comparison
## Overview
The JavaScript ecosystem has many excellent HTTP clients. This page provides an honest comparison between Caller and popular alternatives, helping you understand when Caller is the right choice and when you might prefer something else.
We believe in picking the right tool for the job. Caller excels at type-safe, schema-validated API consumption in TypeScript projects—but it's not always the best choice for every scenario.
***
## Quick Comparison
| Feature | Caller | Axios | ky | ofetch | fetch |
| --------------------- | ---------------- | --------- | ---------- | ---------- | ------ |
| **Type Safety** | ✅ Full inference | ⚠️ Manual | ⚠️ Manual | ⚠️ Manual | ❌ None |
| **Schema Validation** | ✅ Built-in | ❌ | ❌ | ❌ | ❌ |
| **Fluent API** | ✅ | ❌ | ✅ | ⚠️ Partial | ❌ |
| **Retries** | ✅ Built-in | ❌ Plugin | ✅ Built-in | ✅ Built-in | ❌ |
| **Caching** | ✅ Built-in | ❌ | ❌ | ❌ | ❌ |
| **React Hooks** | ✅ Built-in | ❌ | ❌ | ❌ | ❌ |
| **Interceptors** | ✅ | ✅ | ✅ | ✅ | ❌ |
| **Bundle Size** | \~8kb | \~13kb | \~4kb | \~5kb | 0kb |
| **Node.js Support** | ✅ | ✅ | ✅ | ✅ | ✅ 18+ |
| **Browser Support** | Modern | All | Modern | Modern | Modern |
***
## Detailed Comparisons
**Axios** is the most popular HTTP client in the JavaScript ecosystem with excellent browser compatibility and a mature ecosystem.
### When to choose Axios
* You need to support older browsers (IE11)
* Your team is already familiar with Axios
* You need upload progress events
* You're working in a pure JavaScript project
### When to choose Caller
* You want automatic type inference from schemas
* You need built-in caching and retries
* You're building with React and want hooks
* You value compile-time API contract validation
### Code Comparison
```typescript title="Axios"
import axios from 'axios';
interface User {
id: string;
name: string;
}
// Manual type annotation required
const response = await axios.get('/users/123');
const user = response.data; // Type: User (but not validated)
```
```typescript title="Caller"
import { api } from './lib/api';
// Type inferred from schema, validated at runtime
const { data } = await api.get('/users/:id')
.params({ id: '123' })
.execute();
// data is typed AND validated
```
**Key Difference:** Axios requires you to manually define and maintain TypeScript interfaces. Caller infers types from your schema definitions and validates responses at runtime.
**ky** is a lightweight HTTP client with a clean API, built by the creator of got. It's essentially a modern wrapper around fetch.
### When to choose ky
* You want the smallest possible bundle size
* You prefer a minimal API surface
* You don't need schema validation
* You're doing simple HTTP requests without complex flows
### When to choose Caller
* You want type-safe API contracts
* You need schema validation out of the box
* You want React integration
* You're building an Igniter.js application
### Code Comparison
```typescript title="ky"
import ky from 'ky';
const user = await ky.get('https://api.example.com/users/123').json();
// Type: unknown - requires manual casting
```
```typescript title="Caller"
const { data } = await api.get('/users/:id')
.params({ id: '123' })
.execute();
// Type: { id: string; name: string } - from schema
```
**Key Difference:** ky focuses on being a thin, elegant wrapper around fetch. Caller adds a comprehensive type system and validation layer on top.
**ofetch** (from UnJS/Nuxt team) is a modern fetch wrapper with automatic JSON parsing, retries, and good defaults.
### When to choose ofetch
* You're building a Nuxt application
* You want a simple, batteries-included fetch wrapper
* You don't need schema-level type safety
* You want universal (Node/Browser) support out of the box
### When to choose Caller
* You want compile-time type checking of API contracts
* You need built-in caching with invalidation
* You're building with React
* You want to generate clients from OpenAPI specs
### Code Comparison
```typescript title="ofetch"
import { ofetch } from 'ofetch';
const user = await ofetch('/users/123', {
retry: 3,
retryDelay: 1000,
});
// Type: unknown
```
```typescript title="Caller"
const { data } = await api.get('/users/:id')
.params({ id: '123' })
.retry({ attempts: 3, baseDelay: 1000 })
.execute();
// Type: User (from schema)
```
**Key Difference:** ofetch is excellent for quick, type-agnostic requests. Caller shines when you want your entire API surface typed and validated.
**fetch** is the standard API built into modern browsers and Node.js 18+. No dependencies needed.
### When to choose fetch
* You have very simple needs (one or two API calls)
* You want zero dependencies
* You're writing a library that can't have external deps
* You're comfortable handling all edge cases manually
### When to choose Caller
* You're building an application with many API calls
* You want consistent error handling
* You need retries, caching, or timeouts
* You value developer experience and type safety
### Code Comparison
```typescript title="Native fetch"
interface User {
id: string;
name: string;
}
const response = await fetch('https://api.example.com/users/123');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json() as User;
// Type: User (but cast, not validated)
```
```typescript title="Caller"
const { data, error } = await api.get('/users/:id')
.params({ id: '123' })
.execute();
if (error) {
// Structured error with code, message, status
console.error(error.code, error.message);
}
// data is typed and validated
```
**Key Difference:** fetch requires significant boilerplate for production use (error handling, retries, timeouts, type safety). Caller provides all of this out of the box.
***
## Feature Deep Dive
### Type Safety
The biggest differentiator between Caller and alternatives is the type system. Most HTTP clients require you to manually define interfaces and hope they match your API:
```typescript title="Traditional Approach"
// You define this manually
interface User {
id: string;
name: string;
email: string;
}
// API changes "email" to "emailAddress"
// TypeScript doesn't know - runtime error!
const user = await axios.get('/users/123');
console.log(user.data.email); // undefined at runtime
```
With Caller, your schema is the source of truth:
```typescript title="Caller Approach"
// Schema is the source of truth
const schemas = IgniterCallerSchema.create()
.path('/users/:id', (path) =>
path.get({
responses: {
200: z.object({
id: z.string(),
name: z.string(),
emailAddress: z.string(), // API changed this
}),
},
})
)
.build();
const { data } = await api.get('/users/:id').execute();
console.log(data.emailAddress); // TypeScript knows the correct field
```
### React Integration
Caller provides first-class React hooks, while other libraries require external wrappers like TanStack Query:
```tsx title="Caller React Hooks"
import { useCaller } from '@igniter-js/caller/client';
function UserProfile({ id }: { id: string }) {
const api = useCaller('main');
const { data, isLoading } = api.get('/users/:id')
.params({ id })
.useQuery({ staleTime: 10_000 });
return {data?.name}
;
}
```
```tsx title="Axios + TanStack Query"
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
function UserProfile({ id }: { id: string }) {
const { data, isLoading } = useQuery({
queryKey: ['user', id],
queryFn: () => axios.get(`/users/${id}`).then(r => r.data),
});
return {data?.name}
;
}
```
***
## Migration Guides
***
## Summary
| Scenario | Recommendation |
| ------------------------------------------- | ------------------- |
| New TypeScript project with APIs | **Caller** |
| Existing Axios codebase, no time to migrate | **Axios** |
| Ultra-minimal bundle, simple needs | **ky** or **fetch** |
| Nuxt.js application | **ofetch** |
| IE11 support required | **Axios** |
| Type-safe, validated API layer | **Caller** |
Choose Caller when type safety, developer experience, and production-readiness matter. Choose alternatives when simplicity or specific compatibility requirements take priority.
***
## Next Steps
---
# Introduction
> A type-safe HTTP client for Igniter.js with automatic schema validation, caching, retries, and React integration. Built on native fetch.
URL: https://igniterjs.com/docs/caller
## The Missing Piece
Every modern application needs to talk to APIs. Whether you're fetching user data, sending form submissions, or syncing with third-party services, HTTP requests are everywhere. But making HTTP requests in TypeScript has always been frustrating:
* **No type safety** — responses are `any` by default, forcing you to cast or define interfaces manually
* **Boilerplate everywhere** — error handling, retries, and caching require custom code for each request
* **Client/server disconnect** — types defined on the server don't automatically flow to the client
* **Testing is painful** — mocking fetch or axios requires complex setup
**Caller** solves all of these problems with a simple, fluent API.
```typescript
// Define your API once
const api = IgniterCaller.create()
.withBaseUrl('https://api.example.com')
.withSchemas(myApiSchemas)
.build();
// Every request is fully typed
const { data, error } = await api.get('/users/:id')
.params({ id: '123' })
.execute();
// data is automatically typed as User — no manual interfaces!
console.log(data?.name);
```
Caller works out of the box with no setup. `IgniterCaller.create().build()` is all you need to start making requests.
***
## Why Caller?
Define your API schema once and get automatic TypeScript inference everywhere. When your server response changes, TypeScript catches breaking changes instantly.
```typescript
const schemas = IgniterCallerSchema.create()
.path('/users/:id', (path) =>
path.get({
responses: {
200: z.object({ id: z.string(), name: z.string() }),
404: z.object({ message: z.string() }),
},
})
)
.build();
// TypeScript knows exact response types
const { data } = await api.get('/users/:id').params({ id: '1' }).execute();
// ^? { id: string; name: string } | undefined
```
No more `as User` casts or manually syncing interfaces between client and server.
Chain methods naturally to configure your requests. Every method returns the builder, enabling powerful composition.
```typescript
const result = await api.post('/users')
.body({ name: 'John', email: 'john@example.com' })
.headers({ 'X-Request-ID': crypto.randomUUID() })
.timeout(5000)
.retry(3, { backoff: 'exponential' })
.execute();
```
Production applications need retry logic, timeouts, and fallbacks. Caller provides these out of the box.
```typescript
const { data } = await api.get('/external-service')
.retry(3, { backoff: 'exponential', baseDelay: 500 })
.timeout(10_000)
.fallback(() => ({ cached: true, stale: true }))
.execute();
```
No more writing custom retry wrappers or handling transient failures manually.
Cache responses automatically with configurable stale times. Invalidate programmatically when data changes.
```typescript
// Cache for 5 minutes
const users = await api.get('/users')
.stale(300_000)
.execute();
// Make a mutation...
await api.post('/users').body(newUser).execute();
// Next GET /users call will fetch fresh data after stale time expires
```
First-class React hooks for data fetching with automatic loading states, refetching, and cache invalidation.
```tsx
function UserProfile({ id }: { id: string }) {
const main = useCaller('main');
const { data, isLoading, error } = main.get('/users/:id')
.params({ id })
.useQuery({ staleTime: 30_000 });
if (isLoading) return ;
if (error) return ;
return {data?.name} ;
}
```
***
## Architecture
Caller follows a builder pattern similar to other Igniter.js packages:
```
IgniterCaller.create()
→ .withBaseUrl() / .withSchemas() / .withInterceptors()
→ .build()
→ IgniterCallerManager
→ .get() / .post() / .put() / .patch() / .delete()
→ IgniterCallerRequestBuilder
→ .params() / .body() / .retry() / .stale()
→ .execute()
→ Type-Safe Response
```
Key components:
| Component | Purpose |
| ------------------------------- | -------------------------------------------- |
| **IgniterCaller** | Entry point — creates the builder |
| **IgniterCallerBuilder** | Immutable configuration chain |
| **IgniterCallerManager** | Runtime engine that executes requests |
| **IgniterCallerRequestBuilder** | Fluent per-request API |
| **IgniterCallerSchema** | Type-safe schema definitions with validation |
| **IgniterCallerMock** | Typed mock registry for testing |
***
## Quick Start
### Install the Package
```bash
npm install @igniter-js/caller
```
```bash
pnpm add @igniter-js/caller
```
```bash
yarn add @igniter-js/caller
```
```bash
bun add @igniter-js/caller
```
### Create Your Client
```typescript title="lib/api.ts"
import { IgniterCaller } from '@igniter-js/caller';
export const api = IgniterCaller.create()
.withBaseUrl(process.env.API_URL || 'http://localhost:3000')
.build();
```
### Make Your First Request
```typescript
import { api } from './lib/api';
async function fetchUsers() {
const { data, error } = await api.get('/users').execute();
if (error) {
console.error('Failed to fetch users:', error.message);
return [];
}
return data;
}
```
That's it! You now have a working HTTP client. Continue to [Quick Start](/docs/caller/quick-start) for a guided tutorial, or jump to [Core Concepts](/docs/caller/builder) to learn about the builder pattern.
***
## When to Use Caller
Caller is ideal for:
* ✅ **Igniter.js applications** — seamless integration with the framework
* ✅ **Type-safe API clients** — compile-time validation of your API contracts
* ✅ **React/Next.js projects** — built-in hooks and SSR support
* ✅ **Microservices communication** — retries, caching, and observability
* ✅ **OpenAPI consumers** — generate clients from specs with the CLI
Consider alternatives if:
* ❌ You need browser support for IE11 (Caller uses native fetch)
* ❌ You're building a simple script with one-off requests (use `fetch` directly)
* ❌ You need GraphQL support (consider urql or Apollo)
***
## Next Steps
---
# Installation
> Complete guide to installing and configuring @igniter-js/caller. Covers all package managers, TypeScript setup, optional dependencies, and environment configuration.
URL: https://igniterjs.com/docs/caller/installation
## Prerequisites
Before installing Caller, ensure your environment meets these requirements:
| Requirement | Minimum | Notes |
| -------------- | ------- | -------------------------------- |
| **Node.js** | ≥ 18.0 | Native `fetch` support required |
| **Bun** | ≥ 1.0 | Alternative runtime |
| **Deno** | ≥ 1.40 | Alternative runtime |
| **TypeScript** | ≥ 5.0 | Recommended for full type safety |
Caller works in **any environment** where `fetch` is available: Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge, and modern browsers.
***
## Install the Package
```bash
npm install @igniter-js/caller
```
```bash
pnpm add @igniter-js/caller
```
```bash
yarn add @igniter-js/caller
```
```bash
bun add @igniter-js/caller
```
`@igniter-js/common` is installed automatically as a peer dependency.
***
## Optional Dependencies
Caller has a modular architecture — install only what you need:
### Schema Validation
To use schema-based type inference and runtime validation, install a StandardSchemaV1-compatible library:
```bash
npm install zod
```
```bash
pnpm add zod
```
```bash
yarn add zod
```
```bash
bun add zod
```
While the examples use Zod, Caller supports **any** library that implements the [StandardSchemaV1](https://standardschema.dev) interface — including Valibot, ArkType, and custom schemas.
### Telemetry
For observability features (request tracing, metrics, event emission):
```bash
npm install @igniter-js/telemetry
```
```bash
pnpm add @igniter-js/telemetry
```
```bash
yarn add @igniter-js/telemetry
```
```bash
bun add @igniter-js/telemetry
```
### React Integration
For React hooks (`useQuery`, `useMutate`), install React alongside Caller:
```bash
npm install react @igniter-js/caller
```
```bash
pnpm add react @igniter-js/caller
```
```bash
yarn add react @igniter-js/caller
```
```bash
bun add react @igniter-js/caller
```
The React client (`@igniter-js/caller/client`) requires React 18 or later. It uses hooks and the `useId` API.
***
## Create Your First Client
After installation, create a configured API client:
```typescript title="lib/api.ts"
import { IgniterCaller } from '@igniter-js/caller';
export const api = IgniterCaller.create()
.withBaseUrl(process.env.API_URL || 'http://localhost:3000')
.withHeaders({
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'Content-Type': 'application/json',
})
.build();
```
Caller works out of the box with no configuration. If you just need to make HTTP requests, `IgniterCaller.create().build()` is all you need.
***
## Configuration Options
The `IgniterCallerBuilder` exposes a fluent, immutable API for configuring your client:
| Method | Purpose |
| --------------------------------- | ----------------------------------------------- |
| `.withBaseUrl(url)` | Prefix for all request URLs |
| `.withHeaders(headers)` | Default headers merged into every request |
| `.withCookies(cookies)` | Default cookies (serialized as `Cookie` header) |
| `.withLogger(logger)` | Logger instance for request lifecycle events |
| `.withTelemetry(telemetry)` | Telemetry manager for observability |
| `.withSchemas(schemas, options?)` | Schema-based type safety and runtime validation |
| `.withRequestInterceptor(fn)` | Transform requests before they are sent |
| `.withResponseInterceptor(fn)` | Transform responses after they are received |
| `.withStore(adapter, options?)` | Persistent store for caching (Redis, etc.) |
| `.withMock(config)` | Enable mock mode for testing |
| `.build()` | Finalize configuration and return the manager |
Every `with*` method returns a **new builder instance** — the builder is fully immutable. This means you can safely create multiple derived clients from a base configuration:
```typescript
// Base client with shared auth
const base = IgniterCaller.create()
.withHeaders({ 'Authorization': `Bearer ${token}` });
// Public API client
const api = base.withBaseUrl('https://api.example.com').build();
// Admin API client (different base URL, same auth)
const admin = base.withBaseUrl('https://admin.example.com').build();
```
***
## Project Structure
For larger projects, we recommend organizing your API configuration:
```
src/
├── lib/
│ ├── api.ts # Core API client setup
│ └── schemas/
│ ├── index.ts # Schema registry
│ ├── users.ts # User-related schemas
│ └── products.ts # Product-related schemas
├── callers/
│ └── facebook/ # CLI-generated clients
│ ├── index.ts
│ └── schema.ts
└── features/
└── users/
└── api.ts # Feature-specific API helpers
```
### Monorepo Setup
In a monorepo with shared packages:
```typescript title="packages/api-client/src/index.ts"
import { IgniterCaller, IgniterCallerSchema } from '@igniter-js/caller';
import { sharedSchemas } from '@my-org/schemas';
export const api = IgniterCaller.create()
.withBaseUrl(process.env.API_URL!)
.withSchemas(sharedSchemas, { mode: 'strict' })
.withRequestInterceptor(async (config) => ({
...config,
headers: {
...config.headers,
'X-Client-Id': 'web',
},
}))
.build();
```
***
## Environment Configuration
Caller doesn't require any environment variables by default, but here's a typical setup:
```bash title=".env"
API_URL=https://api.example.com/v1
API_TOKEN=your-api-token
```
```typescript title="lib/api.ts"
import { IgniterCaller } from '@igniter-js/caller';
export const api = IgniterCaller.create()
.withBaseUrl(process.env.API_URL!)
.withHeaders({
'Authorization': `Bearer ${process.env.API_TOKEN}`,
})
.build();
```
### Next.js Configuration
For Next.js applications, use the appropriate variable prefix:
```bash title=".env.local"
NEXT_PUBLIC_API_URL=https://api.example.com/v1
```
```typescript title="lib/api.ts"
export const api = IgniterCaller.create()
.withBaseUrl(process.env.NEXT_PUBLIC_API_URL || '/api')
.build();
```
In Next.js App Router, you can use Caller in both Server Components (direct `.execute()`) and Client Components (via the React provider). Server-side usage doesn't need `NEXT_PUBLIC_` prefix — use regular `process.env` variables.
***
## TypeScript Configuration
Caller requires no special TypeScript configuration. The default `strict: true` setting provides the best type inference experience:
```json title="tsconfig.json"
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
```
### Path Aliases
If you use path aliases, ensure they resolve correctly:
```json title="tsconfig.json"
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
```
***
## Common Issues
This occurs when running Caller in a Node.js version older than 18, or in an environment that doesn't provide global `fetch`.
**Solutions:**
1. Upgrade to Node.js 18 or later (recommended)
2. Install a polyfill:
```bash
npm install node-fetch
```
3. If using Bun or Deno, global `fetch` is built-in — no polyfill needed
Complex schema maps with many nested types can cause TypeScript to hit instantiation depth limits.
**Solutions:**
1. Split large schema maps into smaller, composable registries
2. Use `$Infer` helpers to extract types explicitly instead of relying on deep inference
3. Ensure `skipLibCheck: true` is set in `tsconfig.json` for faster builds
The React client is exposed through a **subpath export**. Make sure your bundler/TypeScript resolves subpath exports:
```typescript
// ✅ Correct
import { IgniterCallerProvider } from '@igniter-js/caller/client';
// ❌ Incorrect — internal path, may break
import { IgniterCallerProvider } from '@igniter-js/caller/dist/client';
```
If you're using an older bundler, ensure `moduleResolution` is set to `bundler` or `node16`.
You're using `useIgniterCaller()` or `.useQuery()` without wrapping your component tree in ``.
```tsx title="app/layout.tsx"
import { IgniterCallerProvider } from '@igniter-js/caller/client';
import { api } from '@/lib/api';
export default function RootLayout({ children }) {
return (
{children}
);
}
```
Check your validation mode. In `'loose'` mode, Caller logs warnings instead of rejecting the request. In `'none'` mode, no validation happens at all.
```typescript
// Strict mode — rejects mismatched responses
const api = IgniterCaller.create()
.withSchemas(schemas, { mode: 'strict' })
.build();
// Verify mode is set as expected
```
By default, Caller uses **in-memory** caching. For persistent caching across deployments or multiple instances, configure a store adapter:
```typescript
const api = IgniterCaller.create()
.withStore(redisAdapter, {
ttl: 3600,
keyPrefix: 'api-cache:',
})
.build();
```
***
## Next Steps
---
# Quick Start
> Make your first type-safe HTTP request with Caller in under 2 minutes. Step-by-step guide covering installation, client creation, and common patterns.
URL: https://igniterjs.com/docs/caller/quick-start
This guide walks you through creating your first Caller client and making requests. By the end, you'll have a working API client with type-safe responses.
### Install Caller
```bash
npm install @igniter-js/caller
```
```bash
pnpm add @igniter-js/caller
```
```bash
yarn add @igniter-js/caller
```
```bash
bun add @igniter-js/caller
```
### Create Your Client
Create a shared API client instance. This is typically done once at application startup:
```typescript title="lib/api.ts"
import { IgniterCaller } from '@igniter-js/caller';
export const api = IgniterCaller.create()
.withBaseUrl('https://jsonplaceholder.typicode.com')
.withHeaders({
'Content-Type': 'application/json',
})
.build();
```
For the simplest case, you can skip all configuration:
```typescript
const api = IgniterCaller.create().build();
```
This works anywhere `fetch` is available.
### Make Your First GET Request
```typescript
import { api } from './lib/api';
async function fetchUsers() {
const result = await api.get('/users').execute();
if (result.error) {
console.error('Failed to fetch users:', result.error.message);
return [];
}
// result.data is the parsed response body
console.log(`Fetched ${result.data.length} users`);
return result.data;
}
```
Caller **never throws** on HTTP error statuses (like 404 or 500). Instead, it returns an `error` object in the result envelope. It only throws on network failures or configuration errors.
### Add Query Parameters
Use `.params()` to add both path parameters and query string parameters:
```typescript
// Path parameters — :id is replaced
const user = await api.get('/users/:id')
.params({ id: '1' })
.execute();
// Requests: GET /users/1
// Query parameters — added to the URL
const filtered = await api.get('/users')
.params({ page: 1, limit: 10, status: 'active' })
.execute();
// Requests: GET /users?page=1&limit=10&status=active
```
Any params not matching path segments are automatically appended as query string parameters.
### Send Data with POST
```typescript
async function createUser(name: string, email: string) {
const result = await api.post('/users')
.body({ name, email })
.execute();
if (result.error) {
// Handle validation errors, conflicts, etc.
if (result.status === 409) {
throw new Error('User already exists');
}
throw result.error;
}
// result.status is 201 for successful creation
console.log('Created user:', result.data.id);
return result.data;
}
```
### Update and Delete Resources
```typescript
// PUT — replace entire resource (idempotent)
const updated = await api.put('/users/:id')
.params({ id: '1' })
.body({ name: 'Jane Doe', email: 'jane@example.com' })
.execute();
// PATCH — partial update
const patched = await api.patch('/users/:id')
.params({ id: '1' })
.body({ name: 'Jane Updated' })
.execute();
// DELETE — remove resource
const deleted = await api.delete('/users/:id')
.params({ id: '1' })
.execute();
if (!deleted.error) {
console.log('User deleted successfully');
}
```
***
## Adding Resilience
Production applications need to handle transient failures. Caller provides retries, timeouts, and fallbacks out of the box:
```typescript
// Retry up to 3 times with exponential backoff
const result = await api.get('/external-service')
.retry(3, {
backoff: 'exponential',
baseDelay: 500,
retryOnStatus: [502, 503, 504],
})
.timeout(10000) // 10 second timeout
.fallback(() => []) // Return empty array if all retries fail
.execute();
```
By default, Caller retries on status codes `408`, `429`, `500`, `502`, `503`, and `504`. You can customize this with `retryOnStatus`.
***
## Adding Type Safety
The real power of Caller comes from schema-based type inference. Define your API contract once and get automatic TypeScript types everywhere:
```typescript
import { IgniterCaller, IgniterCallerSchema } from '@igniter-js/caller';
import { z } from 'zod';
// 1. Define your API schemas
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const schemas = IgniterCallerSchema.create()
.path('/users/:id', (path) =>
path.get({
responses: {
200: UserSchema,
404: z.object({ message: z.string() }),
},
})
)
.path('/users', (path) =>
path.get({
responses: { 200: z.array(UserSchema) },
})
.post({
request: z.object({
name: z.string(),
email: z.string().email(),
}),
responses: {
201: UserSchema,
400: z.object({ message: z.string() }),
},
})
)
.build();
// 2. Create your typed client
const api = IgniterCaller.create()
.withBaseUrl('https://api.example.com')
.withSchemas(schemas, { mode: 'strict' })
.build();
// 3. Full type inference — no manual interfaces needed!
const user = await api.get('/users/:id')
.params({ id: '1' }) // ✅ params are typed
.execute();
// user.data is typed as { id: number; name: string; email: string } | null
const created = await api.post('/users')
.body({ name: 'John', email: 'john@example.com' }) // ✅ body is typed
.execute();
// created.data is typed as { id: number; name: string; email: string } | null
```
***
## Error Handling Patterns
Caller returns a `{ data, error }` envelope. Here are the most common patterns:
```typescript
// Pattern 1: Guard clause
const { data, error } = await api.get('/users').execute();
if (error) {
console.error(`${error.code}: ${error.message}`);
return;
}
// TypeScript knows data is defined here
console.log(data.length);
// Pattern 2: Inline with fallback
const { data } = await api.get('/users')
.fallback(() => [])
.execute();
// data is always defined
console.log(data.length);
// Pattern 3: Match on status
const result = await api.post('/users').body(payload).execute();
switch (result.status) {
case 201: return result.data;
case 400: throw new ValidationError(result.error);
case 409: throw new ConflictError(result.error);
default: throw result.error;
}
```
***
## Using with React
Caller provides first-class React hooks through the `/client` subpath:
```tsx title="app/providers.tsx"
import { IgniterCallerProvider } from '@igniter-js/caller/client';
import { api } from '@/lib/api';
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
```tsx title="components/user-profile.tsx"
import { useIgniterCaller } from '@igniter-js/caller/client';
const useCaller = useIgniterCaller<{ main: typeof api }>();
function UserProfile({ id }: { id: string }) {
const main = useCaller('main');
const { data, isLoading, error, refetch } = main
.get('/users/:id')
.params({ id })
.useQuery({ staleTime: 30_000 });
if (isLoading) return Loading...
;
if (error) return Error: {error.message}
;
return (
{data?.name}
{data?.email}
refetch()}>Refresh
);
}
```
***
## Next Steps
Now that you've made your first requests, explore the full capabilities:
---
# Real-World Examples
> Production-ready patterns for common API workflows. Covers authentication, CRUD operations, file uploads, pagination, external API integration, and more.
URL: https://igniterjs.com/docs/caller/real-world-examples
This page demonstrates production-ready patterns using `@igniter-js/caller`. Each example is complete, copy-paste ready, and grounded in real-world scenarios.
***
## 1. Authenticated API Client
How to build an API client with automatic token management, refresh logic, and auth error handling.
```typescript title="lib/api.ts"
import { IgniterCaller, IgniterCallerError } from '@igniter-js/caller';
// Shared auth state
let accessToken: string | null = null;
let refreshPromise: Promise | null = null;
async function getAccessToken(): Promise {
if (accessToken) return accessToken;
// Avoid concurrent refresh calls
if (!refreshPromise) {
refreshPromise = fetch('/api/auth/refresh', { credentials: 'include' })
.then(r => r.json())
.then(data => {
accessToken = data.accessToken;
return accessToken!;
})
.finally(() => {
refreshPromise = null;
});
}
return refreshPromise;
}
export const api = IgniterCaller.create()
.withBaseUrl(process.env.API_URL!)
// Inject auth token on every request
.withRequestInterceptor(async (config) => {
const token = await getAccessToken();
return {
...config,
headers: {
...config.headers,
'Authorization': `Bearer ${token}`,
},
};
})
// Handle 401s — clear token and retry
.withResponseInterceptor(async (response) => {
if (response.status === 401) {
accessToken = null; // Force token refresh on next request
}
return response;
})
.build();
```
**Usage:**
```typescript
// Token is automatically attached
const { data: user } = await api.get('/me')
.stale(30_000) // Cache profile for 30 seconds
.execute();
// If token expires, it's refreshed automatically
const { data: orders } = await api.get('/orders').execute();
```
***
## 2. CRUD Service with Validation
A complete CRUD service for a User resource, with schema validation and error handling.
```typescript title="services/user.service.ts"
import { IgniterCallerSchema } from '@igniter-js/caller';
import { z } from 'zod';
import type { api } from '@/lib/api';
// --- Schemas ---
const UserSchema = z.object({
id: z.string(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user']),
createdAt: z.string().datetime(),
});
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const UpdateUserSchema = z.object({
name: z.string().min(1).optional(),
email: z.string().email().optional(),
role: z.enum(['admin', 'user']).optional(),
});
const PaginatedResponse = (schema: T) =>
z.object({
data: z.array(schema),
meta: z.object({
page: z.number(),
limit: z.number(),
total: z.number(),
totalPages: z.number(),
}),
});
export const userSchemas = IgniterCallerSchema.create()
.path('/users', (path) =>
path.get({
responses: { 200: PaginatedResponse(UserSchema) },
})
.post({
request: CreateUserSchema,
responses: {
201: UserSchema,
400: z.object({ message: z.string(), errors: z.record(z.array(z.string())) }),
409: z.object({ message: z.string() }),
},
})
)
.path('/users/:id', (path) =>
path.get({
responses: {
200: UserSchema,
404: z.object({ message: z.string() }),
},
})
.patch({
request: UpdateUserSchema,
responses: {
200: UserSchema,
404: z.object({ message: z.string() }),
},
})
.delete({
responses: {
204: undefined,
404: z.object({ message: z.string() }),
},
})
)
.build();
// --- Service ---
export const UserService = {
async list(page = 1, limit = 20) {
const { data, error } = await api.get('/users')
.params({ page, limit })
.stale(10_000)
.execute();
if (error) throw error;
return data!;
},
async getById(id: string) {
const { data, error } = await api.get('/users/:id')
.params({ id })
.stale(30_000)
.execute();
if (error) {
if (error.code === 'IGNITER_CALLER_HTTP_ERROR' && (error as any).statusCode === 404) {
return null;
}
throw error;
}
return data!;
},
async create(input: z.infer) {
const { data, error, status } = await api.post('/users')
.body(input)
.execute();
if (error) {
if (status === 400) {
throw new ValidationError('Invalid user data', (error as any).details);
}
if (status === 409) {
throw new ConflictError('User already exists');
}
throw error;
}
return data!;
},
async update(id: string, input: z.infer) {
const { data, error } = await api.patch('/users/:id')
.params({ id })
.body(input)
.execute();
if (error) throw error;
return data!;
},
async remove(id: string): Promise {
const { error } = await api.delete('/users/:id')
.params({ id })
.execute();
return !error;
},
};
// Custom errors
class ValidationError extends Error {
constructor(message: string, public errors: Record) {
super(message);
this.name = 'ValidationError';
}
}
class ConflictError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConflictError';
}
}
```
***
## 3. File Upload with Progress
Handling file uploads with FormData, progress tracking, and timeout management.
```typescript title="services/upload.service.ts"
import { api } from '@/lib/api';
interface UploadResult {
id: string;
url: string;
filename: string;
size: number;
}
export async function uploadFile(
file: File,
onProgress?: (percent: number) => void,
): Promise {
const formData = new FormData();
formData.append('file', file);
formData.append('folder', 'documents');
const { data, error } = await api.post('/uploads')
.body(formData)
.timeout(120_000) // 2 minutes for large files
.retry(2, { retryOnStatus: [502, 503] })
.execute();
if (error) {
if (error.code === 'IGNITER_CALLER_TIMEOUT') {
throw new Error('Upload timed out. File may be too large.');
}
throw error;
}
return data as UploadResult;
}
// Multiple files
export async function uploadMultipleFiles(
files: File[],
): Promise {
const formData = new FormData();
files.forEach((file, i) => {
formData.append(`files[${i}]`, file);
});
const { data, error } = await api.post('/uploads/batch')
.body(formData)
.timeout(300_000) // 5 minutes for batch uploads
.execute();
if (error) throw error;
return data as UploadResult[];
}
// Download with progress
export async function downloadFile(fileId: string): Promise {
const { data, error } = await api.get('/files/:id/download')
.params({ id: fileId })
.responseType()
.execute();
if (error) throw error;
return data!;
}
```
***
## 4. External API Integration (Stripe)
Integrating with a third-party API with error classification, retry strategy, and response normalization.
```typescript title="services/stripe.service.ts"
import { IgniterCaller } from '@igniter-js/caller';
const stripe = IgniterCaller.create()
.withBaseUrl('https://api.stripe.com/v1')
.withHeaders({
'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`,
'Stripe-Version': '2024-06-20',
})
.withRequestInterceptor(async (config) => ({
...config,
headers: {
...config.headers,
'Idempotency-Key': config.headers?.['Idempotency-Key'] || crypto.randomUUID(),
},
}))
.build();
export async function createPaymentIntent(amount: number, currency = 'usd') {
const body = new URLSearchParams({
amount: String(amount),
currency,
});
const { data, error } = await stripe.post('/payment_intents')
.body(body)
.headers({ 'Content-Type': 'application/x-www-form-urlencoded' })
.retry(3, {
backoff: 'exponential',
baseDelay: 1000,
retryOnStatus: [429, 500, 502, 503],
})
.execute();
if (error) {
throw classifyStripeError(error);
}
return data as PaymentIntent;
}
function classifyStripeError(error: any): Error {
switch (error.statusCode) {
case 400: return new StripeError('Invalid request', error.details);
case 401: return new StripeError('Invalid API key', error.details);
case 402: return new StripeError('Payment required', error.details);
case 429: return new StripeError('Rate limited — please retry', error.details);
default: return new StripeError('Stripe API error', error.details);
}
}
```
***
## 5. Paginated Data Fetching
A reusable utility for fetching paginated APIs with cursor or offset-based pagination.
```typescript title="lib/pagination.ts"
import type { IgniterCallerManager } from '@igniter-js/caller';
interface PaginationParams {
page?: number;
limit?: number;
cursor?: string;
}
interface PaginatedResponse {
data: T[];
nextCursor?: string;
hasMore: boolean;
total?: number;
}
/**
* Generic paginated fetcher. Works with both cursor-based and offset-based pagination.
*/
export async function fetchPaginated(
api: IgniterCallerManager,
path: string,
params: PaginationParams = {},
): Promise> {
const { data, error } = await api.get(path)
.params(params as any)
.stale(15_000)
.retry(2)
.execute();
if (error) throw error;
return data as PaginatedResponse;
}
/**
* Fetches all pages automatically (use with caution for large datasets).
*/
export async function fetchAllPages(
api: IgniterCallerManager