# Cossistant documentation URL: /docs Quickstart with your framework [#quickstart-with-your-framework] Start by selecting your framework of choice, then follow the instructions to install the dependencies and structure your app. Cossistant currently ships quickstarts for Next.js and React.
Next.js Next.js Add Cossistant {``} widget to your Next.js project. React React Add Cossistant {``} widget to any React project.
# Advanced URL: /docs/advanced `` is for shipping fast. `Advanced` is for teams that want to build their own support experience. Use this section when [#use-this-section-when] * you want full control over the support UI * you want to start from our source code instead of the ready-made widget * you want to compose your own support shell from reusable logic and primitives The vision [#the-vision] The shipped support widget is built from reusable logic and primitives. That means you can: * ship fast with [``](/docs/support-component) * inspect the shipped widget source and use it as a base * build your own support UI with the same logic and primitives underneath Templates are coming soon [#templates-are-coming-soon] We are working on templates with full examples for common custom builds. What to do today [#what-to-do-today] 1. Start with the [support source](https://github.com/cossistantcom/cossistant/tree/main/packages/react/src/support) if you want a real widget codebase to adapt. 2. Use [Primitives](/docs/advanced/primitives) when you want headless building blocks. 3. Keep [Hooks Reference](/docs/support-component/hooks) and [Types Reference](/docs/support-component/types) nearby while you build. # Primitives URL: /docs/advanced/primitives This page is for the full-custom path. If you want the ready-made widget, stay in [``](/docs/support-component). If you want to build your own support UI, these primitives are the building blocks underneath it. Templates with full examples are coming soon. If you want to start today, use the [support source](https://github.com/cossistantcom/cossistant/tree/main/packages/react/src/support) as your base and pull in the primitives you need. Use this page when [#use-this-page-when] * `Support` and `slots` are no longer enough * you want to own the support shell, layout, and interaction model * you want reusable building blocks instead of copying a monolithic widget Import [#import] Access primitives through the `Primitives` namespace: ```tsx import { Primitives } from "@cossistant/react"; ``` Smallest working example [#smallest-working-example] ```tsx import { Primitives, useSupportConfig } from "@cossistant/react"; function CustomWidget() { const { isOpen, toggle } = useSupportConfig(); return ( <> {({ unreadCount }) => ( )} {({ close }) => isOpen ? (

