# 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
Add Cossistant
{``}
widget to your Next.js project.
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 (
);
}
```
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 (
);
}
```
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.