Custom support content

) : null }
); } ``` Common building blocks [#common-building-blocks] Shell and routing [#shell-and-routing] * `Primitives.Trigger` * `Primitives.Window` * `Primitives.Router` * `Primitives.Config` Conversation UI [#conversation-ui] * `Primitives.ConversationTimeline` * `Primitives.TimelineItem` * `Primitives.TimelineItemGroup` * `Primitives.ToolActivityRow` Input and feedback [#input-and-feedback] * `Primitives.MultimodalInput` * `Primitives.FileInput` * `Primitives.FeedbackCommentInput` * `Primitives.FeedbackRatingSelector` * `Primitives.FeedbackTopicSelect` Shared display pieces [#shared-display-pieces] * `Primitives.Avatar` * `Primitives.DaySeparator` * `Primitives.TypingIndicator` * `Primitives.Button` When to stop here [#when-to-stop-here] * the headless build is working and you only need hooks or shared types next * you still want Cossistant state, navigation, and message APIs under your own UI Next step [#next-step] * [Advanced](/docs/advanced) for the full-custom path and source-code starting point * [Hooks Reference](/docs/support-component/hooks) for state, visitor, and navigation control * [Types Reference](/docs/support-component/types) for the shared data models behind the primitives # Visitors URL: /docs/concepts What are Visitors? [#what-are-visitors] **Visitors** are automatically created when someone loads your application with the Cossistant SDK. They represent anonymous users before they're identified as [contacts](/docs/concepts/contacts). Every visitor is unique per device/browser, persisting across page loads and sessions. How Visitors are Tracked [#how-visitors-are-tracked] Cossistant uses a combination of techniques to maintain visitor identity: * **LocalStorage**: Stores a unique visitor ID in the browser * **Fingerprinting**: Browser and device characteristics for additional persistence * **Automatic creation**: No setup required—visitors are created on first load This means a visitor on desktop and the same person on mobile will be two different visitors until they're [identified](/docs/concepts/contacts). Anonymous by Default [#anonymous-by-default] Visitors start anonymous with no personal information: * No name, email, or external ID * Only browser-derived data (language, timezone) * Can start [conversations](/docs/concepts/conversations) without authentication * Perfect for public-facing pages or logged-out users Visitor Properties [#visitor-properties] Each visitor has: * **id**: Unique identifier for this visitor * **language**: Browser language (e.g., "en-US") * **timezone**: Browser timezone (e.g., "America/New\_York") * **isBlocked**: Whether this visitor has been blocked from support * **contact**: The associated contact (null until identified) Identifying Visitors [#identifying-visitors] Transform anonymous visitors into identified [contacts](/docs/concepts/contacts) when users authenticate: Using the Component (Server Components) [#using-the-component-server-components] ```tsx showLineNumbers title="app/dashboard/layout.tsx" import { IdentifySupportVisitor } from "@cossistant/next"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; export default async function DashboardLayout({ children }) { const session = await auth.api.getSession({ headers: await headers(), }); return (
{session?.user && ( )} {children}
); } ``` Using the Hook (Client Components) [#using-the-hook-client-components] ```tsx showLineNumbers title="components/auth-handler.tsx" "use client"; import { useVisitor } from "@cossistant/next"; import { useEffect } from "react"; export function AuthHandler({ user }) { const { visitor, identify } = useVisitor(); useEffect(() => { if (user && !visitor?.contact) { identify({ externalId: user.id, email: user.email, name: user.name, }); } }, [user, visitor?.contact, identify]); return null; } ``` Once identified, all conversations and data are linked to the [contact](/docs/concepts/contacts), even across different devices. Learn More [#learn-more] * **[Contacts](/docs/concepts/contacts)**: Identified visitors with metadata and cross-device support * **[IdentifySupportVisitor](/docs/support-component#identifying-visitors)**: Component for visitor identification * **[useVisitor](/docs/support-component/hooks#usevisitor)**: Hook for programmatic visitor control # Contacts URL: /docs/concepts/contacts What are Contacts? [#what-are-contacts] **Contacts** are identified [visitors](/docs/concepts). When you identify an anonymous visitor with an `externalId` (your user ID) or `email`, they become a contact. Contacts enable: * **Cross-device support**: Same user on desktop and mobile shares conversation history * **Rich metadata**: Attach context like plan type, MRR, company, or lifecycle stage * **Dashboard visibility**: Support agents see user details alongside conversations * **Persistent identity**: Conversations follow the user, not the device Creating Contacts [#creating-contacts] Identification Requirements [#identification-requirements] A contact requires **at least one** of: * `externalId`: Your internal user ID (recommended) * `email`: User's email address Both are accepted, but `externalId` is preferred for robust cross-system tracking. Using the Component [#using-the-component] ```tsx showLineNumbers title="app/dashboard/layout.tsx" import { IdentifySupportVisitor } from "@cossistant/next"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; export default async function DashboardLayout({ children }) { const session = await auth.api.getSession({ headers: await headers(), }); return (
{session?.user && ( )} {children}
); } ``` Using the Hook [#using-the-hook] ```tsx showLineNumbers title="components/auth-handler.tsx" "use client"; import { useVisitor } from "@cossistant/next"; import { useEffect } from "react"; export function AuthHandler({ user }) { const { visitor, identify } = useVisitor(); useEffect(() => { if (user && !visitor?.contact) { identify({ externalId: user.id, email: user.email, name: user.name, image: user.avatar, metadata: { plan: user.plan, signupDate: user.createdAt, }, }); } }, [user, visitor?.contact, identify]); return null; } ``` Contact Metadata [#contact-metadata] Metadata provides context to support agents during conversations. It appears in your dashboard alongside chat threads. What to Include [#what-to-include] Common metadata fields: * **plan**: Subscription tier (free, pro, enterprise) * **signupDate**: When the user joined * **company**: Organization name * **mrr**: Monthly recurring revenue * **lifecycleStage**: lead, trial, customer, churned * **lastActive**: Last activity timestamp Metadata Schema [#metadata-schema] ```typescript type VisitorMetadata = Record; ``` Only primitive values are supported—no nested objects or arrays. Updating Metadata [#updating-metadata] Metadata can be updated anytime to reflect user changes: ```tsx showLineNumbers title="components/upgrade-button.tsx" "use client"; import { useVisitor } from "@cossistant/next"; export function UpgradeButton() { const { setVisitorMetadata } = useVisitor(); const handleUpgrade = async () => { await upgradeToPro(); // Update metadata so agents see the new plan await setVisitorMetadata({ plan: "pro", upgradedAt: new Date().toISOString(), mrr: 99, }); }; return ; } ``` Efficient Metadata Updates [#efficient-metadata-updates] Cossistant **hashes metadata** before sending updates. If metadata hasn't changed, no API call is made—preventing unnecessary network requests and database writes. This means you can safely call `setVisitorMetadata()` or re-render `` without performance concerns. One Contact, Multiple Visitors [#one-contact-multiple-visitors] A single contact can have multiple [visitors](/docs/concepts) associated with it: * **Desktop visitor**: User on their laptop * **Mobile visitor**: Same user on their phone * **Tablet visitor**: Same user on their iPad All three visitors share: * Conversation history * Contact metadata * Support context This provides seamless support across devices—agents see the full picture regardless of where the user reaches out. Learn More [#learn-more] * **[Visitors](/docs/concepts)**: Anonymous users before identification * **[IdentifySupportVisitor](/docs/support-component#identifying-visitors)**: Component for creating contacts * **[useVisitor](/docs/support-component/hooks#usevisitor)**: Hook for programmatic contact management * **[Conversations](/docs/concepts/conversations)**: Chat threads associated with contacts # Conversations URL: /docs/concepts/conversations What are Conversations? [#what-are-conversations] **Conversations** are threaded chat sessions between [visitors](/docs/concepts) (or [contacts](/docs/concepts/contacts)) and your support team. Each conversation has a timeline of [items](/docs/concepts/timeline-items) such as messages, events, identification records, and tool activity. Key Properties [#key-properties] Every conversation includes: * **status**: `open`, `resolved`, or `spam` * **priority**: `low`, `normal`, `high`, or `urgent` * **participants**: Support agents (human or AI) involved * **tags**: Labels for categorization (billing, technical, onboarding, etc.) * **timeline**: Ordered list of [timeline items](/docs/concepts/timeline-items) * **lastTimelineItem**: Most recent activity for sorting/preview Conversation Lifecycle [#conversation-lifecycle] Creation [#creation] Conversations are created when: * A visitor sends their first message through the support widget * A support agent initiates a conversation from the dashboard * Your backend creates one via the API Status Flow [#status-flow] ``` open → resolved │ ↑ │ │ └→ spam └─ reopened ``` * **open**: Active conversation requiring attention * **resolved**: Conversation marked as complete (can be reopened) * **spam**: Conversation classified as unwanted or abusive traffic Priority Levels [#priority-levels] Conversations can be prioritized: * **low**: General questions, non-urgent feedback * **normal**: Standard support requests (default) * **high**: Important issues affecting user experience * **urgent**: Critical problems requiring immediate attention Support agents can adjust priority based on conversation context. Real-time Updates [#real-time-updates] Conversations support **real-time synchronization** via WebSocket: * New messages appear instantly * Typing indicators show when agents are composing * Seen receipts track when messages are read * Status changes broadcast immediately The Cossistant SDK handles all WebSocket management automatically—no configuration needed. Conversation Timeline [#conversation-timeline] Each conversation has a timeline of [items](/docs/concepts/timeline-items): * **Messages**: Text and files from visitors or agents * **Events**: System activities (assigned, resolved, participant joined) * **Identification records**: Visitor/contact identification activity * **Tool items**: AI and tool execution records written into the timeline The timeline provides a complete audit trail of the conversation. Example Timeline [#example-timeline] ``` 1. [MESSAGE] Visitor: "How do I reset my password?" 2. [EVENT] Agent Sarah joined the conversation 3. [MESSAGE] Sarah: "I can help! Click your profile..." 4. [EVENT] Conversation marked as resolved 5. [MESSAGE] Visitor: "Thanks, that worked!" 6. [EVENT] Conversation reopened ``` Multi-Agent Support [#multi-agent-support] Conversations can involve multiple participants: * **Human agents**: Support team members * **AI agents**: Automated assistants * **Mixed mode**: AI handles initial triage, escalates to humans Agents can: * Join and leave conversations * See full conversation history * Add internal notes (private [timeline items](/docs/concepts/timeline-items)) Tags and Organization [#tags-and-organization] Tag conversations for filtering and reporting: ```typescript tags: ["billing", "urgent", "enterprise-customer"]; ``` Tags help: * Route conversations to specialized teams * Generate reports and analytics * Filter dashboard views * Track common issue types Seen Tracking [#seen-tracking] Cossistant tracks when participants last viewed a conversation: * **Visitors**: Automatic seen updates when widget is open * **Agents**: Tracked in dashboard * **Unread count**: Calculated per participant This powers unread badges in the support widget and dashboard. Cross-Device Continuity [#cross-device-continuity] For identified [contacts](/docs/concepts/contacts), conversations sync across devices: * Start conversation on desktop * Continue on mobile * Same history, same context Anonymous [visitors](/docs/concepts) have device-specific conversations. Learn More [#learn-more] * **[Timeline Items](/docs/concepts/timeline-items)**: Building blocks of conversations * **[Visitors](/docs/concepts)**: Anonymous users who start conversations * **[Contacts](/docs/concepts/contacts)**: Identified users with cross-device history # Timeline Items URL: /docs/concepts/timeline-items What are Timeline Items? [#what-are-timeline-items] **Timeline items** are the building blocks of [conversations](/docs/concepts/conversations). Instead of treating a conversation as a flat list of messages, Cossistant stores a timeline of structured records that can represent both chat content and everything happening around it. This design enables rich, auditable conversation histories with: * **Messages**: Visitor, human agent, or AI message content * **Events**: System activities and state changes * **Identification records**: Visitor/contact identification activity * **Tool activity**: AI and tool timeline records Why Timeline Items? [#why-timeline-items] Traditional chat systems only handle messages. Cossistant's timeline architecture provides: * **Complete audit trail**: See when agents joined, when status changed, who resolved the conversation * **Rich context**: System events provide context between messages * **Flexibility**: Mix messages, events, identification, and tool activity in one ordered history * **Extensibility**: New item types can be added without breaking existing conversations Current Item Types [#current-item-types] Message Items [#message-items] Message items hold the actual content exchanged in a conversation. They can come from a visitor, a human teammate, or an AI agent. The important implementation detail is that `text` remains the original message body. Rich content and derived content live in `parts`, so one timeline item can keep the original message while also storing things like attachments, metadata, or translations. Canonical Timeline Item Shape [#canonical-timeline-item-shape] **Message Parts:** * **text**: Plain text content * **image**: Image attachments with URL and metadata * **file**: File attachments with URL, name, size * **translation**: Stored translated text for a specific audience * **metadata**: Source channel details such as widget, email, or API Additional AI and internal parts can also appear in the same `parts` array, but conceptually the important rule is that `parts` enrich the message instead of replacing it. Event Items [#event-items] Event items are system-generated records that explain what changed around the conversation. Common examples include: * **assigned**: Agent assigned to conversation * **unassigned**: Agent removed from conversation * **participant\_requested**: Another participant was requested * **participant\_joined**: Agent joined the conversation * **participant\_left**: Agent left the conversation * **status\_changed**: Conversation status updated * **priority\_changed**: Priority level adjusted * **tag\_added**: Tag applied to conversation * **tag\_removed**: Tag removed from conversation * **resolved**: Conversation marked as resolved * **reopened**: Resolved conversation reopened * **visitor\_blocked**: Visitor blocked from support * **visitor\_unblocked**: Visitor unblocked * **visitor\_identified**: A visitor was linked to contact information * **ai\_paused / ai\_resumed**: AI handling state changed Events capture **who** did **what** and **when**, creating a transparent history. Identification Items [#identification-items] Identification items capture moments where the visitor becomes known or their identity changes in a meaningful way. They help preserve an auditable record of contact identification and related lifecycle changes inside the same conversation timeline. Tool Items [#tool-items] Tool items represent AI and tool activity that is written into the timeline. Depending on visibility and tool policy, they can power customer-facing progress, internal logs, or decision traces without needing a separate storage model. Translation Parts [#translation-parts] Translations are stored as `translation` parts on the same timeline item instead of replacing the original `text`. That lets Cossistant keep the source message forever while resolving the best display text for each audience. * **team** translations are used for dashboard-facing display * **visitor** translations are used for widget or other visitor-facing display * The dashboard can show translated text and still offer **Show original** * The widget can prefer a visitor-facing translation when one exists Translation Part Shape [#translation-part-shape] Example [#example] ```json { "type": "message", "text": "Hola, necesito ayuda con mi factura", "parts": [ { "type": "translation", "text": "Hello, I need help with my invoice", "sourceLanguage": "es", "targetLanguage": "en", "audience": "team", "mode": "auto", "modelId": "google/gemini-2.5-flash-lite" } ] } ``` Timeline Item Properties [#timeline-item-properties] All timeline items share common fields: * **id**: Unique identifier * **conversationId**: Parent conversation * **organizationId**: Owning organization * **type**: Item type (`message`, `event`, `identification`, `tool`) * **text**: Canonical/original message text when applicable * **parts**: Array of structured content parts * **visibility**: `public` or `private` (internal agent notes) * **createdAt**: Timestamp **Actor fields** (who created the item): * **visitorId**: If created by visitor * **userId**: If created by human agent * **aiAgentId**: If created by AI agent `deletedAt` exists as an internal lifecycle field, but it is usually not part of the day-to-day conceptual model when reasoning about timelines. Visibility Control [#visibility-control] Timeline items can be: * **public**: Visible to visitors and agents (default) * **private**: Internal agent notes, hidden from visitors This enables agents to: * Add context for other team members * Document resolutions internally * Share insights without visitor visibility Timeline Ordering [#timeline-ordering] Items are ordered chronologically by `createdAt` timestamp, providing a linear conversation history. The most recent item (`lastTimelineItem`) is cached on the conversation for: * Sorting conversations by activity * Displaying conversation previews * Determining unread status Learn More [#learn-more] * **[Conversations](/docs/concepts/conversations)**: Chat threads containing timeline items * **[Visitors](/docs/concepts)**: Anonymous users who create timeline items * **[Contacts](/docs/concepts/contacts)**: Identified users with persistent timelines # Contribute to Cossistant URL: /docs/others/contributors Use this guide if you want to contribute code, docs, or examples to Cossistant. It walks through the local setup, the services that should be running, the parts of the monorepo you will touch most often, and the checks to run before you open a pull request. Prerequisites [#prerequisites] Before you start, make sure your machine has: * **Docker Desktop** - Required for running Postgres and Redis locally * **Bun v1.2+** - Our package manager and runtime ([install Bun](https://bun.sh)) * **Git** - For version control * **Node-compatible shell tooling** - Helpful for local debugging and scripts Quick Start [#quick-start] Clone the monorepo, install dependencies, and start the local stack: ```bash git clone https://github.com/cossistantcom/cossistant.git cd cossistant bun install --workspaces bun dev ``` That's it! The `bun dev` command will: 1. Start Docker Compose (Postgres + Redis containers) 2. Start the API server with Upstash Workflow in local mode 3. Start the Next.js web application **Upstash Workflow runs locally** - No account needed! When the server starts, you'll see the workflow credentials displayed in your console. These credentials don't change between restarts. Local Services At A Glance [#local-services-at-a-glance] After `bun dev` finishes, you should have: * **Web app**: `http://localhost:3000` for the landing site, docs, and dashboard * **API server**: `http://localhost:3001` for REST, tRPC, WebSocket, and auth flows * **Postgres**: `localhost:5432` for relational data * **Redis**: `localhost:6379` for queues, caching, and real-time support * **Upstash Workflow local mode**: credentials printed in the API console output Database Setup [#database-setup] Default connection [#default-connection] The local database connection string is: ``` postgresql://postgres:postgres@localhost:5432/cossistant ``` This is automatically configured when you run `bun dev`. Run migrations [#run-migrations] After starting the services for the first time, run the database migrations: ```bash cd apps/api bun db:migrate ``` Seed data (optional) [#seed-data-optional] To populate the database with sample data for development: ```bash cd apps/api bun db:seed ``` Make schema changes [#make-schema-changes] When you need to modify the database schema: 1. Update the schema files in `apps/api/src/db/schema` 2. Generate a migration: ```bash cd apps/api bun db:generate ``` 3. Apply the migration: ```bash bun db:migrate ``` Open Database Studio [#open-database-studio] To explore the database with Drizzle Studio: ```bash cd apps/api bun db:studio ``` Optional Storage Setup [#optional-storage-setup] S3 file storage is **only needed if you're testing file uploads**. For most development work, you can skip this. If you do need S3: 1. Navigate to the infrastructure directory: ```bash cd infra/aws/s3-public-setup ``` 2. Follow the [Storage guide](/docs/self-host/storage) for the recommended AWS setup, the runtime env vars Cossistant expects, and a verification checklist. 3. The Terraform configuration will create the AWS resources needed for signed uploads and public asset reads. Repo Map For Contributors [#repo-map-for-contributors] These are the directories most contributors touch: Apps [#apps] * **`apps/api`** - Hono + tRPC backend with WebSocket server * RESTful and tRPC APIs * Real-time WebSocket communication * Database queries and mutations * Authentication via Better Auth * Background jobs via Upstash Workflow * **`apps/web`** - Next.js application * Marketing landing page * Documentation (Fumadocs) * Dashboard interface Packages [#packages] * **`packages/react`** - Main React SDK * Headless hooks and primitives * Pre-built `` component * Real-time WebSocket integration * **`packages/next`** - Next.js-specific SDK * Server Component support * Next.js optimized bindings * **`packages/core`** - Shared client logic * State management stores * REST and WebSocket clients * Utility functions * **`packages/types`** - TypeScript definitions * Shared types across all packages * API schemas and validation * **`packages/transactional`** - Email templates * React Email templates * Transactional email utilities * **`packages/location`** - Location utilities * Country and timezone data * Geolocation helpers Contributor Workflow [#contributor-workflow] Root commands [#root-commands] Run these from the repo root: ```bash # Start all services bun dev # Build all packages bun run build # Build specific package bun run build --filter @cossistant/react # Run linter and auto-fix issues bun run fix # Type check all packages bun run check-types # Check documentation links bun run docs:links ``` API-specific commands [#api-specific-commands] Run these from `apps/api`: ```bash # Start API server only bun run dev # Run migrations bun run db:migrate # Seed database bun run db:seed # Open Drizzle Studio bun run db:studio # Generate Better Auth schema bun run better-auth:generate-schema ``` Quality Checks Before A Pull Request [#quality-checks-before-a-pull-request] Run the smallest set of checks that matches your change: ```bash # Auto-fix linting issues across the repo bun run fix # Verify TypeScript types bun run check-types # Run docs link checks for documentation edits bun run docs:links # Run tests in the package or app you changed cd packages/react bun test ``` If you changed the API, run tests from `apps/api`. If you changed docs or marketing pages, make sure the affected pages still build and render cleanly. Pull Request Checklist [#pull-request-checklist] Before you open or merge a PR: 1. Use a clear Conventional Commits title such as `fix: tighten llms route indexing headers`. 2. Explain what changed, why it changed, and any follow-up work still pending. 3. Link the related issue, discussion, or support thread when there is one. 4. Add screenshots or recordings for UI changes. 5. Add a changeset when you touch published packages such as `@cossistant/react` or `@cossistant/next`. 6. Mention any environment variables, migrations, or manual QA steps reviewers need. Testing Notes [#testing-notes] Cossistant uses Bun's built-in test runner. Tests are colocated with source files using the `*.test.ts` naming convention. ```bash # Run tests in a specific package cd packages/react bun test # Watch mode bun test --watch # Coverage report bun test --coverage ``` Troubleshooting [#troubleshooting] * **`bun dev` fails immediately**: confirm Docker Desktop is running and ports `3000`, `3001`, `5432`, and `6379` are not already taken. * **Database errors after pulling main**: rerun `bun db:migrate` from `apps/api` to catch up with schema changes. * **Missing dependencies or stale lockfile state**: rerun `bun install --workspaces` from the repo root. * **Docs pages render but links fail**: run `bun run docs:links` before opening the PR. * **You only need package-level tests**: run `bun test` inside the package you changed instead of the entire monorepo. Commit Message Format [#commit-message-format] We follow [Conventional Commits](https://www.conventionalcommits.org/) for commit messages: * `feat:` - New features * `fix:` - Bug fixes * `docs:` - Documentation changes * `chore:` - Maintenance tasks * `refactor:` - Code refactoring * `test:` - Test updates Example: `feat: add message reactions to timeline items` Changesets [#changesets] For changes to published packages (`@cossistant/react`, `@cossistant/next`), add a changeset: ```bash bun run changeset ``` Follow the prompts to describe your changes. This will be used to generate changelogs and version bumps. Getting Help [#getting-help] * **Documentation**: [cossistant.com/docs](https://cossistant.com/docs) * **Discord**: [Join our community](https://discord.gg/vQkPjgvzcc) * **GitHub Issues**: [Report bugs or request features](https://github.com/cossistantcom/cossistant/issues) License [#license] Cossistant is licensed under **AGPL-3.0** for non-commercial use. For commercial use, please contact us at [anthony@cossistant.com](mailto:anthony@cossistant.com). # Mentions URL: /docs/others/mentions About [#about] Cossistant is a project by [Anthony Riera](https://x.com/_anthonyriera). Credits [#credits] * [ui.shadcn.com](https://ui.shadcn.com) - For the inspiration, docs, code and components. * [Vercel](https://vercel.com) - Where we host all our projects and for maintaining NextJS & Turborepo. * [Railway](https://railway.com?referralCode=nOTIh8) - Where we host the hono backend. * [Midday](https://midday.ai) - For the initial inspiration, motivation to go open source and code structure. * [Anron Icons](https://anron.pro) - For the icons (we paid for the license ofc). License [#license] © 2025 [Cossistant](https://cossistant.com). open source under GPL-3.0 license. # Third-Party Services URL: /docs/others/third-party-services Overview [#overview] Cossistant leverages best-in-class third-party services to deliver a secure, reliable, and performant customer support platform. We are committed to achieving SOC 2 Type II certification as soon as possible and have carefully selected service providers that maintain the highest security and compliance standards. This page describes the managed Cossistant Cloud stack. If you are running Cossistant on your own infrastructure, use the [Self-Host overview](/docs/self-host) for the storage and email setup paths, including the AWS-first infrastructure route and the choice between Resend and SES for email transport. Infrastructure & Hosting [#infrastructure--hosting] * **[Vercel](https://vercel.com)** - Hosts our Next.js web application and provides edge infrastructure for optimal performance worldwide. * **[Railway](https://railway.com)** - Hosts our Hono backend API, Redis instance, and provides automatic deployments with monitoring. * **[AWS S3](https://aws.amazon.com/s3/)** - Secure cloud storage for file uploads and media assets (SOC 2 Type II certified). * **[AWS CloudFront](https://aws.amazon.com/cloudfront/)** - Content delivery network (CDN) for fast, global content distribution (SOC 2 Type II certified). * **[Upstash](https://upstash.com)** - QStash and Workflows for serverless background job processing and workflow orchestration (SOC 2 Type II certified). Database & Storage [#database--storage] * **[PostgreSQL](https://www.postgresql.org/)** - Primary database for all application data. * **[Drizzle ORM](https://orm.drizzle.team/)** - Type-safe database toolkit and ORM. Authentication & Payments [#authentication--payments] * **[Better Auth](https://www.better-auth.com/)** - Modern authentication solution for secure user authentication. * **[Polar.sh](https://polar.sh)** - Payment processing and subscription management for our billing system. Communication [#communication] * **[Resend](https://resend.com)** - Transactional email delivery with high deliverability rates (SOC 2 Type II certified). Monitoring & Analytics [#monitoring--analytics] * **[OpenStatus](https://www.openstatus.dev/)** - Uptime monitoring and status page infrastructure. * **[Tinybird](https://www.tinybird.co/)** - Real-time analytics platform for inbox metrics, visitor tracking, and geolocation data (SOC 2 Type II certified). * **[DataFast](https://datafa.st/)** - Third-party web analytics script used for hosted site analytics. Security & Compliance Commitment [#security--compliance-commitment] All third-party services we use are carefully vetted for: * SOC 2 Type II compliance (current or in progress) * GDPR compliance * Robust data encryption (in transit and at rest) * Regular security audits and penetration testing * High availability and disaster recovery capabilities We regularly review our vendor landscape to ensure we maintain the highest standards of security and privacy for our customers. # Next.js URL: /docs/quickstart If you are contributing to the Cossistant repo itself, start with the [Contributor Setup Guide](/docs/others/contributors) so you boot the full monorepo and local services, not just the widget in an existing app. Quick start with AI prompt [#quick-start-with-ai-prompt] Manually [#manually] 1\. Install the package [#1-install-the-package] ```bash npm install @cossistant/next ``` 2\. Add your public API key [#2-add-your-public-api-key] ```bash title=".env.local" NEXT_PUBLIC_COSSISTANT_API_KEY=pk_test_xxxx ``` 3\. Add `SupportProvider` [#3-add-supportprovider] ```tsx title="app/layout.tsx" import { SupportProvider } from "@cossistant/next"; import "./globals.css"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ``` 4\. Import styles [#4-import-styles] Tailwind v4 Plain CSS ```css title="app/globals.css" @import "tailwindcss"; @import "@cossistant/next/support.css"; ``` ```tsx title="app/layout.tsx" import { SupportProvider } from "@cossistant/next"; import "@cossistant/next/styles.css"; import "./globals.css"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ``` 5\. Render the widget [#5-render-the-widget] ```tsx title="app/page.tsx" import { Support } from "@cossistant/next"; export default function Page() { return (

You are ready to chat

); } ``` 6\. Identify logged-in visitors (optional) [#6-identify-logged-in-visitors-optional] ```tsx title="app/(app)/layout.tsx" import { IdentifySupportVisitor } from "@cossistant/next"; export default function AppLayout({ children }: { children: React.ReactNode }) { const user = { id: "user_123", email: "jane@acme.com", name: "Jane Doe", }; return ( <> {children} ); } ``` 7\. Display custom messages with `SupportConfig defaultMessages` [#7-display-custom-messages-with-supportconfig-defaultmessages] ```tsx title="app/page.tsx" import { Support, SupportConfig } from "@cossistant/next"; import { type DefaultMessage, SenderType } from "@cossistant/types"; const user: { name: string | null } = { name: "Jane Doe", }; const defaultMessages: DefaultMessage[] = [ { content: `Hi ${user.name ?? "there"}, anything I can help with?`, senderType: SenderType.TEAM_MEMBER, }, ]; const quickOptions: string[] = ["How to identify a visitor?"]; export default function Page() { return ( <> ); } ``` Next in the Support docs [#next-in-the-support-docs] 1. [Overview](/docs/support-component) for the fastest path from first render to production-ready widget. 2. [Change One Thing](/docs/support-component/customization) to swap the bubble or first screen without rebuilding the widget. 3. [Match Your Brand](/docs/support-component/theme) to set colors, radius, and dark mode. # API Keys URL: /docs/quickstart/api-keys Get your public key [#get-your-public-key] Open your dashboard at **Settings → Developers** and create or copy a **Public key**. Configure environment variables [#configure-environment-variables] Next.js Vite Other ```bash title=".env" VITE_COSSISTANT_API_KEY=pk_live_xxxxxxxxxxxx ``` ```bash title=".env.local" NEXT_PUBLIC_COSSISTANT_API_KEY=pk_live_xxxxxxxxxxxx ``` ```bash title=".env" COSSISTANT_API_KEY=pk_live_xxxxxxxxxxxx ``` Auto-detection The SDK automatically detects your framework and checks `VITE_COSSISTANT_API_KEY` (Vite), `NEXT_PUBLIC_COSSISTANT_API_KEY` (Next.js), or `COSSISTANT_API_KEY` (other). You can also pass the key explicitly through `publicKey`. Public vs private keys [#public-vs-private-keys] | Type | Prefix | Purpose | Safe in browser | | ----------- | ------------------------ | -------------------- | --------------- | | **Public** | `pk_live_*`, `pk_test_*` | Widget integration | Yes | | **Private** | `sk_live_*`, `sk_test_*` | Server-to-server API | No | Only use **public** keys in frontend widget code. Allowed domains [#allowed-domains] Public keys work only on whitelisted domains. * Production keys (`pk_live_*`) require explicit allowlisting. * Test keys (`pk_test_*`) work on localhost. Use full origins, for example: * `https://example.com` * `https://app.example.com` * `https://staging.example.com` Troubleshooting [#troubleshooting] Configuration error in the widget [#configuration-error-in-the-widget] Check: * env variable name is correct for your framework * key is still active * app was restarted after env changes Domain not allowed [#domain-not-allowed] Check: * domain is listed in **Settings → Developers → Allowed domains** * protocol matches (`https://`) # React URL: /docs/quickstart/react Quick start with AI prompt [#quick-start-with-ai-prompt] Manually [#manually] 1\. Install the package [#1-install-the-package] ```bash npm install @cossistant/react ``` 2\. Add your public API key [#2-add-your-public-api-key] Vite Next.js Other ```bash title=".env" VITE_COSSISTANT_API_KEY=pk_test_xxxx ``` ```bash title=".env.local" NEXT_PUBLIC_COSSISTANT_API_KEY=pk_test_xxxx ``` For other frameworks (CRA, Remix, etc.), either set the env variable: ```bash title=".env" COSSISTANT_API_KEY=pk_test_xxxx ``` Or pass the key directly via the `publicKey` prop: ```tsx ``` Auto-detection The SDK automatically detects your framework and reads the right environment variable (`VITE_COSSISTANT_API_KEY`, `NEXT_PUBLIC_COSSISTANT_API_KEY`, or `COSSISTANT_API_KEY`). You can also pass the key explicitly through `publicKey`. 3\. Add `SupportProvider` [#3-add-supportprovider] ```tsx title="src/main.tsx" import React from "react"; import ReactDOM from "react-dom/client"; import { SupportProvider } from "@cossistant/react"; import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( , ); ``` Passing the key explicitly If your framework does not support automatic env variable detection, pass `publicKey` directly: ``. 4\. Import styles [#4-import-styles] The widget does not inject styles automatically. Import one CSS entrypoint at the app root. Start with plain CSS unless your app is already running Tailwind CSS v4. Plain CSS Tailwind v4 ```tsx title="src/main.tsx" import React from "react"; import ReactDOM from "react-dom/client"; import { SupportProvider } from "@cossistant/react"; import "@cossistant/react/styles.css"; import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( , ); ``` ```css title="src/index.css" @import "tailwindcss"; @import "@cossistant/react/support.css"; ``` 5\. Render the widget [#5-render-the-widget] ```tsx title="src/App.tsx" import { Support } from "@cossistant/react"; export default function App() { return (

You are ready to chat

); } ``` 6\. Identify logged-in visitors (optional) [#6-identify-logged-in-visitors-optional] ```tsx title="src/App.tsx" import { IdentifySupportVisitor, Support } from "@cossistant/react"; export default function App() { const user = { id: "user_123", email: "jane@acme.com", name: "Jane Doe", }; return ( <> ); } ``` 7\. Display custom messages with `SupportConfig defaultMessages` [#7-display-custom-messages-with-supportconfig-defaultmessages] ```tsx title="src/App.tsx" import { Support, SupportConfig } from "@cossistant/react"; import { type DefaultMessage, SenderType } from "@cossistant/types"; const user: { name: string | null } = { name: "Jane Doe", }; const defaultMessages: DefaultMessage[] = [ { content: `Hi ${user.name ?? "there"}, anything I can help with?`, senderType: SenderType.TEAM_MEMBER, }, ]; const quickOptions: string[] = ["How to identify a visitor?"]; export default function App() { return ( <> ); } ``` Next in the Support docs [#next-in-the-support-docs] 1. [Overview](/docs/support-component) for the fastest path from first render to production-ready widget. 2. [Change One Thing](/docs/support-component/customization) to swap the bubble or first screen without rebuilding the widget. 3. [Match Your Brand](/docs/support-component/theme) to set colors, radius, and dark mode. # Overview URL: /docs/self-host If you are self-hosting Cossistant, there are four infrastructure responsibilities you want to settle early: billing, storage, email, and analytics. Cossistant expects: * a clear billing mode decision: keep Polar enabled, or disable billing and run with unlimited self-hosted entitlements * object storage for uploads and public file reads * transactional email infrastructure that can send mail, receive replies, and report lifecycle events * analytics infrastructure for inbox metrics and live presence, or an explicit decision to disable those managed analytics features We recommend an AWS-first path for the official self-host setup because the repo already ships Terraform modules for it, but the docs in this section are organized around roles rather than vendors: * [Billing](/docs/self-host/billing) covers the `POLAR_ENABLED` switch, why Polar stays enabled by default, and what changes when billing is disabled * [Storage](/docs/self-host/storage) covers uploads, public file reads, and the AWS S3 setup path * [Email Setup](/docs/self-host/email-setup) covers transactional email, inbound replies, and how to choose between Resend and SES * [Analytics](/docs/self-host/analytics) covers Tinybird-backed analytics, the Tinybird/DataFast toggle env vars, and what happens when analytics are disabled Billing responsibility [#billing-responsibility] Cossistant has two valid billing modes for self-hosting: * keep Polar enabled and preserve the hosted subscription and AI metering behavior * disable Polar and run in a self-hosted mode where billing flows, credit metering, and plan limits are bypassed Polar stays enabled by default so the repo behaves like the managed product unless you explicitly opt out. Use the [Billing guide](/docs/self-host/billing) to decide which mode you want and to configure `POLAR_ENABLED`. Why the AWS-first path is recommended [#why-the-aws-first-path-is-recommended] The repo already ships Terraform modules for both services: * `infra/aws/s3-public-setup` * `infra/aws/ses-email-setup` That matters because the self-host path is concrete instead of theoretical. You are not starting from scratch with generic storage or email plumbing. You are following infrastructure that already matches what the app expects at runtime. Cossistant supports S3-compatible storage settings at runtime through `S3_ENDPOINT` and `S3_FORCE_PATH_STYLE`, and it supports both `resend` and `ses` as transactional email transports. The bundled Terraform setup paths in this repo are AWS-first for storage and for the SES option. Storage responsibility [#storage-responsibility] Cossistant needs object storage where the API can generate presigned upload URLs and where uploaded files can be read back through stable public URLs. In practice that means: * the API signs uploads * the browser uploads directly to object storage * the app stores and renders the resulting public URLs * uploaded files can be grouped by organization, website, and entity Use the [Storage guide](/docs/self-host/storage) to set that up. Email responsibility [#email-responsibility] For email, Cossistant needs more than simple outbound delivery. The app also depends on reply routing and lifecycle events. In practice that means: * React Email renders the email content inside the app * outbound sending is selected by `EMAIL_TRANSPORT_PROVIDER` * reply-to addresses point to an inbound domain controlled by Cossistant * inbound replies eventually come back into the API as normalized webhook payloads * bounce, complaint, and failure events feed suppression and delivery monitoring The good news is that you can choose your transport: * `resend` is supported and remains the default * `ses` is supported and is the AWS-native path for self-hosting Use the [Email Setup guide](/docs/self-host/email-setup) to choose a provider and configure the full inbound and outbound path. Analytics responsibility [#analytics-responsibility] Cossistant uses analytics infrastructure for two product areas: * inbox analytics * live visitor presence and "last seen in app" enrichment For self-hosting, you have two valid paths: * keep Tinybird enabled and point the app at your Tinybird setup * disable Tinybird entirely and run without the Tinybird-backed analytics UI The app also includes a separate DataFast script for our hosted web analytics. Self-hosters can disable that independently. Use the [Analytics guide](/docs/self-host/analytics) to choose how you want to handle both Tinybird and DataFast in your deployment. How the pieces fit together [#how-the-pieces-fit-together] At a high level, a self-hosted deployment looks like this: 1. A browser requests a presigned upload URL from the API. 2. The API signs a `PUT` to S3 and returns the upload URL plus the public URL. 3. The browser uploads the file directly to S3 instead of streaming it through the API. 4. Cossistant sends email using React Email templates and the provider selected by `EMAIL_TRANSPORT_PROVIDER`. 5. New reply-to addresses point to the inbound domain for the active email provider. 6. Resend or SES delivers inbound replies and lifecycle events back into the API using the provider-specific bridge path configured in the app. 7. The API turns those events into timeline messages, notification triggers, and bounce or complaint records. 8. Polar-backed billing stays enabled by default unless you explicitly disable it with `POLAR_ENABLED=false`. 9. Tinybird-backed inbox analytics and live presence are either enabled through your analytics setup or explicitly disabled through env flags. Recommended setup order [#recommended-setup-order] For a clean first deployment: 1. Set up [Storage](/docs/self-host/storage) first and verify uploads. 2. Read [Billing](/docs/self-host/billing) and decide whether Polar stays enabled or whether you want `POLAR_ENABLED=false`. 3. Read [Email Setup](/docs/self-host/email-setup) and decide whether you want Resend or SES for transactional mail. 4. Configure the chosen provider and verify outbound email plus inbound reply handling. 5. Read [Analytics](/docs/self-host/analytics) and decide whether you will keep Tinybird enabled or disable Tinybird/DataFast for your deployment. 6. If you are rolling out SES gradually, keep the default `EMAIL_TRANSPORT_PROVIDER=resend` until the SES path is healthy, then flip to `ses`. If you are doing a greenfield self-host deployment and do not plan to use Resend at all, you can move directly to SES once DNS, identities, and webhooks are confirmed working in your environment. Guides [#guides] * [Storage](/docs/self-host/storage) * [Billing](/docs/self-host/billing) * [Email Setup](/docs/self-host/email-setup) * [Analytics](/docs/self-host/analytics) # Analytics URL: /docs/self-host/analytics Start with the [Self-Host overview](/docs/self-host) if you want the broader architecture first. This page focuses on the analytics-specific decisions you need to make for a self-hosted deployment. What Cossistant uses analytics for [#what-cossistant-uses-analytics-for] Today, Cossistant uses Tinybird for: * inbox analytics * live visitor presence * live "last seen in app" enrichment in Tinybird-backed dashboard surfaces Separately, the docs and marketing site include a DataFast script for hosted web analytics. These are independent concerns: * Tinybird powers product analytics and live presence features inside the app * DataFast is a third-party site analytics script that can be disabled separately Runtime switches [#runtime-switches] For self-hosting, all analytics-related toggles default to `true`: ```bash title=".env" TINYBIRD_ENABLED=true NEXT_PUBLIC_TINYBIRD_ENABLED=true NEXT_PUBLIC_DATAFAST_ENABLED=true ``` The parsing is strict: * `"true"` enables the feature * any other value disables it Tinybird enabled path [#tinybird-enabled-path] If you want inbox analytics and live visitor dashboards, keep Tinybird enabled and point the app at your Tinybird instance: ```bash title=".env" TINYBIRD_ENABLED=true NEXT_PUBLIC_TINYBIRD_ENABLED=true TINYBIRD_HOST=https://your-tinybird.example.com TINYBIRD_TOKEN=... TINYBIRD_SIGNING_KEY=... TINYBIRD_WORKSPACE=... ``` If you want to self-host or self-manage Tinybird itself, start with Tinybird's own guides: * [Tinybird infrastructure / self-host overview](https://www.tinybird.co/blog/tb-infra) * [Tinybird self-managed installation guide](https://www.tinybird.co/docs/forward/install-tinybird/self-managed) Tinybird disabled path [#tinybird-disabled-path] If Tinybird is hard to support in your environment, you can disable it cleanly: ```bash title=".env" TINYBIRD_ENABLED=false NEXT_PUBLIC_TINYBIRD_ENABLED=false ``` When Tinybird is disabled: * Tinybird ingestion stops on the API side * Tinybird JWT/token generation stops * inbox analytics UI is hidden * live visitors overlay, live presence counts, and Tinybird-backed live maps are hidden What still works: * Postgres-backed visitor and user `lastSeenAt` timestamps continue updating * places in the app that already fall back to database `lastSeenAt` continue to show those timestamps * the rest of the product continues working without Tinybird What this does **not** do: * it does not recreate Tinybird analytics on Postgres * it does not keep the Tinybird-only dashboards visible with replacement data DataFast [#datafast] DataFast is treated as an optional third-party analytics dependency for our hosted web analytics script. To disable it for self-hosting: ```bash title=".env" NEXT_PUBLIC_DATAFAST_ENABLED=false ``` When disabled, the root web layout simply omits the `datafa.st` script. Recommended self-host defaults [#recommended-self-host-defaults] If you want the simplest self-host path first: 1. Decide whether you truly need the Tinybird-backed analytics UI on day one. 2. If not, set `TINYBIRD_ENABLED=false` and `NEXT_PUBLIC_TINYBIRD_ENABLED=false`. 3. Disable `NEXT_PUBLIC_DATAFAST_ENABLED` unless you explicitly want that third-party script. 4. Bring Tinybird back later only if you want the inbox analytics and live visitor dashboards. # Billing URL: /docs/self-host/billing Start with the [Self-Host overview](/docs/self-host) if you want the full deployment picture first. This page focuses on Polar, plan enforcement, and AI credit metering. Why Polar is enabled by default [#why-polar-is-enabled-by-default] Cossistant ships with Polar enabled by default because that is the same behavior the hosted product relies on: * plan upgrades and downgrades go through Polar * website subscriptions are resolved from Polar customer state * AI credits are metered through Polar * billing pages and customer portal flows assume Polar is available That default is important because it keeps the repo aligned with the managed cloud behavior out of the box. The self-hosted override [#the-self-hosted-override] If you do not want any Polar dependency in your deployment, disable it explicitly: ```bash title=".env" POLAR_ENABLED=false ``` The parsing is strict: * missing `POLAR_ENABLED` means Polar stays enabled * `POLAR_ENABLED=true` keeps Polar enabled * any other value disables Polar What happens when billing is disabled [#what-happens-when-billing-is-disabled] When `POLAR_ENABLED=false`, Cossistant switches into a self-hosted billing-disabled mode. That means: * Polar checkout and customer portal flows are bypassed * Polar webhooks are not mounted * organization creation does not create Polar customers * website creation does not provision free subscriptions * website deletion does not try to revoke Polar subscriptions * AI credit metering is disabled * plan limits and feature gates become effectively unlimited In practice, the app resolves a synthetic `self_hosted` plan internally. That plan is used to keep the rest of the app simple: * numeric limits are treated as unlimited * boolean-gated features are treated as enabled * AI training cooldown becomes immediate * hard plan limits are not enforced So the feature code keeps calling the same plan and entitlement helpers it already uses. The billing mode is centralized instead of spreading `if/else` checks across the product. What the dashboard shows [#what-the-dashboard-shows] When billing is disabled: * the plan page shows a self-hosted state instead of hosted pricing copy * upgrade and billing actions are hidden * the billing route explains that subscription management is disabled for this deployment * AI usage is shown as unmetered instead of as an outage or credit-sync problem Recommended self-host default [#recommended-self-host-default] If you are running Cossistant fully on your own infrastructure and do not want hosted billing behavior, set: ```bash title=".env" POLAR_ENABLED=false ``` If you want your self-host deployment to keep the same billing and metering behavior as Cossistant Cloud, leave `POLAR_ENABLED` unset or set it to `true`. # Email Setup URL: /docs/self-host/email-setup Start with the [Self-Host overview](/docs/self-host) if you want the broader architecture first, and pair this guide with [Storage](/docs/self-host/storage) so uploads and email are configured together. Cossistant supports both `resend` and `ses` as transactional email transports. That choice is controlled by `EMAIL_TRANSPORT_PROVIDER`. This page is intentionally provider-neutral at the top level because email is a responsibility, not a vendor. The app renders emails the same way either way: React Email still produces the content, and the provider choice only changes the transport and inbound plumbing around it. Choose your provider [#choose-your-provider] Both providers are supported and both can live cleanly in the codebase today. * `resend` is the default and is a good fit if you want the simplest hosted email path * `ses` is the AWS-native option and is a good fit if you want the email infrastructure in your own AWS account The main transport switch is: ```bash title=".env" EMAIL_TRANSPORT_PROVIDER=resend ``` or: ```bash title=".env" EMAIL_TRANSPORT_PROVIDER=ses ``` `EMAIL_TRANSPORT_PROVIDER` controls transactional outbound mail and the active reply domain for newly sent emails. It does not automatically replace every Resend-specific helper in the codebase, such as audience and contact helpers. Current capability comparison [#current-capability-comparison] Here is the practical difference between the two transports in the current implementation: * both support transactional sending through the same app mail API * both work with `EMAIL_TRANSPORT_PROVIDER` * both coexist with provider-aware reply routing for new mail * inbound reply parsing accepts both the Resend and SES inbound domains, which makes overlap possible * Resend still owns its own audience and contact helpers outside the transport switch * SES currently does not support scheduled sends or provider tags in this rollout How email works in Cossistant [#how-email-works-in-cossistant] No matter which provider you choose, the shape of the app flow stays the same: 1. React Email renders the outbound message inside the app. 2. `EMAIL_TRANSPORT_PROVIDER` selects the active transport for new sends. 3. New reply-to addresses are generated from the inbound domain for the active provider. 4. Inbound replies and lifecycle events are normalized back into the API. 5. The API turns those events into conversation messages, delivery tracking, and suppression updates. That means Resend and SES can coexist during migration or overlap periods: * new outbound mail follows the active provider * old Resend reply chains can keep working through the Resend inbound path * SES can be enabled and verified before you flip the transport flag Shared email environment [#shared-email-environment] These env vars matter no matter which provider you choose: ```bash title=".env" EMAIL_TRANSPORT_PROVIDER=resend EMAIL_NOTIFICATION_FROM=support@example.com EMAIL_MARKETING_FROM=hello@example.com EMAIL_RESEND_INBOUND_DOMAIN=inbound.example.com EMAIL_SES_INBOUND_DOMAIN=ses-inbound.example.com ``` What they control: * `EMAIL_TRANSPORT_PROVIDER`: selects the active transactional transport for new outbound mail * `EMAIL_NOTIFICATION_FROM`: sender address for notification-style email * `EMAIL_MARKETING_FROM`: sender address for marketing-style email * `EMAIL_RESEND_INBOUND_DOMAIN`: the inbound reply domain used by the Resend path * `EMAIL_SES_INBOUND_DOMAIN`: the inbound reply domain used by the SES path Even if you only plan to use one provider, it is useful to understand both domains because inbound parsing accepts both and overlap is a supported state. Resend setup [#resend-setup] Resend is the default transport in the app today. It is a good choice if you want a simple hosted provider without adding SES infrastructure right away. What to configure [#what-to-configure] Set the Resend-specific env vars: ```bash title=".env" EMAIL_TRANSPORT_PROVIDER=resend RESEND_API_KEY=re_... RESEND_WEBHOOK_SECRET=whsec_... EMAIL_RESEND_INBOUND_DOMAIN=inbound.example.com EMAIL_NOTIFICATION_FROM=support@example.com EMAIL_MARKETING_FROM=hello@example.com ``` What the app expects from Resend [#what-the-app-expects-from-resend] For the Resend path, Cossistant expects: * a valid API key for outbound transactional sends * a verified sender domain for the `from` addresses you use * an inbound domain for reply handling * webhook delivery back into the existing Resend webhook routes Webhook expectations [#webhook-expectations] The legacy Resend path stays in place specifically so it can continue handling: * replies to older Resend-sent threads * Resend lifecycle events during overlap periods If you keep Resend active while evaluating SES, do not remove the Resend inbound setup yet. SES setup [#ses-setup] SES is the AWS-native option and the recommended path if you want the full email stack to live in your own AWS account. Why choose SES [#why-choose-ses] SES is a strong fit for self-hosting because it gives Cossistant the full set of building blocks it needs: * transactional outbound sending * inbound reply handling * delivery, bounce, complaint, and failure events * infrastructure that runs in your AWS account instead of a separate email SaaS Recommended setup path [#recommended-setup-path] The SES module lives in `infra/aws/ses-email-setup`. It provisions: * SES identities and configuration * an inbound reply domain * an S3 bucket for raw inbound mail * SNS and SQS plumbing * small TypeScript Lambda bridge functions * IAM credentials for outbound SES sending Step 1: Fill the Terraform variables [#step-1-fill-the-terraform-variables] Move into the module: ```bash cd infra/aws/ses-email-setup ``` Copy the example vars file: ```bash cp terraform.tfvars.example terraform.dev.tfvars ``` Then fill in the environment-specific values: ```hcl title="terraform.dev.tfvars" aws_region = "us-east-1" environment = "dev" sender_domain = "example.com" inbound_domain = "ses-inbound.example.com" inbound_bucket_name = "cossistant-dev-ses-example" api_webhook_base_url = "https://api.example.com" ses_webhook_secret = "replace-me" route53_zone_id = "Z1234567890" ``` Step 2: Install dependencies and deploy [#step-2-install-dependencies-and-deploy] The module builds the bridge lambdas from the monorepo with `bun build`, so make sure dependencies are installed at the repo root: ```bash bun install ``` Then initialize and apply: ```bash terraform init terraform apply -var-file="terraform.dev.tfvars" ``` Step 3: Confirm DNS and SES identity health [#step-3-confirm-dns-and-ses-identity-health] Do not move on until the sender identity and inbound domain are healthy. If you use Route53 automation, Terraform can create the records for you. Otherwise, you need to add the outputs manually. At minimum, verify: * the SES sender verification record * DKIM records * MAIL FROM records * the inbound MX record for your SES reply domain Step 4: Configure the SES runtime environment [#step-4-configure-the-ses-runtime-environment] Set the SES-specific env vars: ```bash title=".env" EMAIL_TRANSPORT_PROVIDER=resend EMAIL_NOTIFICATION_FROM=support@example.com EMAIL_MARKETING_FROM=hello@example.com EMAIL_RESEND_INBOUND_DOMAIN=inbound.example.com EMAIL_SES_INBOUND_DOMAIN=ses-inbound.example.com SES_REGION=us-east-1 SES_ACCESS_KEY_ID=AKIA... SES_SECRET_ACCESS_KEY=... SES_CONFIGURATION_SET=cossistant-email-dev SES_WEBHOOK_SECRET=replace-me ``` SES-specific meanings: * `EMAIL_SES_INBOUND_DOMAIN`: the inbound reply domain owned by SES * `SES_REGION`: the AWS region used for outbound SES * `SES_ACCESS_KEY_ID` and `SES_SECRET_ACCESS_KEY`: credentials used for outbound sends * `SES_CONFIGURATION_SET`: the SES configuration set created by Terraform * `SES_WEBHOOK_SECRET`: the shared secret used to sign the bridge webhook requests Step 5: Understand the webhook bridge [#step-5-understand-the-webhook-bridge] The SES path uses small serverless bridge functions that normalize inbound mail and lifecycle events before they hit the API. Those bridges post back to: * `/ses/webhooks/inbound` * `/ses/webhooks/events` Requests include these headers: * `x-cossistant-timestamp` * `x-cossistant-signature` * `x-cossistant-event` The signature is an HMAC-SHA256 over: ```text . ``` You do not need to build this yourself if you use the Terraform module, but it is useful to know when debugging webhook traffic. Step 6: Roll SES out safely [#step-6-roll-ses-out-safely] The clean rollout path is: 1. Leave `EMAIL_TRANSPORT_PROVIDER=resend` first. 2. Deploy the SES infrastructure and verify DNS and identities. 3. Confirm the SES webhook endpoints are reachable from your public API host. 4. Test staging with outbound email, inbound replies, and lifecycle events. 5. Flip `EMAIL_TRANSPORT_PROVIDER=ses` only after SES is healthy. 6. Keep the Resend inbound path active during the overlap window so older reply chains still work. If you are doing a greenfield SES-only deployment, you can switch directly to `ses` once the identities, webhooks, and inbound flow are fully verified. Verification checklist [#verification-checklist] No matter which provider you choose, make sure all of these work: * a transactional email sends successfully * the sender domain is verified and accepted by the provider * replies land back in the correct conversation * lifecycle events reach the API and update delivery or suppression state correctly For SES specifically, also confirm: * `/ses/webhooks/inbound` is receiving normalized bridge payloads * `/ses/webhooks/events` is receiving delivery, bounce, complaint, and failure events * older Resend reply chains still work if you are migrating gradually When to pick which provider [#when-to-pick-which-provider] Choose `resend` if you want: * the quickest setup path * a fully hosted email transport * to stay aligned with the default app configuration Choose `ses` if you want: * the email stack in your own AWS account * one more self-hostable piece of infrastructure * inbound replies and lifecycle events without leaning on a separate email SaaS long term If you are unsure, start with Resend, keep the shared email env clean, and add SES when you are ready to own the infrastructure yourself. # Storage URL: /docs/self-host/storage Start with the [Self-Host overview](/docs/self-host) if you want the bigger picture first, then come back here to configure storage. Once uploads are working, finish your infrastructure setup with [Email Setup](/docs/self-host/email-setup). Cossistant needs object storage for user uploads, brand assets, and conversation attachments. For self-hosted deployments, the recommended path is Amazon S3 because the repo already includes a Terraform module that matches how the app generates upload URLs today. Why Cossistant needs storage [#why-cossistant-needs-storage] The app is built around direct browser uploads rather than proxying large files through the API server. That gives you a few practical advantages: * uploads go straight from the browser to object storage through presigned URLs * the API only signs the upload request instead of buffering file contents itself * uploaded assets can be read back through stable public URLs * you can add a CDN later without changing the application-level upload flow How Cossistant uses storage internally [#how-cossistant-uses-storage-internally] At runtime, the upload flow looks like this: 1. A client asks the API for a signed upload URL. 2. The API creates a presigned `PUT` for the configured bucket. 3. The client uploads the file directly to object storage. 4. The API returns a public URL that the app stores and renders later. Uploads are not written into a flat bucket namespace. Keys are scoped by the tenant and feature area so the data stays organized by organization, website, and entity. Depending on the feature, that entity can be a conversation, visitor, user, or contact. Example key shapes look like: ```text ///attachment.png ///avatar.png cdn////image.jpg ``` The exact suffixes vary, but the important part is that storage is tenancy-aware and feature-aware. Recommended setup path [#recommended-setup-path] The AWS-first storage module lives in `infra/aws/s3-public-setup`. It creates: * an S3 bucket for uploaded files * public read access to exact object URLs * CORS rules for direct browser uploads * an IAM user that the API can use to generate presigned upload URLs This setup allows public reads to exact object URLs. It does not allow bucket listing, but you should still treat uploaded asset URLs as publicly reachable unless you add stricter delivery controls on top. Step 1: Deploy the storage module [#step-1-deploy-the-storage-module] Move into the Terraform module: ```bash cd infra/aws/s3-public-setup ``` Copy the example vars file: ```bash cp terraform.tfvars.example terraform.dev.tfvars ``` Set a globally unique bucket name and the region you want to use: ```hcl title="terraform.dev.tfvars" bucket_name = "cossistant-dev-your-unique-suffix" environment = "dev" aws_region = "us-east-1" ``` Initialize Terraform: ```bash terraform init ``` Create or select a workspace, then apply: ```bash terraform workspace new dev terraform workspace select dev terraform apply -var-file="terraform.dev.tfvars" ``` Repeat the same pattern for production with a production bucket name and workspace. Step 2: Collect the values the app needs [#step-2-collect-the-values-the-app-needs] After `terraform apply`, record: * the bucket name * the AWS region * the IAM access key ID * the IAM secret access key Those values are what the API uses when it generates presigned upload URLs. Step 3: Configure the runtime environment [#step-3-configure-the-runtime-environment] For the standard AWS path, set these env vars in the API runtime: ```bash title=".env" S3_BUCKET_NAME=cossistant-dev-your-unique-suffix S3_REGION=us-east-1 S3_ACCESS_KEY_ID=AKIA... S3_SECRET_ACCESS_KEY=... S3_PUBLIC_BASE_URL=https://cossistant-dev-your-unique-suffix.s3.us-east-1.amazonaws.com S3_CDN_BASE_URL= ``` What each one does: * `S3_BUCKET_NAME`: the bucket that receives uploads * `S3_REGION`: the AWS region for that bucket * `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`: credentials the API uses to sign uploads * `S3_PUBLIC_BASE_URL`: the read base URL when you are serving files directly from S3 * `S3_CDN_BASE_URL`: an optional CDN base URL if you later put CloudFront or another CDN in front of reads If you are not using a CDN yet, leave `S3_CDN_BASE_URL` empty. Step 4: Restart the API and verify uploads [#step-4-restart-the-api-and-verify-uploads] Restart the API so it picks up the new storage config, then verify the full flow: 1. Generate a presigned upload URL from the app. 2. Upload a file from the browser. 3. Open the returned public URL directly. 4. Confirm the asset renders correctly where it was uploaded. A good verification pass includes: * profile avatar uploads * website branding uploads * conversation attachment uploads Advanced note: S3-compatible storage [#advanced-note-s3-compatible-storage] The documented path is AWS-first, but the runtime also supports S3-compatible providers. If you intentionally use another S3-compatible service, the extra settings are: ```bash title=".env" S3_ENDPOINT=https://your-object-store.example.com S3_FORCE_PATH_STYLE=true ``` Treat that as an advanced option. The Terraform module in this repo provisions AWS S3, not third-party object storage. Verification checklist [#verification-checklist] Before you consider storage finished, confirm all of the following: * a presigned upload URL can be generated successfully * a browser upload completes without CORS errors * the returned public URL resolves correctly * uploaded files render in the dashboard where you expect them * the configured `S3_PUBLIC_BASE_URL` or `S3_CDN_BASE_URL` matches the URLs the app stores Common issues [#common-issues] * `403` during upload: the signing credentials or bucket permissions are wrong * browser CORS failure: the bucket CORS rules were not applied or are too restrictive * upload succeeds but the public URL fails: `S3_PUBLIC_BASE_URL` is wrong or missing * URLs work directly but not in-app: confirm the stored `publicUrl` matches the configured base URL # Overview URL: /docs/support-component `Support` is the fast path. Ship the widget with good defaults, change the parts users see first, and go deeper only if you need to. Use this when [#use-this-when] * the widget already renders and you want the quickest path to production * you want to know which API to reach for next * you want to keep the default shell while you brand a few parts 30-second version [#30-second-version] ```tsx title="src/App.tsx" import { Support } from "@cossistant/react"; export default function App() { return ; } ``` The default widget gives you three good things right away: * fast to production * easy branding * safe partial overrides What you can change without rebuilding [#what-you-can-change-without-rebuilding] * `classNames` and `slotProps` for styling the built-in UI * `slots.trigger` to swap the bubble * `slots.homePage` to change the first screen * `SupportConfig` for route-level welcome messages and quick options * `theme` and `--co-theme-*` tokens for colors, radius, and light or dark mode If that covers your use case, stay here. You do not need the `Advanced` track to ship a polished widget. Identifying visitors [#identifying-visitors] Use `IdentifySupportVisitor` when a user signs in. Add `SupportConfig` when a page needs its own support context. ```tsx title="src/App.tsx" import { IdentifySupportVisitor, Support, SupportConfig, } from "@cossistant/react"; import { SenderType, type DefaultMessage } from "@cossistant/types"; const defaultMessages: DefaultMessage[] = [ { content: "Hi Jane, I can help with billing, onboarding, or migration.", senderType: SenderType.TEAM_MEMBER, }, ]; export default function App() { const user = { id: "user_123", email: "jane@acme.com", name: "Jane Doe", }; return ( <> ); } ``` When should I use `SupportConfig` ? Use `SupportConfig` for page-level defaults. Use `slots` when you want to swap UI. Use `Support.Root` when you want your own shell. When to stop here [#when-to-stop-here] * the default widget already fits your product * you only need identity, welcome messages, quick options, or prop-level styling * you do not need custom pages or a custom shell Next step [#next-step] 1. [Change One Thing](/docs/support-component/customization) to swap the bubble or first screen without rebuilding the widget. 2. [Match Your Brand](/docs/support-component/theme) to set colors, radius, and dark mode. 3. [Pages & Layouts](/docs/support-component/routing) when you need an inline embed, a custom page, or a custom shell. Want to build your own? [#want-to-build-your-own] That is a separate track now. Use [Advanced](/docs/advanced) when you want to go fully custom. The shipped widget is built from reusable logic and primitives, and you can inspect the [support source](https://github.com/cossistantcom/cossistant/tree/main/packages/react/src/support) if you want a real starting point today. We are also working on templates with full examples for common custom builds. Support Props [#support-props] # Change One Thing URL: /docs/support-component/customization `Support` is built for small edits. Start with styling, then swap one component, and keep the rest of the widget working as-is. Use this when [#use-this-when] * you want the widget live fast * you need the bubble or first screen to feel more like your product * you want safe overrides without rebuilding the router or conversation UI Smallest working change [#smallest-working-change] ```tsx import { Support } from "@cossistant/react"; ; ``` Use `classNames` for the built-in trigger and content. Use `slotProps` when you want to pass extra presentational props to built-in parts. The default widget also exposes stable styling hooks: * `data-slot` * `data-state` * `data-page` ```css [data-slot="trigger"][data-state="open"] { transform: scale(1.02); } [data-slot="content"] { backdrop-filter: blur(12px); } ``` Bubble example 1 [#bubble-example-1] Use `slots.trigger` when the launcher is the only thing that should change. ```tsx import { Support, type SupportTriggerSlotProps } from "@cossistant/react"; import * as React from "react"; function mergeClassNames(...classes: Array) { return classes.filter(Boolean).join(" "); } const ClassicBubble = React.forwardRef( function ClassicBubbleTrigger( { className, isOpen, isTyping: _isTyping, toggle, unreadCount, ...props }, ref ) { return ( ); } ); ; ``` Bubble example 2 [#bubble-example-2] The same slot can feel more like an in-product button instead of a floating chat bubble. ```tsx import { Support, type SupportTriggerSlotProps } from "@cossistant/react"; import * as React from "react"; function mergeClassNames(...classes: Array) { return classes.filter(Boolean).join(" "); } const PillBubble = React.forwardRef( function PillBubble( { className, isOpen: _isOpen, isTyping, unreadCount: _unreadCount, toggle, ...props }, ref ) { return ( ); } ); ; ``` Replace the first screen [#replace-the-first-screen] Keep the default conversation flow and change only the home page. ```tsx import { Support, type SupportHomePageSlotProps } from "@cossistant/react"; function CustomHomePage({ className, openConversationHistory, quickOptions, startConversation, website, }: SupportHomePageSlotProps) { return (

{website?.name}

Rewrite the first screen while keeping the default conversation flow.

{quickOptions.map((option) => ( ))}
); } ; ``` When to stop here [#when-to-stop-here] * your widget is in production * branding changes fit inside `classNames`, `slotProps`, or one slot override * you still want the default router, conversation page, and composer Next step [#next-step] * [Match Your Brand](/docs/support-component/theme) for colors, radius, and dark mode * [Pages & Layouts](/docs/support-component/routing) when you need custom pages or an inline embed # Events Reference URL: /docs/support-component/events Reach for this page when prop-level customization is not enough and you need to react to widget activity in your app. If you are still shipping the widget, start with [Overview](/docs/support-component), [Change One Thing](/docs/support-component/customization), or [Pages & Layouts](/docs/support-component/routing). Use this page when [#use-this-page-when] * you want analytics events when visitors start or continue a conversation * you want error logging tied to widget activity * you are building custom pages that need to emit Support events Smallest working snippet [#smallest-working-snippet] Callback props on `` are the fastest way to listen to widget events. ```tsx import { Support } from "@cossistant/react"; export function SupportWidget() { return ( { analytics.track("support_conversation_started", { conversationId }); }} onConversationEnd={({ conversationId }) => { analytics.track("support_conversation_ended", { conversationId }); }} onMessageSent={({ conversationId, message }) => { analytics.track("support_message_sent", { conversationId, messageId: message.id, }); }} onMessageReceived={({ conversationId, message }) => { analytics.track("support_message_received", { conversationId, messageId: message.id, }); }} onError={({ error, context }) => { logger.error("Support widget error", { error, context }); }} /> ); } ``` Event payloads [#event-payloads] onConversationStart [#onconversationstart] onConversationEnd [#onconversationend] onMessageSent [#onmessagesent] onMessageReceived [#onmessagereceived] onError [#onerror] Subscribe inside custom UI [#subscribe-inside-custom-ui] Use `useSupportEvents()` when the listener belongs inside a custom widget component. ```tsx "use client"; import { useSupportEvents } from "@cossistant/react"; import { useEffect } from "react"; export function AnalyticsTracker() { const events = useSupportEvents(); useEffect(() => { if (!events) { return; } const unsubscribe = events.subscribe("messageSent", (event) => { analytics.track("message_sent", event); }); return unsubscribe; }, [events]); return null; } ``` Emit from custom pages [#emit-from-custom-pages] Use `useSupportEventEmitter()` when your own page or composer needs to emit the same widget events other listeners expect. ```tsx "use client"; import { useSupportEventEmitter } from "@cossistant/react"; export function CustomConversationPage({ conversationId, }: { conversationId: string; }) { const emitter = useSupportEventEmitter(); const handleSendMessage = async (message: string) => { try { const sentMessage = await sendMessage(conversationId, message); emitter.emitMessageSent(conversationId, sentMessage); } catch (error) { emitter.emitError(error as Error, "message_send_failed"); } }; return ; } ``` Next step [#next-step] * [Hooks Reference](/docs/support-component/hooks) for the hook-level APIs used on this page * [Types Reference](/docs/support-component/types) for the shared event and message types # Hooks Reference URL: /docs/support-component/hooks Use this page when [#use-this-page-when] Reach for hooks when prop-level customization is not enough. * open or close the widget from your own UI * read or change navigation state * identify visitors or sync contact metadata in code * build custom pages on top of the Support runtime If you are still shaping the widget, start with [Overview](/docs/support-component), [Change One Thing](/docs/support-component/customization), or [Pages & Layouts](/docs/support-component/routing). Hook families [#hook-families] * `useSupport` and `useSupportConfig` for widget state * `useSupportNavigation` and page hooks for route-aware UI * `useVisitor` for identity and contact metadata * conversation hooks for custom pages, drafts, typing, uploads, and send flows useSupport [#usesupport] Access support widget state and controls from any client component. Basic Example [#basic-example] ```tsx showLineNumbers title="components/custom-support-button.tsx" "use client"; import { useSupport } from "@cossistant/react"; export function CustomSupportButton() { const { isOpen, toggle, unreadCount } = useSupport(); return ( ); } ``` Return Values [#return-values] useVisitor [#usevisitor] Programmatically identify visitors and manage contact metadata. Important: Metadata storage Metadata is stored on **contacts**, not visitors. You must call `identify()` before `setVisitorMetadata()` will work. Learn more about{" "} visitors and{" "} contacts. Example: Identify on Auth [#example-identify-on-auth] ```tsx showLineNumbers title="components/auth-handler.tsx" "use client"; import { useVisitor } from "@cossistant/react"; import { useEffect } from "react"; export function AuthHandler({ user }) { const { visitor, identify } = useVisitor(); useEffect(() => { // Only identify if we have a user and visitor isn't already a contact if (user && !visitor?.contact) { identify({ externalId: user.id, email: user.email, name: user.name, image: user.avatar, }); } }, [user, visitor?.contact, identify]); return null; } ``` Example: Update Metadata on Action [#example-update-metadata-on-action] ```tsx showLineNumbers title="components/upgrade-button.tsx" "use client"; import { useVisitor } from "@cossistant/react"; export function UpgradeButton() { const { setVisitorMetadata } = useVisitor(); const handleUpgrade = async () => { // Upgrade user's plan await upgradeToPro(); // Update contact metadata so support agents see the change await setVisitorMetadata({ plan: "pro", upgradedAt: new Date().toISOString(), mrr: 99, }); }; return ; } ``` Return Values [#return-values-1] identify() Parameters [#identify-parameters] Prefer declarative code? Use the{" "} IdentifySupportVisitor {" "} component for a simpler, declarative approach to visitor identification in Server Components. useSupportConfig [#usesupportconfig] Access and control widget visibility and size configuration. Basic Example [#basic-example-1] ```tsx showLineNumbers title="components/custom-toggle.tsx" "use client"; import { useSupportConfig } from "@cossistant/react"; export function CustomToggle() { const { isOpen, open, close, toggle, size } = useSupportConfig(); return (
Size: {size}
); } ``` Return Values [#return-values-2] Controlled mode support When using controlled mode (`open` and `onOpenChange` props on Support), these functions will call `onOpenChange` instead of modifying internal state. useSupportNavigation [#usesupportnavigation] Access navigation state and routing methods for the widget. Basic Example [#basic-example-2] ```tsx showLineNumbers title="components/navigation-buttons.tsx" "use client"; import { useSupportNavigation } from "@cossistant/react"; export function NavigationButtons() { const { page, navigate, goBack, canGoBack } = useSupportNavigation(); return (
{canGoBack && } Current page: {page}
); } ``` Return Values [#return-values-3] useSupportHandle [#usesupporthandle] Access the imperative handle from within the widget tree. Alternative to using refs on the Support component. The hook returns `null` outside the widget tree, and the table below describes the handle when it is available. Basic Example [#basic-example-3] ```tsx showLineNumbers title="components/help-button.tsx" "use client"; import { useSupportHandle } from "@cossistant/react"; export function HelpButton() { const support = useSupportHandle(); const handleNeedHelp = () => { // Open support and start a new conversation support?.startConversation("I need help with my order"); }; return ( ); } ``` Return Values [#return-values-4] useHomePage [#usehomepage] Logic hook for building custom home pages. Provides all state and actions needed for the home page. Basic Example [#basic-example-4] ```tsx showLineNumbers title="pages/custom-home.tsx" "use client"; import { useHomePage } from "@cossistant/react"; export function CustomHomePage() { const home = useHomePage({ onStartConversation: () => console.log("Conversation started"), onOpenConversation: (id) => console.log("Opened:", id), }); return (

Welcome!

{home.lastOpenConversation && ( )} {home.availableConversationsCount > 0 && ( )}
); } ``` Return Values [#return-values-5] useConversationPage [#useconversationpage] Logic hook for building custom conversation pages. Manages the conversation lifecycle, messages, and composer. Basic Example [#basic-example-5] ```tsx showLineNumbers title="pages/custom-conversation.tsx" "use client"; import { useConversationPage } from "@cossistant/react"; export function CustomConversationPage({ conversationId }: { conversationId: string }) { const conversation = useConversationPage({ conversationId, onConversationIdChange: (id) => console.log("Active:", id), }); return (
{/* Messages */}
{conversation.items.map((item) => (
{/* Render message */}
))}
{/* Composer */}
{ e.preventDefault(); conversation.composer.submit(); }}> conversation.composer.setMessage(e.target.value)} placeholder={conversation.isPending ? "Start the conversation..." : "Type a message..."} />
); } ``` Return Values [#return-values-6] useMessageComposer [#usemessagecomposer] Hook for managing message composition with file attachments. Basic Example [#basic-example-6] ```tsx showLineNumbers title="components/message-input.tsx" "use client"; import { useMessageComposer } from "@cossistant/react"; export function MessageInput({ conversationId }: { conversationId: string }) { const composer = useMessageComposer({ conversationId, onMessageSent: () => console.log("Message sent!"), }); return (
{ e.preventDefault(); composer.submit(); }}> composer.setMessage(e.target.value)} placeholder="Type a message..." /> { if (e.target.files) { composer.addFiles(Array.from(e.target.files)); } }} /> {composer.files.map((file, index) => ( {file.name}{" "} ))}
); } ``` Return Values [#return-values-7] useFileUpload [#usefileupload] Hook for handling file uploads with progress tracking. Basic Example [#basic-example-7] ```tsx showLineNumbers title="components/file-uploader.tsx" "use client"; import { useFileUpload } from "@cossistant/react"; export function FileUploader() { const upload = useFileUpload(); const conversationId = "conv_123"; return (
{ if (e.target.files?.length) { await upload.uploadFiles(Array.from(e.target.files), conversationId); } }} /> {upload.isUploading && (
{upload.progress}%
)} {upload.error &&

{upload.error.message}

}
); } ``` Return Values [#return-values-8] useSupportText [#usesupporttext] Access the localization system for the support widget. Basic Example [#basic-example-8] ```tsx showLineNumbers title="components/localized-button.tsx" "use client"; import { useSupportText } from "@cossistant/react"; export function LocalizedButton() { const format = useSupportText(); return ( ); } ``` Returned Formatter [#returned-formatter] `useSupportText()` returns a formatter function. The table below documents that function reference. useSupportEvents [#usesupportevents] Access the events context for subscribing to widget lifecycle events. The hook returns `null` when used outside the widget's event provider, and the table below documents the event context when present. Basic Example [#basic-example-9] ```tsx showLineNumbers title="components/analytics-tracker.tsx" "use client"; import { useSupportEvents } from "@cossistant/react"; import { useEffect } from "react"; export function AnalyticsTracker() { const events = useSupportEvents(); useEffect(() => { if (!events) return; const unsubscribe = events.subscribe("messageSent", (event) => { // Track in your analytics analytics.track("support_message_sent", { conversationId: event.conversationId, }); }); return unsubscribe; }, [events]); return null; } ``` Return Values [#return-values-9] useSupportEventEmitter [#usesupporteventemitter] Convenience hook for emitting events from within the widget. Return Values [#return-values-10] Types [#types] Shared support hook and data-model types now live on the [Types](/docs/support-component/types) page. Use it for `PublicVisitor`, `PublicWebsiteResponse`, `CossistantClient`, `TimelineItem`, `Conversation`, and the rest of the canonical support type reference. # Pages & Layouts URL: /docs/support-component/routing `Support` stretches pretty far before you need the full-custom track. Most layout changes still fit inside the built-in runtime. Use this when [#use-this-when] * you want a different first screen or an extra page * you want support inline instead of floating * you need your own widget shell but still want the Support router and state Smallest working change [#smallest-working-change] Use `customPages` when you want to replace one built-in page and keep the default shell. ```tsx import { Support, useSupportNavigation } from "@cossistant/react"; function CustomHomePage() { const { navigate } = useSupportNavigation(); return (

Launch support

Keep the default conversation page. Only change the first screen.

); } ; ``` Common variants [#common-variants] Embed support inline [#embed-support-inline] Use `mode="responsive"` when support should live inside the page instead of floating above it. ```tsx import { Support } from "@cossistant/react"; export default function SupportPanel() { return (
); } ``` Own the shell with `Support.Root` [#own-the-shell-with-supportroot] Use full composition when you want your own trigger, content wrapper, and page registration. ```tsx import { Support } from "@cossistant/react"; function LaunchChecklistPage() { return
Your custom page
; } export default function App() { return ( ); } ``` `Support.Page` also works with `Support` when you want to keep the default widget and register one extra page. When to stop here [#when-to-stop-here] * `customPages`, `mode="responsive"`, or `Support.Root` give you enough control * you still want the Support router, state, and built-in conversation flow * you do not need a headless build Next step [#next-step] * [Copy & Locale](/docs/support-component/text) if the next change is wording * [Advanced](/docs/advanced) when you want to leave the ready-made widget path and build your own UI # Copy & Locale URL: /docs/support-component/text Change the widget voice the same way you change the widget UI: keep the default runtime and override only what you need. Use this when [#use-this-when] * you want to rename labels or buttons * you need a second language * you want copy that reacts to visitor or app context Smallest working change [#smallest-working-change] ```tsx import { Support } from "@cossistant/react"; ; ``` Built-in locales are `en`, `fr`, and `es`. Fallback order is: explicit `locale`, browser locale, then English. Common variants [#common-variants] Use typed copy inside custom UI [#use-typed-copy-inside-custom-ui] Use `` for markup or `useSupportText()` when you need a string in code. ```tsx import { Text, useSupportText } from "@cossistant/react"; export function AskButton() { const text = useSupportText(); return ( ); } ``` Add a custom locale [#add-a-custom-locale] You can pass any locale code as long as you provide the strings for it. ```tsx ``` When to stop here [#when-to-stop-here] * the default UI is fine and you only need different wording * one or two overrides are enough * your team wants the widget to follow product voice without a custom page Next step [#next-step] * [Hooks Reference](/docs/support-component/hooks) if you need code-level control in custom components * [Types Reference](/docs/support-component/types) when you need the shared widget types behind these APIs Debugging tip [#debugging-tip] Every rendered `` includes `data-key-name="..."` in the DOM, so you can inspect which key is driving a string before you override it. # Match Your Brand URL: /docs/support-component/theme You can make the widget feel like part of your product without replacing any components. Use this when [#use-this-when] * the default widget shape is good and you mainly need branding * you want a copy-paste way to set colors and radius * you want the widget to follow your app theme or force light or dark mode Smallest working change [#smallest-working-change] Set the widget tokens once and keep the default UI. ```css title="src/index.css" .cossistant { --co-theme-primary: #111827; --co-theme-primary-foreground: #ffffff; --co-theme-background: #ffffff; --co-theme-foreground: #111827; --co-theme-border: #e5e7eb; --co-theme-radius: 0px; } ``` That is usually enough to make the widget feel like yours. Common variants [#common-variants] Force light or dark mode [#force-light-or-dark-mode] ```tsx import { Support } from "@cossistant/react"; ; ``` By default, the widget follows your app theme. It checks for `.dark` or `data-color-scheme="dark"` on parent elements. Reuse your shadcn tokens [#reuse-your-shadcn-tokens] ```css :root { --co-theme-background: var(--background); --co-theme-foreground: var(--foreground); --co-theme-primary: var(--primary); --co-theme-primary-foreground: var(--primary-foreground); --co-theme-border: var(--border); --co-theme-radius: var(--radius); } ``` This keeps the widget aligned with the rest of your app instead of inventing a second theme. When to stop here [#when-to-stop-here] * colors, radius, and dark mode are enough * you want the default layout to stay in place * you do not need custom pages or a custom shell Next step [#next-step] * [Pages & Layouts](/docs/support-component/routing) when you need a different first screen, an inline embed, or your own shell * [Copy & Locale](/docs/support-component/text) when the next change is wording instead of UI Core token reference [#core-token-reference] | Variable | Light preview | Default (Light) | Dark preview | Default (Dark) | | ------------------------------- | --------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------- | ------------------ | | `--co-theme-background` | | `oklch(99% 0 0)` | | `oklch(15.5% 0 0)` | | `--co-theme-foreground` | | `oklch(20.5% 0 0)` | | `oklch(95% 0 0)` | | `--co-theme-primary` | | `oklch(14.5% 0 0)` | | `oklch(98.5% 0 0)` | | `--co-theme-primary-foreground` | | `oklch(98.5% 0 0)` | | `oklch(14.5% 0 0)` | | `--co-theme-border` | | `oklch(92.2% 0 0)` | | `oklch(26.9% 0 0)` | | `--co-theme-muted` | | Color-mixed | | Color-mixed | | `--co-theme-muted-foreground` | | Color-mixed | | Color-mixed | | `--co-theme-radius` | - | `0.375rem` | - | `0.375rem` | Extra tokens [#extra-tokens] Use these when the core tokens are not enough: Status colors [#status-colors] | Variable | Light preview | Default (Light) | Dark preview | Default (Dark) | | ------------------------ | ------------------------------------------------- | --------------------------- | ------------------------------------------------- | --------------------------- | | `--co-theme-destructive` | | `oklch(57.7% 0.245 27.325)` | | `oklch(39.6% 0.141 25.723)` | | `--co-theme-success` | | `oklch(71.7% 0.18 142)` | | `oklch(60% 0.15 142)` | | `--co-theme-warning` | | `oklch(86.4% 0.144 99)` | | `oklch(90.3% 0.111 99)` | | `--co-theme-neutral` | | `oklch(60.8% 0 0)` | | `oklch(50% 0 0)` | Avatar accents [#avatar-accents] | Variable | Light preview | Default (Light) | Dark preview | Default (Dark) | | ------------------- | ---------------------------------------------- | ------------------------ | ---------------------------------------------- | ------------------------ | | `--co-theme-pink` | | `oklch(76.3% 0.152 354)` | | `oklch(84.2% 0.109 354)` | | `--co-theme-yellow` | | `oklch(86.4% 0.144 99)` | | `oklch(90.3% 0.111 99)` | | `--co-theme-blue` | | `oklch(72.5% 0.132 241)` | | `oklch(79.8% 0.089 241)` | | `--co-theme-orange` | | `oklch(74.5% 0.166 50)` | | `oklch(68.2% 0.194 50)` | Background shades [#background-shades] | Variable | Light preview | Default (Light) | Dark preview | Default (Dark) | | --------------------------- | --------------------------------------------------------------------------------- | --------------- | --------------------------------------------------------------------------------- | -------------- | | `--co-theme-background-50` | | Color-mixed | | Color-mixed | | `--co-theme-background-100` | | Color-mixed | | Color-mixed | | `--co-theme-background-200` | | Color-mixed | | Color-mixed | | `--co-theme-background-300` | | Color-mixed | | Color-mixed | The background shades are derived from your base colors with `color-mix()` unless you override them directly. # Types Reference URL: /docs/support-component/types Use this page when [#use-this-page-when] Use this page as the shared type index behind the Support docs. * you want the exact shape of a prop, event payload, or shared model * a guide links to a named type like `TimelineItem`, `Conversation`, or `PublicVisitor` * you are building custom UI on top of hooks, events, or primitives If you are still deciding how to shape the widget, start with [Overview](/docs/support-component), [Change One Thing](/docs/support-component/customization), or [Pages & Layouts](/docs/support-component/routing). Widget Types [#widget-types] DefaultMessage [#defaultmessage] Structure for pre-conversation welcome messages. VisitorMetadata [#visitormetadata] Key-value pairs for storing custom data about contacts. SenderType [#sendertype] Enum defining who can send messages. SupportMode [#supportmode] Layout mode for the widget. ```tsx type SupportMode = "floating" | "responsive"; ``` TriggerRenderProps [#triggerrenderprops] Props provided to custom trigger render functions. SupportHandle [#supporthandle] Imperative handle for programmatic widget control via refs. Visitor And Website Data [#visitor-and-website-data] PublicVisitor [#publicvisitor] The visitor object returned by the widget, representing an anonymous or identified visitor. PublicContact [#publiccontact] Contact information for an identified visitor. PublicWebsiteResponse [#publicwebsiteresponse] Website configuration and agent availability information. HumanAgent [#humanagent] Information about a human support agent. AIAgent [#aiagent] Information about an AI support agent. Conversations And Messages [#conversations-and-messages] Conversation [#conversation] Conversation record used throughout the support widget and event payloads. TimelineItem [#timelineitem] Timeline item payload used for widget messages, events, and AI tool output. Hook And Event Types [#hook-and-event-types] IdentifyParams [#identifyparams] Parameters for the `identify()` function. MessageComposer [#messagecomposer] State and actions returned by `useConversationPage` for message composition. SupportEvent [#supportevent] Union type of all possible widget events. Client Types [#client-types] CossistantClient [#cossistantclient] The low-level client instance for advanced programmatic control. The table below highlights the main public stores and methods exposed by the client. # What is Cossistant? URL: /docs/what **Own your support, either use our pre-built `` component, or build your own based on our headless components.** You know how most traditional support systems work: you load an external iframe and code within your app, making it harder to customize, use and even by time are being blocked by your users' ads blocker. This approach worked well until you need to customize your support and more recently, when you're trying to build AI agents that can truly be helpful with custom tools and logic. **You need control over the code to give true power to your agents.** This is what Cossistant aims to solve. It is built around the following principles: * **Open source:** You and the community can participate in making the tool safer and more powerful, no opaque black box. * **Open components:** Following shadcn/ui philosophy, every components used in `` are based on headless primitives available to you. * **Code first:** A single source of truth within your codebase to defined your agents and support behavior * **Beautiful Default:** The default `` comes with a carefully crafted support experience, powerful and beautiful as is * **AI-Ready:** Open code and code first for LLMs to read, understand, and improve.