Chat interface for group-based communication with API-first architecture
npm install hazo_chatA full-featured React chat component library for group-based communication with document sharing, file attachments, and real-time messaging capabilities.
Version 5.2.0 - Added hide_sidebar prop to completely hide the document viewer sidebar for maximized chat area and simpler layouts.
Version 5.1.0 - Logger prop now optional! Simpler integration with zero-config default logger. No need to install hazo_logs or create logger instances for basic use cases.
Version 5.0.0 - Added chat customization props: hide_references, hide_preview, display_mode ('embedded', 'side_panel', 'overlay'), and container_element for flexible rendering modes.
Version 4.0.12 - Added log_polling configuration option (default: false) to suppress routine polling debug logs and reduce console verbosity. Error logs are always shown regardless of this setting.
Version 4.0.11 - Further reduced polling log verbosity: per-request logs now at debug level to eliminate noise during normal operation.
Version 4.0.8 - Fixed excessive API calls caused by polling callback instability and concurrent initial load requests.
Version 4.0.4 - Added read_only prop for view-only mode (hides chat input).
Version 4.0 - Mandatory logging integration with hazo_logs. See Logging Integration section.
Version 3.1 - Generic schema supporting multiple chat patterns: support (client-to-staff), peer (1:1), and group conversations.
Version 3.0 - Introduced group-based chat architecture. Multiple users can participate in a single chat group, perfect for support staff rotating on client sessions.
Version 2.0 introduced API-first architecture with no server-side dependencies in client components.
- 👥 Group-Based Chat - Multiple users can participate in a single chat group
- 🏗️ Multiple Chat Patterns - Support for support (client-to-staff), peer (1:1), and group conversations
- 🔄 Role-Based Access - Support for 'client', 'staff', 'owner', 'admin', and 'member' roles within groups
- 📱 Responsive Design - Works on desktop and mobile with adaptive layout
- 💬 Real-time Messaging - Polling or manual refresh modes for message updates with optimistic UI
- 📎 File Attachments - Support for documents and images with preview
- 📄 Document Viewer - Built-in PDF and image viewer with expand/collapse toggle, download, and open in new tab actions
- 👤 User Profiles - Avatar display and user information
- 🔄 Infinite Scroll - Cursor-based pagination for message history
- ✅ Read Receipts - Automatic mark-as-read when messages become visible using Intersection Observer
- 🗑️ Soft Delete - Delete messages with undo capability
- 🎨 Customizable - TailwindCSS-based theming
- 🚀 API-First - No server-side dependencies in client components
- Installation
- UI Requirements
- Quick Start
- API Routes Setup
- Props Reference
- Hooks
- Types
- Database Schema
- Configuration
- Migration from v1.x
- Development
- License
``bash`
npm install hazo_chat hazo_connect hazo_logs next
hazo_chat is a self-contained component library that includes all necessary UI components. No external UI dependencies are required.
Your tailwind.config.ts must include hazo_chat package paths in the content array. This ensures that Tailwind CSS scans the component library files and includes all utility classes in your final CSS bundle.
Complete Configuration Example:
`typescript
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
// Your application's content paths
'./src/pages/*/.{js,ts,jsx,tsx,mdx}',
'./src/components/*/.{js,ts,jsx,tsx,mdx}',
'./src/app/*/.{js,ts,jsx,tsx,mdx}',
// Add this line to scan the hazo_chat package
'./node_modules/hazo_chat/dist/*/.js',
],
theme: {
extend: {
// Required: Font family configuration for consistent typography
fontFamily: {
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
mono: ['var(--font-mono)', 'monospace'],
},
// Required: Color variables for shadcn/ui compatibility
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)',
card: {
DEFAULT: 'var(--card)',
foreground: 'var(--card-foreground)',
},
popover: {
DEFAULT: 'var(--popover)',
foreground: 'var(--popover-foreground)',
},
primary: {
DEFAULT: 'var(--primary)',
foreground: 'var(--primary-foreground)',
},
secondary: {
DEFAULT: 'var(--secondary)',
foreground: 'var(--secondary-foreground)',
},
muted: {
DEFAULT: 'var(--muted)',
foreground: 'var(--muted-foreground)',
},
accent: {
DEFAULT: 'var(--accent)',
foreground: 'var(--accent-foreground)',
},
destructive: {
DEFAULT: 'var(--destructive)',
foreground: 'var(--destructive-foreground)',
},
border: 'var(--border)',
input: 'var(--input)',
ring: 'var(--ring)',
},
// Required: Border radius configuration
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
// Your additional theme extensions can go here
},
},
plugins: [],
};
export default config;
`
Important Theme Extensions:
The hazo_chat component relies on specific Tailwind theme extensions to ensure consistent styling:
1. Font Families: The font-sans and font-mono utilities must map to CSS variables for consistent typography across all consuming applications.
2. Color Variables: All color utilities (e.g., bg-background, text-foreground, border-border) must map to CSS variables defined in your globals.css.
3. Border Radius: The component uses rounded-lg, rounded-md, and rounded-sm which must map to your CSS --radius variable.
Note: If you're using shadcn/ui in your project, your Tailwind config likely already includes these extensions. Simply add the hazo_chat package path to the content array.
Why This Is Required:
The hazo_chat component library uses Tailwind CSS utility classes (e.g., flex, h-full, bg-background, p-4) for all styling. These classes are not compiled into CSS by default. By adding the package path to your Tailwind content array, Tailwind will:
1. Scan all JavaScript files in the hazo_chat package
2. Extract all Tailwind utility classes used in the components
3. Include them in your application's final CSS bundle
4. Ensure the component styles are correctly applied at runtime
If you're using Tailwind CSS v4, the default content scanning will NOT include classes from this package in node_modules/. You MUST add a @source directive to enable Tailwind to scan this package's files.
Add the following to your globals.css or main CSS file AFTER the tailwindcss import:
`css
@import "tailwindcss";
/ Required: Enable Tailwind to scan hazo_chat package classes /
@source "../node_modules/hazo_chat/dist";
`
Why this is required:
- Tailwind v4 uses JIT compilation and only generates CSS for classes found in scanned files
- By default, files in node_modules/ are NOT scanned@source
- Without the directive, Tailwind utility classes in hazo_chat will have no CSS generated
- This causes invisible hover states, missing colors, broken layouts, etc.
Symptoms of missing @source:
- Hover states appear transparent (e.g., hover:bg-muted/50)text-sm
- Text sizing has no effect (e.g., )rounded-md
- Rounded corners missing (e.g., )
- Colors appear as unstyled defaults
Testing:
Build your application and inspect the generated CSS. Search for classes like hover:bg-muted or text-sm. If they're not present, you need to add the @source directive.
Important: Without this configuration, Tailwind classes used by hazo_chat components will not be compiled, causing styling issues such as:
- Missing styles (components appear unstyled)
- Broken layouts
- Incorrect spacing and colors
- Non-functional responsive breakpoints
Troubleshooting:
If you're experiencing styling issues after adding the configuration:
1. Restart your development server
2. Clear your Next.js cache: rm -rf .nextnode_modules
3. Verify the path matches your locationtailwindcss
4. Check that is installed: npm list tailwindcss
Your global CSS must define these CSS variables (shadcn/ui standard):
Core Colors:
- --background, --foreground--primary
- , --primary-foreground--secondary
- , --secondary-foreground--muted
- , --muted-foreground--accent
- , --accent-foreground--destructive
- , --destructive-foreground
Border and Input:
- --border, --input, --ring
Card:
- --card, --card-foreground
See test-app/src/app/globals.css for complete variable definitions with example values.
This section defines the visual design standards and component behavior specifications for hazo_chat. All consuming projects should follow these standards to ensure consistent UI appearance and behavior.
#### Visual Design Elements
Document Preview Icon:
- Location: Empty state in document viewer (HazoChatDocumentViewer)IoDocumentOutline
- Icon: (from react-icons/io5)w-12 h-12
- Size: (48px × 48px)opacity-50
- Opacity: text-sm
- Text: "Select a document to preview" in
REFERENCES Section Font:
- Location: References section header
- Font size: text-[9px] (9px)font-medium
- Font weight: uppercase
- Text transform: tracking-wider
- Letter spacing: text-muted-foreground
- Color:
Input Area Padding:
- Location: Chat input container (HazoChatInput)p-4
- Padding: (16px on all sides)border-t
- Border: (top border only)bg-background
- Background:
Message Timestamp Display:
- Location: Chat bubble footer (ChatBubble)text-xs
- Font size: text-muted-foreground
- Color: dd/MMM
- Time format: 24-hour format (e.g., "10:37", "15:51")
- Date prefix: Messages before today show date in format (e.g., "02/Dec 10:37")timezone
- Timezone: Respects prop (default: "GMT+10")
Message Status Indicators (Sender's Messages Only):
- Location: Chat bubble footer, after timestamp
- Position: After timestamp with gap-1 spacingh-4 w-4
- Size: (16px × 16px)
Sent Indicator (Grey Single Check):
- Icon: IoCheckmark (from react-icons/io5)text-muted-foreground
- Color: (grey)read_at
- Display condition: Shown when message is sent but is null
- Meaning: Message delivered but not yet read by recipient
Read Receipt (Green Double Check):
- Icon: IoCheckmarkDoneSharp (from react-icons/io5)text-green-500
- Color: (green)read_at
- Display condition: Only shown when is not null
- Meaning: Message has been read by recipient
#### Component Behavior
Close Button:
- Visibility: Always visible when on_close prop is provided to HazoChat componentIoClose
- Icon: (from react-icons/io5)h-8 w-8
- Size: (32px × 32px)ghost
- Variant: hover:bg-destructive/10 hover:text-destructive
- Hover state:
- Position: Top-right of header, after refresh button
Hamburger Menu Button:
- Desktop behavior: Hidden (md:hidden class)IoMenuOutline
- Mobile behavior: Visible on screens < 768px
- Purpose: Toggle document viewer sidebar on mobile
- Icon: (from react-icons/io5)h-8 w-8
- Size: (32px × 32px)
- Position: Top-left of header, before title
Important: If hamburger button appears on desktop, check Tailwind CSS configuration and ensure md: breakpoint utilities are working correctly.
Document Viewer Toggle Button:
- Icon: Chevron (IoChevronBack when expanded, IoChevronForward when collapsed)h-8 w-6
- Size: (32px height × 24px width)outline
- Position: Absolute, vertically centered between columns
- Variant: rounded-r-md rounded-l-none border-l-0
- Border: transition-all duration-300
- Behavior: Smooth transitions with
- Desktop: Visible when document viewer column is present
- Mobile: Hidden when sidebar is closed
References Section Collapse/Expand:
- Collapsed height: max-h-8max-h-96
- Expanded height: transition-all duration-300 ease-in-out
- Transition: IoChevronDown
- Indicator: Chevron icon ( when collapsed, IoChevronUp when expanded)
- Default state: Collapsed when no references, expanded when references exist
Automatic Mark-as-Read:
- Detection: Uses Intersection Observer API to detect when messages become visible
- Trigger threshold: Messages marked as read when 50% visible in the ScrollArea viewport
- Scope: Only marks messages where the current user is the receiver (not the sender)
- State tracking: Prevents duplicate marking using in-memory Set
- API endpoint: Requires PATCH /api/hazo_chat/messages/[id]/read route
- Visual indicator: Green double-checkmark (IoCheckmarkDoneSharp) appears after timestamp
- Automatic: No user action required - messages are marked as read automatically when scrolled into view
Note: The mark-as-read functionality requires:
1. The HazoChat component (includes all hooks and logic)
2. The API route for marking messages as read (see API Routes Setup below)
3. Messages must be received by the current user (messages sent by current user are not marked)
#### Layout Standards
Chat Input Area:
- Layout: Flex container with flex items-center gap-2Input
- Components: Single field and Send button (Button with IoSend icon)p-4
- No attachment buttons: Attachment/image buttons removed for simplified design
- Input padding: on container
- Button alignment: Aligned with input height using flex
Button Alignment and Sizing:
- Send button: Standard button size, aligned with input
- All interactive buttons: Consistent sizing for visual harmony
- Icon sizes within buttons: w-4 h-4 (16px × 16px)
Responsive Breakpoints:
- Mobile: < 768px (default)
- Desktop: >= 768px (md: prefix)
- Standard Tailwind breakpoints used throughout
#### Container Requirements
Important: When wrapping HazoChat in containers (e.g., Card components):
1. Container Height Requirement:
- The HazoChat component uses h-full which requires its parent container to have a defined height.h-[600px]
- Required: Parent container must have a fixed height (e.g., , h-screen, min-h-[500px]).`
- Example:
tsx`
{/ Required: parent must have height /}
- Without a defined height, the component may not render correctly and the chat message area may collapse.
2. Container Width Requirements:
- Recommended minimum width: 500px for optimal two-column layout (document viewer + chat messages).
- For narrow containers (< 500px): Document references will automatically open in a new tab when clicked instead of showing in the preview panel.
- Document viewer defaults to collapsed to maximize chat space. Users can expand it using the toggle button.
- Example:
`tsx`
{/ Recommended: at least 500px width /}
3. Avoid nested overflow-hidden: Nested overflow-hidden containers can clip rounded corners. Use overflow-hidden only on the HazoChat component itself.
4. Padding: If wrapping in a Card, ensure proper padding is maintained. Avoid p-0 on CardContent as it may affect internal spacing.
5. Tailwind Configuration: Ensure ./node_modules/hazo_chat/dist/*/.{js,ts,jsx,tsx} is included in your Tailwind content array so all utility classes (including rounded-* classes) are compiled.
Complete Container Example:
`tsx`
title="Chat"
className="h-full"
/>
#### Typography
Font Families:
- Primary: System font stack or custom font via --font-sans CSS variable--font-mono
- Monospace: System monospace or custom font via CSS variable
Font Sizes:
- Header title: text-sm (14px)text-xs
- Header subtitle: (12px)text-sm
- Message text: (14px)text-xs
- Timestamp: (12px)text-[9px]
- References header: (9px)text-sm
- Empty state text: (14px)
#### Spacing and Padding
Standard Padding Values:
- Container padding: p-4 (16px)px-4
- Header padding: (16px horizontal)px-4 py-2
- Message bubble padding: (16px horizontal, 8px vertical)gap-2
- Gap between elements: (8px) or gap-1 (4px) for tight spacing
#### Animation and Transitions
Standard Transitions:
- Component state changes: transition-all duration-300transition-colors
- Hover states: ease-in-out
- Smooth animations for expand/collapse:
Loading States:
- Skeleton loaders for initial message load
- Spinner animation for refresh button when loading
#### Accessibility
ARIA Labels:
All interactive elements must have appropriate ARIA labels:
- Input fields: aria-label="Message input"aria-label
- Buttons: describing action (e.g., "Send message", "Refresh chat history")aria-label="Close chat"
- Close button: aria-label
- Toggle buttons: and aria-expanded attributes
Keyboard Navigation:
- Send message: Enter key
- Close dialogs: Escape key (handled by Alert Dialog component)
#### Component Dependencies
Required External Components:
The following components must be available in consuming projects:
1. AlertDialog (shadcn/ui style) - Used for user acknowledgment dialogs, not included in hazo_chat package
2. Toaster (from sonner package) - Used for toast notifications, must be added to root layout with position="top-right" and richColors prop
Included Components:
The following UI components are included in hazo_chat package:
- Button, Input, Textarea, Avatar, ScrollArea, Tooltip, Separator, Badge
- ChatBubble (chat-specific), LoadingSkeleton (chat-specific)
Example References:
For complete implementation examples, see:
- test-app/src/app/page.tsx - Usage exampletest-app/src/app/layout.tsx
- - Toaster setuptest-app/src/components/ui/alert-dialog.tsx
- - Alert Dialog implementationtest-app/src/app/globals.css
- - CSS variables exampletest-app/tailwind.config.ts
- - Tailwind configuration example
The component communicates via API calls. Create the required endpoints:
`typescript
// app/api/hazo_chat/messages/route.ts
import { createMessagesHandler } from 'hazo_chat/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
import { createLogger } from 'hazo_logs';
export const dynamic = 'force-dynamic';
const logger = createLogger('hazo_chat');
const { GET, POST } = createMessagesHandler({
getHazoConnect: () => getHazoConnectSingleton(),
getLogger: () => logger
});
export { GET, POST };
`
`tsx
'use client';
import { HazoChat } from 'hazo_chat';
// Simplest usage - logger is optional (v5.1+)
export default function ChatPage() {
return (
That's it! No need to pass database adapters, authentication services, or even a logger - everything works via API calls with sensible defaults.
$3
By default, HazoChat uses an internal logger with standard verbosity. For debugging or custom logging behavior, provide a custom logger:
Default Behavior (No Logger Provided):
- Uses internal logger with standard console output
- All log levels enabled (error, warn, info, debug)
- Suitable for both development and production use
Custom Logger (Advanced):
`tsx
import { HazoChat } from 'hazo_chat';
import { createClientLogger } from 'hazo_logs/ui';// Create custom logger with debug level for detailed logging
const logger = createClientLogger({
packageName: 'hazo_chat',
defaultLevel: 'debug', // See all logs including debug
});
export default function DebugChatPage() {
return (
chat_group_id="group-uuid"
logger={logger} // Custom logger for detailed debugging
title="Chat with Support"
/>
);
}
`Logger Levels:
-
'debug' - All messages (use for development)
- 'info' - Info, warnings, and errors
- 'warn' - Warnings and errors only (default)
- 'error' - Errors onlyAPI Routes Setup
hazo_chat requires these API endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
|
/api/hazo_chat/messages | GET | Fetch chat messages (requires chat_group_id param) |
| /api/hazo_chat/messages | POST | Send a new message (requires chat_group_id in body) |
| /api/hazo_chat/messages/[id]/read | PATCH | Mark a message as read (automatic) |
| /api/hazo_chat/unread_count | GET | Get unread message counts by chat_group_id (optional) |
| /api/hazo_auth/me | GET | Get current authenticated user |
| /api/hazo_auth/profiles | POST | Fetch user profiles by IDs |Breaking Change (v3.0): Messages API now uses
chat_group_id instead of receiver_user_id. All group members can see and send messages.$3
`typescript
// app/api/hazo_chat/messages/route.ts
import { createMessagesHandler } from 'hazo_chat/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
import { createLogger } from 'hazo_logs';export const dynamic = 'force-dynamic';
const logger = createLogger('hazo_chat');
const { GET, POST } = createMessagesHandler({
getHazoConnect: () => getHazoConnectSingleton(),
getLogger: () => logger,
// Optional: custom authentication
getUserIdFromRequest: async (request) => {
// Return user ID from your auth system
return request.cookies.get('user_id')?.value || null;
}
});
export { GET, POST };
``typescript
// app/api/hazo_chat/messages/[id]/read/route.ts
import { NextRequest } from 'next/server';
import { createMarkAsReadHandler } from 'hazo_chat/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
import { createLogger } from 'hazo_logs';export const dynamic = 'force-dynamic';
const logger = createLogger('hazo_chat');
const { PATCH } = createMarkAsReadHandler({
getHazoConnect: () => getHazoConnectSingleton(),
getLogger: () => logger,
// Optional: custom authentication
getUserIdFromRequest: async (request) => {
// Return user ID from your auth system
return request.cookies.get('user_id')?.value || null;
}
});
// Wrapper to handle Next.js App Router params
async function handlePATCH(
request: NextRequest,
context: { params: { id: string } | Promise<{ id: string }> }
) {
return PATCH(request, context);
}
export { handlePATCH as PATCH };
`$3
If you need more control, implement the endpoints manually. See SETUP_CHECKLIST.md for detailed examples.
Library Functions
hazo_chat provides server-side library functions that can be used in API routes, server components, or server actions.
$3
Get unread message counts grouped by chat_group_id for a user.
Purpose: Returns an array of chat group IDs with the count of unread messages (where
read_at is null) for a given user ID. Useful for displaying unread message badges or notifications.Breaking Change (v3.0): Now groups by
chat_group_id instead of reference_id. Accepts optional chat_group_ids filter.Usage:
`typescript
import { createUnreadCountFunction } from 'hazo_chat/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
import { createLogger } from 'hazo_logs';const logger = createLogger('hazo_chat');
// Create the function using the factory
const hazo_chat_get_unread_count = createUnreadCountFunction({
getHazoConnect: () => getHazoConnectSingleton(),
getLogger: () => logger
});
// Use the function
const unreadCounts = await hazo_chat_get_unread_count({
user_id: 'user-id-123',
chat_group_ids: ['group-1', 'group-2'] // Optional: filter by specific groups
});
// Returns: [
// { chat_group_id: 'group-1', count: 5 },
// { chat_group_id: 'group-2', count: 3 }
// ]
`Return Type:
`typescript
interface UnreadCountResult {
chat_group_id: string; // The chat group ID
count: number; // Number of unread messages in this group
}
`Function Behavior:
- Only counts messages where
read_at is null and deleted_at is null
- Only counts messages in groups where the user is a member
- Groups results by chat_group_id
- Optionally filters by specific chat_group_ids if provided
- Sorts results by count (descending - most unread first)
- Returns empty array if no unread messages found
- Returns empty array on errors (doesn't throw)Example: API Route Implementation
`typescript
// app/api/hazo_chat/unread_count/route.ts
import { createUnreadCountFunction } from 'hazo_chat/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
import { NextRequest, NextResponse } from 'next/server';
import { createLogger } from 'hazo_logs';export const dynamic = 'force-dynamic';
const logger = createLogger('hazo_chat');
const hazo_chat_get_unread_count = createUnreadCountFunction({
getHazoConnect: () => getHazoConnectSingleton(),
getLogger: () => logger
});
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const user_id = searchParams.get('user_id');
const chat_group_ids_param = searchParams.get('chat_group_ids');
if (!user_id) {
return NextResponse.json(
{
success: false,
error: 'user_id is required',
unread_counts: []
},
{ status: 400 }
);
}
const chat_group_ids = chat_group_ids_param
? chat_group_ids_param.split(',')
: undefined;
const unread_counts = await hazo_chat_get_unread_count({
user_id,
chat_group_ids
});
return NextResponse.json({
success: true,
user_id,
unread_counts,
total_groups: unread_counts.length,
total_unread: unread_counts.reduce((sum, item) => sum + item.count, 0)
});
} catch (error) {
const error_message = error instanceof Error ? error.message : 'Unknown error';
return NextResponse.json(
{
success: false,
error: error_message,
unread_counts: []
},
{ status: 500 }
);
}
}
`Example: Server Component Usage
`typescript
// app/chat/unread-badge.tsx
import { createUnreadCountFunction } from 'hazo_chat/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
import { createLogger } from 'hazo_logs';const logger = createLogger('hazo_chat');
const hazo_chat_get_unread_count = createUnreadCountFunction({
getHazoConnect: () => getHazoConnectSingleton(),
getLogger: () => logger
});
export default async function UnreadBadge({
receiver_user_id
}: {
receiver_user_id: string
}) {
const unread_counts = await hazo_chat_get_unread_count(receiver_user_id);
const total_unread = unread_counts.reduce((sum, item) => sum + item.count, 0);
if (total_unread === 0) return null;
return (
{unread_counts.map((item) => (
{item.reference_id || 'General'}: {item.count}
))}
Total: {total_unread}
);
}
`Example: Server Action Usage
`typescript
// app/actions/chat.ts
'use server';import { createUnreadCountFunction } from 'hazo_chat/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
const hazo_chat_get_unread_count = createUnreadCountFunction({
getHazoConnect: () => getHazoConnectSingleton()
});
export async function getUnreadCounts(receiver_user_id: string) {
return await hazo_chat_get_unread_count(receiver_user_id);
}
`Props Reference
$3
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
|
chat_group_id | string | ✅ | - | UUID of the chat group (CHANGED from receiver_user_id in v3.0) |
| logger | ClientLogger | ❌ | Internal logger | Logger instance from hazo_logs/ui (optional in v5.1+, required in v4.0-v5.0) |
| read_only | boolean | ❌ | false | When true, hides chat input for view-only mode (NEW in v4.0.4) |
| reference_id | string | ❌ | - | Reference ID for chat context grouping |
| reference_type | string | ❌ | 'chat' | Type of reference |
| api_base_url | string | ❌ | '/api/hazo_chat' | Base URL for API endpoints |
| realtime_mode | 'polling' \| 'manual' | ❌ | 'polling' | Real-time update mode: 'polling' (automatic) or 'manual' (refresh only) |
| polling_interval | number | ❌ | 5000 | Polling interval in ms (only used when realtime_mode = 'polling') |
| messages_per_page | number | ❌ | 20 | Number of messages per page for pagination |
| log_polling | boolean | ❌ | false | Enable polling debug logs (set to true to see polling activity in console) |
| additional_references | ReferenceItem[] | ❌ | [] | Pre-loaded document references |
| timezone | string | ❌ | 'GMT+10' | Timezone for timestamps |
| title | string | ❌ | - | Chat header title |
| subtitle | string | ❌ | - | Chat header subtitle |
| on_close | () => void | ❌ | - | Close button callback |
| show_sidebar_toggle | boolean | ❌ | false | Show sidebar toggle button (hamburger menu) |
| show_delete_button | boolean | ❌ | true | Show delete button on chat bubbles |
| bubble_radius | 'default' \| 'full' | ❌ | 'default' | Bubble border radius style: 'default' (rounded with tail) or 'full' (fully round) |
| hide_references | boolean | ❌ | false | When true, hides the references section at the top of chat (NEW in v5.0.0) |
| hide_sidebar | boolean | ❌ | false | When true, hides the entire document viewer sidebar. References in messages will open in new tab (NEW in v5.2.0) |
| hide_preview | boolean | ❌ | false | When true, hides attachment previews in message bubbles (NEW in v5.0.0) |
| display_mode | 'embedded' \| 'side_panel' \| 'overlay' | ❌ | 'embedded' | Controls how chat is rendered: 'embedded' (inline), 'side_panel' (fixed right panel), or 'overlay' (modal) (NEW in v5.0.0) |
| container_element | HTMLElement \| null | ❌ | null | Custom container for portal rendering (used with side_panel/overlay modes) (NEW in v5.0.0) |
| className | string | ❌ | - | Additional CSS classes |$3
`tsx
// Optional: Create custom logger for debugging
import { createClientLogger } from 'hazo_logs/ui';
const logger = createClientLogger({
packageName: 'hazo_chat',
defaultLevel: 'debug' // See all debug logs
}); chat_group_id="group-123"
logger={logger} // Optional - omit to use default logger
reference_id="project-456"
reference_type="project_chat"
api_base_url="/api/hazo_chat"
realtime_mode="polling" // or "manual" for refresh-only updates
polling_interval={5000} // only used when realtime_mode = "polling"
log_polling={false} // set to true to see polling debug logs (default: false)
timezone="Australia/Sydney"
title="Project Discussion"
subtitle="Design Review"
additional_references={[
{ id: 'doc-1', type: 'document', name: 'Design.pdf', url: '/files/design.pdf', scope: 'field' }
]}
on_close={() => console.log('Chat closed')}
read_only={false} // set to true for view-only mode
className="h-[600px]"
/>
`Customization
hazo_chat provides props to customize component appearance and behavior without needing CSS overrides. This eliminates the need for extensive CSS customizations in consuming projects.
$3
#### Read-Only Mode (View Only)
To display chat messages without allowing users to send new messages:
`tsx
chat_group_id="group-123"
logger={logger}
read_only={true} // Hides chat input - view only
/>
`Use cases:
- Displaying archived conversations
- Showing chat history to non-participants
- Read-only audit views
#### Hide Sidebar Toggle Button (Hamburger Menu)
By default, the sidebar toggle button is hidden (
show_sidebar_toggle={false}). To show it:`tsx
chat_group_id="group-123"
logger={logger}
show_sidebar_toggle={true} // Show hamburger menu button
/>
`#### Hide Delete Button on Chat Bubbles
To hide the delete button on chat bubbles:
`tsx
chat_group_id="group-123"
logger={logger}
show_delete_button={false} // Hide delete button
/>
`#### Hide References Section (NEW in v5.0.0)
To hide the references section at the top of the chat (documents and links attached to messages):
`tsx
chat_group_id="group-123"
logger={logger}
hide_references={true} // Hides references section
/>
`Use cases:
- Cleaner interface when attachments are not needed
- Mobile views with limited space
- Tax forms or minimal chat interfaces
#### Hide Document Viewer Sidebar (NEW in v5.2.0)
To hide the entire document viewer sidebar (document preview panel on the left):
`tsx
chat_group_id="group-123"
logger={logger}
hide_sidebar={true} // Hides document viewer sidebar
/>
`When
hide_sidebar is enabled:
- The document viewer panel is completely hidden
- The document viewer toggle button is hidden
- Chat messages expand to full width
- Document references in messages will open in a new tab instead of the preview panelUse cases:
- Maximizing chat message area on small screens
- Simple chat-only interfaces without document previews
- When documents should always open externally
- Embedded chat widgets with limited space
Combination example - Hide references section but keep document viewer:
`tsx
chat_group_id="group-123"
logger={logger}
hide_references={true} // Hide references list
hide_sidebar={false} // Keep document viewer (default)
/>
`Combination example - Show references but hide document viewer:
`tsx
chat_group_id="group-123"
logger={logger}
hide_references={false} // Show references list (default)
hide_sidebar={true} // Hide document viewer
/>
`In this mode, users can still see the references list at the top, but clicking on them will open in a new tab instead of the sidebar preview.
#### Hide Attachment Previews (NEW in v5.0.0)
To hide attachment previews in message bubbles:
`tsx
chat_group_id="group-123"
logger={logger}
hide_preview={true} // Hides attachment icons in bubbles
/>
`Use cases:
- Compact chat layouts
- Mobile views to save space
- When document links should not be clickable from messages
#### Display Modes (NEW in v5.0.0)
The
display_mode prop controls how the chat is rendered:Embedded Mode (Default)
Chat renders inline within its parent container:
`tsx
chat_group_id="group-123"
logger={logger}
display_mode="embedded" // Default - inline rendering
/>
`Side Panel Mode
Chat appears as a fixed panel on the right side of the viewport:
`tsx
chat_group_id="group-123"
logger={logger}
display_mode="side_panel"
on_close={() => setShowChat(false)} // Required for close button
/>
`Features:
- Fixed position on right side
- Slides in with animation
- Close button in header
- ESC key to close
- Uses React Portal (renders at document.body)
Overlay Mode
Chat appears as a modal overlay centered on the page:
`tsx
chat_group_id="group-123"
logger={logger}
display_mode="overlay"
on_close={() => setShowChat(false)} // Required for close button
/>
`Features:
- Semi-transparent backdrop
- Centered modal
- Close button in header
- ESC key to close
- Click backdrop to close
- Uses React Portal (renders at document.body)
Custom Portal Container
Specify a custom container for portal rendering (useful with iframes or shadow DOM):
`tsx
const containerRef = useRef(null); chat_group_id="group-123"
logger={logger}
display_mode="overlay"
container_element={containerRef.current}
on_close={() => setShowChat(false)}
/>
`#### Make Chat Bubbles Fully Round
To make all chat bubbles fully round (instead of the default style with a tail):
`tsx
chat_group_id="group-123"
logger={logger}
bubble_radius="full" // Fully round all corners
/>
`$3
| Prop | Default | Options | Description |
|------|--------|---------|------------|
|
read_only | false | boolean | Hide chat input for view-only mode |
| show_sidebar_toggle | false | boolean | Show/hide the hamburger menu button |
| show_delete_button | true | boolean | Show/hide delete button on chat bubbles |
| bubble_radius | 'default' | 'default' \| 'full' | Bubble border radius style |
| hide_references | false | boolean | Hide references section at top |
| hide_sidebar | false | boolean | Hide document viewer sidebar completely |
| hide_preview | false | boolean | Hide attachment previews in message bubbles |
| display_mode | 'embedded' | 'embedded' \| 'side_panel' \| 'overlay' | Chat rendering mode |$3
`tsx
chat_group_id="group-123"
logger={logger}
reference_id="project-456"
read_only={false} // Set true for view-only mode
show_sidebar_toggle={false} // Hide hamburger menu
show_delete_button={false} // Hide delete buttons
bubble_radius="full" // Fully round bubbles
title="Project Chat"
className="h-[600px]"
/>
`$3
Using props instead of CSS overrides provides:
- Type safety: TypeScript will catch invalid values
- Consistency: Ensures all instances use the same styling
- Maintainability: Easier to update across the codebase
- No CSS conflicts: Avoids specificity issues with Tailwind utilities
If you need more advanced styling that isn't covered by props, you can still use CSS overrides, but props should cover most common customization needs.
Checking for Messages
If you need to check whether messages exist for a given reference (without loading all messages), you can call the messages API endpoint directly. This is useful for showing indicators or badges.
Important: The API requires
receiver_user_id as a query parameter and returns an object response, not a direct array.$3
`typescript
// ✅ CORRECT: Include receiver_user_id and handle object response
async function checkMessagesExist(
recipient_user_id: string,
reference_id: string,
reference_type?: string
): Promise {
const params = new URLSearchParams({
receiver_user_id: recipient_user_id, // ✅ Required parameter
reference_id,
...(reference_type && { reference_type }),
}); const response = await fetch(
/api/hazo_chat/messages?${params.toString()}, {
credentials: 'include'
}); if (!response.ok) {
return false;
}
const data = await response.json();
// ✅ Handle object response format: { success: true, messages: [], current_user_id }
const messages = data.messages || data; // Handle both object and array responses
const has_messages_result = Array.isArray(messages) && messages.length > 0;
return has_messages_result;
}
`$3
`typescript
// ❌ WRONG: Missing receiver_user_id parameter
const params = new URLSearchParams({
reference_id,
...(reference_type && { reference_type }),
});
// This will return a 400 error: "receiver_user_id is required"// ❌ WRONG: Expecting direct array response
const data = await response.json();
const has_messages_result = Array.isArray(data) && data.length > 0;
// This fails because API returns: { success: true, messages: [], current_user_id }
`$3
`typescript
'use client';import { useState, useEffect } from 'react';
function useChatMessagesCheck(
recipient_user_id: string,
reference_id: string,
reference_type?: string
) {
const [has_messages, set_has_messages] = useState(false);
const [is_checking, set_is_checking] = useState(true);
useEffect(() => {
async function check() {
if (!recipient_user_id || !reference_id) {
set_is_checking(false);
return;
}
try {
const params = new URLSearchParams({
receiver_user_id: recipient_user_id, // ✅ Required
reference_id,
...(reference_type && { reference_type }),
});
const response = await fetch(
/api/hazo_chat/messages?${params.toString()},
{ credentials: 'include' }
); if (response.ok) {
const data = await response.json();
const messages = data.messages || data; // ✅ Handle object response
set_has_messages(Array.isArray(messages) && messages.length > 0);
}
} catch (error) {
console.error('[useChatMessagesCheck] Error:', error);
set_has_messages(false);
} finally {
set_is_checking(false);
}
}
check();
}, [recipient_user_id, reference_id, reference_type]);
return { has_messages, is_checking };
}
`Hooks
$3
Manages chat messages with pagination, polling, and CRUD operations.
`tsx
import { useChatMessages } from 'hazo_chat';const {
messages, // ChatMessage[] - All loaded messages
is_loading, // boolean - Initial loading state
is_loading_more, // boolean - Pagination loading state
has_more, // boolean - More messages available
error, // string | null - Error message
polling_status, // 'connected' | 'reconnecting' | 'error'
load_more, // () => void - Load older messages
send_message, // (payload) => Promise
delete_message, // (message_id) => Promise
mark_as_read, // (message_id) => Promise
refresh, // () => void - Reload messages
} = useChatMessages({
chat_group_id: 'group-456', // CHANGED from receiver_user_id in v3.0
reference_id: 'chat-123',
reference_type: 'direct',
api_base_url: '/api/hazo_chat',
realtime_mode: 'polling', // Optional: 'polling' (automatic) or 'manual' (refresh only), default: 'polling'
polling_interval: 5000, // Optional, default: 5000ms (only used when realtime_mode = 'polling')
messages_per_page: 20, // Optional, default: 20
log_polling: false, // Optional, default: false (set to true to see polling debug logs)
});
`$3
Manages document references across messages and props.
`tsx
import { useChatReferences } from 'hazo_chat';const {
references, // ChatReferenceItem[] - All references
selected_reference, // ChatReferenceItem | null
select_reference, // (ref) => void
clear_selection, // () => void
add_reference, // (ref) => void
get_message_for_reference, // (ref_id) => string | null
} = useChatReferences({
messages,
initial_references: [],
on_selection_change: (ref) => console.log('Selected:', ref),
});
`$3
Handles file validation, preview, and upload.
`tsx
import { useFileUpload } from 'hazo_chat';const {
pending_attachments, // PendingAttachment[]
add_files, // (files: File[]) => void
remove_file, // (id: string) => void
upload_all, // () => Promise
clear_all, // () => void
is_uploading, // boolean
validation_errors, // string[]
} = useFileUpload({
upload_location: '/api/hazo_chat/uploads',
max_file_size_mb: 10, // Optional, default: 10
allowed_types: ['pdf', 'png', 'jpg'], // Optional
});
`Types
$3
`typescript
interface ChatMessage {
id: string;
reference_id: string;
reference_type: string;
sender_user_id: string;
chat_group_id: string; // CHANGED from receiver_user_id in v3.0
message_text: string | null;
reference_list: ChatReferenceItem[] | null;
read_at: string | null;
deleted_at: string | null;
created_at: string;
changed_at: string;
sender_profile?: HazoUserProfile;
// receiver_profile removed in v3.0
is_sender: boolean;
send_status?: 'sending' | 'sent' | 'failed';
}
`$3
`typescript
interface HazoUserProfile {
id: string;
name: string;
email?: string;
avatar_url?: string;
}
`$3
`typescript
interface ChatReferenceItem {
id: string;
type: 'document' | 'field' | 'url';
scope: 'chat' | 'field';
name: string;
url: string;
mime_type?: string;
file_size?: number;
message_id?: string;
}
`Note on MIME Type: The
mime_type property is optional. If not provided, the component will automatically infer the MIME type from the file extension (e.g., .jpg → image/jpeg, .pdf → application/pdf). This ensures document preview works even when mime_type is not explicitly set. Supported file extensions for inference include: pdf, png, jpg, jpeg, gif, webp, txt, doc, docx.$3
`typescript
interface ChatGroup {
id: string;
client_user_id: string | null | undefined; // NULLABLE in v3.1 (only required for 'support' groups)
group_type: ChatGroupType; // NEW in v3.1
name?: string | null;
created_at: string;
changed_at: string;
}
`$3
`typescript
type ChatGroupType = 'support' | 'peer' | 'group';
`Group Type Definitions:
-
'support': Client-to-staff support conversation (requires client_user_id)
- 'peer': Peer-to-peer direct message between two users
- 'group': Multi-user group conversation$3
`typescript
interface ChatGroupUser {
chat_group_id: string;
user_id: string;
role: ChatGroupUserRole; // EXPANDED in v3.1
created_at: string;
changed_at: string;
}
`$3
`typescript
// v3.0
type ChatGroupUserRole = 'client' | 'staff';// v3.1 (expanded)
type ChatGroupUserRole = 'client' | 'staff' | 'owner' | 'admin' | 'member';
`Role Definitions:
-
'client': Customer/end-user in support scenarios
- 'staff': Support personnel in support scenarios
- 'owner': Creator/owner of peer or group chats
- 'admin': Delegated administrator in group chats
- 'member': Standard participant in peer or group chats$3
`typescript
interface ChatGroupWithMembers extends ChatGroup {
members: (ChatGroupUser & { profile?: HazoUserProfile })[];
owner_profile?: HazoUserProfile; // NEW in v3.1 - profile of the group owner
}
`Database Schema
Breaking Changes in v3.0: The database schema has been updated to support group-based chat. Migration required.
New in v3.1: Generic schema supporting multiple chat patterns - support, peer, and group conversations.
$3
For PostgreSQL installations, create these custom enum types for type safety and consistency:
`sql
-- Reference type for chat contexts
CREATE TYPE hazo_enum_chat_type AS ENUM ('chat', 'field', 'project', 'support', 'general');-- Group type for conversation patterns (v3.1)
CREATE TYPE hazo_enum_group_type AS ENUM ('support', 'peer', 'group');
-- Membership roles (v3.1)
CREATE TYPE hazo_enum_group_role AS ENUM ('client', 'staff', 'owner', 'admin', 'member');
`$3
hazo_chat supports three distinct group types to accommodate different conversation patterns:
| Type | Description | Use Case |
client_user_id | Typical Roles |
|------|-------------|----------|------------------|---------------|
| support | Client-to-staff support conversation | Customer support, helpdesk | Required (identifies the client) | 'client', 'staff' |
| peer | Peer-to-peer direct message (1:1) | Direct messaging between equals | Not used (null) | 'owner', 'member' |
| group | Multi-user group conversation | Team collaboration, channels | Not used (null) | 'owner', 'admin', 'member' |$3
The
role field in hazo_chat_group_users defines each member's permissions and relationship to the group:| Role | Description | Used In | Capabilities |
|------|-------------|---------|--------------|
| client | Customer/end-user receiving support | support groups | Send messages, view messages |
| staff | Support personnel helping clients | support groups | Send messages, view messages, assist client |
| owner | Creator/owner of the conversation | peer, group | Full control, can add/remove members |
| admin | Delegated administrator | group | Can manage members, moderate content |
| member | Standard participant | peer, group | Send messages, view messages |
$3
`sql
-- PostgreSQL with enums (RECOMMENDED)
CREATE TABLE hazo_chat_group (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_user_id UUID REFERENCES hazo_users(id), -- NULLABLE in v3.1
group_type hazo_enum_group_type NOT NULL DEFAULT 'support', -- NEW in v3.1
name VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);-- PostgreSQL without enums (alternative)
CREATE TABLE hazo_chat_group (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_user_id UUID REFERENCES hazo_users(id), -- NULLABLE in v3.1
group_type VARCHAR(20) NOT NULL DEFAULT 'support' CHECK (group_type IN ('support', 'peer', 'group')), -- NEW in v3.1
name VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hazo_chat_group_client ON hazo_chat_group(client_user_id);
CREATE INDEX idx_hazo_chat_group_type ON hazo_chat_group(group_type); -- NEW in v3.1
`Column Changes in v3.1:
-
client_user_id: Changed from NOT NULL to nullable - only required for 'support' type groups
- group_type: NEW field - defines the conversation pattern ('support', 'peer', 'group')$3
`sql
-- PostgreSQL with enums (RECOMMENDED)
CREATE TABLE hazo_chat_group_users (
chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
role hazo_enum_group_role NOT NULL, -- EXPANDED in v3.1
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (chat_group_id, user_id)
);-- PostgreSQL without enums (alternative)
CREATE TABLE hazo_chat_group_users (
chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL CHECK (role IN ('client', 'staff', 'owner', 'admin', 'member')), -- EXPANDED in v3.1
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (chat_group_id, user_id)
);
CREATE INDEX idx_hazo_chat_group_users_user ON hazo_chat_group_users(user_id);
CREATE INDEX idx_hazo_chat_group_users_group ON hazo_chat_group_users(chat_group_id);
CREATE INDEX idx_hazo_chat_group_users_role ON hazo_chat_group_users(role); -- NEW in v3.1
`Column Changes in v3.1:
-
role: EXPANDED from ('client', 'staff') to ('client', 'staff', 'owner', 'admin', 'member')$3
`sql
-- PostgreSQL
CREATE TABLE hazo_chat (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reference_id UUID NOT NULL,
reference_type TEXT DEFAULT 'chat',
sender_user_id UUID NOT NULL REFERENCES hazo_users(id),
chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id), -- CHANGED from receiver_user_id
message_text TEXT,
reference_list JSONB, -- JSON array of ChatReferenceItem
read_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);-- Indexes for performance
CREATE INDEX idx_hazo_chat_reference_id ON hazo_chat(reference_id);
CREATE INDEX idx_hazo_chat_sender ON hazo_chat(sender_user_id);
CREATE INDEX idx_hazo_chat_group ON hazo_chat(chat_group_id); -- CHANGED from receiver index
CREATE INDEX idx_hazo_chat_created ON hazo_chat(created_at DESC);
`Migration Notes:
- Rename column:
receiver_user_id → chat_group_id
- Create new tables: hazo_chat_group, hazo_chat_group_users
- Migrate existing 1-1 chats to groups with both users as members
- Update foreign key constraintsUI Behavior & Responsive Design
$3
The hamburger menu button (☰) appears in the chat header only on mobile devices (screens < 768px wide) and is used to toggle the document viewer sidebar.
Desktop Behavior:
- The hamburger button is hidden (
md:hidden class)
- The document viewer is always visible as a left column
- Use the expand/collapse toggle button (chevron) between the document viewer and chat area to show/hide the document viewerMobile Behavior:
- The hamburger button is visible to toggle the document viewer overlay
- The document viewer slides in from the left as an overlay when opened
- Click the hamburger button or backdrop to toggle the sidebar
Important: If you see the hamburger button on desktop, it may indicate:
1. TailwindCSS classes are not being compiled correctly in your project
2. The
md: breakpoint utilities are not available in your Tailwind config
3. Ensure tailwindcss is properly installed and configured in your consuming project$3
The document viewer column can be collapsed/expanded using a toggle button (chevron icon) positioned between the document viewer and chat area. This button:
- Appears on desktop when the document viewer is visible
- Allows you to collapse the document viewer to maximize chat space
- Automatically positions itself at the edge of the expanded/collapsed viewer
$3
The chat input area includes:
- File attachment button (left side)
- Image attachment button
- Auto-resizing text input
- Send button (aligned with textarea height)
All buttons are sized consistently (
h-10 w-10) to align properly with the text input area.Configuration
$3
Configuration files are stored in the
config/ directory. Consuming applications should also use a config/ directory to store their configuration files for consistency across hazo packages.`ini
[chat]
Real-time update mode: "polling" (automatic) or "manual" (refresh only)
polling: Automatically checks for new messages at the specified interval
manual: Only updates when user manually refreshes (via refresh button)
realtime_mode = pollingPolling interval in milliseconds (only used when realtime_mode = polling)
polling_interval = 5000Messages to load per page
messages_per_page = 20Enable polling debug logs (default: false to reduce console verbosity)
log_polling = false[uploads]
Maximum file size in MB
max_file_size_mb = 10Allowed file extensions (comma-separated)
allowed_types = pdf,png,jpg,jpeg,gif,txt,doc,docx
`Migration from v5.0 to v5.1 (Logger Now Optional)
Version 5.1 makes the
logger prop optional for easier integration. This is a non-breaking change - existing code continues to work.$3
| v5.0 | v5.1 |
|------|------|
|
logger prop required | logger prop optional |
| Must install hazo_logs and create logger | Can use default logger |
| Manual logger setup for every usage | Zero-config for simple use cases |$3
Good News: The
logger prop is now optional! You can remove logger setup code for simpler integration.Before (v5.0):
`tsx
import { HazoChat } from 'hazo_chat';
import { createClientLogger } from 'hazo_logs/ui';// Required logger setup in v5.0
const logger = createClientLogger({ packageName: 'hazo_chat' });
export default function ChatPage() {
return (
chat_group_id="group-123"
logger={logger} // Required in v5.0
/>
);
}
`After (v5.1 - Simplest):
`tsx
import { HazoChat } from 'hazo_chat';// No logger needed - uses default
export default function ChatPage() {
return (
);
}
`After (v5.1 - Custom Logger):
`tsx
import { HazoChat } from 'hazo_chat';
import { createClientLogger } from 'hazo_logs/ui';// Optional: Keep custom logger for debugging
const logger = createClientLogger({
packageName: 'hazo_chat',
defaultLevel: 'debug'
});
export default function ChatPage() {
return (
chat_group_id="group-123"
logger={logger} // Optional - omit to use default
/>
);
}
`$3
- Simpler integration: Remove logger setup boilerplate for basic use cases
- Backward compatible: Existing code with
logger prop continues to work
- Flexible: Custom logger still available when needed for debugging
- Zero-config: Works out of the box without logger setup$3
v5.1 is fully backward compatible with v5.0:
- Existing code that provides
logger prop works unchanged
- Default logger automatically used when logger is omitted
- No schema changes, no API changesMigration from v3.0 to v3.1
Version 3.1 introduces schema changes to support multiple chat patterns (support, peer, group) while maintaining backward compatibility.
$3
| v3.0 | v3.1 |
|------|------|
| Fixed support pattern only | Three patterns: support, peer, group |
|
client_user_id always required | client_user_id nullable (only for support groups) |
| 2 roles: 'client', 'staff' | 5 roles: 'client', 'staff', 'owner', 'admin', 'member' |
| No group_type field | NEW group_type field ('support', 'peer', 'group') |$3
Important: v3.1 is backward compatible with v3.0 schema. Existing support-type groups continue to work without changes.
1. Create PostgreSQL Enum Types (Optional but Recommended):
`sql
-- Reference type for chat contexts
CREATE TYPE hazo_enum_chat_type AS ENUM ('chat', 'field', 'project', 'support', 'general');-- Group type for conversation patterns
CREATE TYPE hazo_enum_group_type AS ENUM ('support', 'peer', 'group');
-- Membership roles
CREATE TYPE hazo_enum_group_role AS ENUM ('client', 'staff', 'owner', 'admin', 'member');
`2. Add group_type Column to hazo_chat_group:
`sql
-- Add group_type column (defaults to 'support' for backward compatibility)
ALTER TABLE hazo_chat_group
ADD COLUMN group_type VARCHAR(20) NOT NULL DEFAULT 'support'
CHECK (group_type IN ('support', 'peer', 'group'));-- Or with enum type (if you created the enum):
ALTER TABLE hazo_chat_group
ADD COLUMN group_type hazo_enum_group_type NOT NULL DEFAULT 'support';
-- Add index for group_type
CREATE INDEX idx_hazo_chat_group_type ON hazo_chat_group(group_type);
`3. Make client_user_id Nullable:
`sql
-- Remove NOT NULL constraint from client_user_id
ALTER TABLE hazo_chat_group
ALTER COLUMN client_user_id DROP NOT NULL;
`4. Update Role Constraint in hazo_chat_group_users:
`sql
-- Drop old constraint
ALTER TABLE hazo_chat_group_users
DROP CONSTRAINT IF EXISTS hazo_chat_group_users_role_check;-- Add new constraint with expanded roles
ALTER TABLE hazo_chat_group_users
ADD CONSTRAINT hazo_chat_group_users_role_check
CHECK (role IN ('client', 'staff', 'owner', 'admin', 'member'));
-- Or with enum type (if you created the enum):
ALTER TABLE hazo_chat_group_users
ALTER COLUMN role TYPE hazo_enum_group_role
USING role::hazo_enum_group_role;
-- Add index for role
CREATE INDEX idx_hazo_chat_group_users_role ON hazo_chat_group_users(role);
`5. Verify Existing Data:
`sql
-- All existing groups should have group_type = 'support'
SELECT group_type, COUNT(*)
FROM hazo_chat_group
GROUP BY group_type;-- All existing members should have role IN ('client', 'staff')
SELECT role, COUNT(*)
FROM hazo_chat_group_users
GROUP BY role;
`$3
After migration, you can create peer and group conversations:
`typescript
// Support group (v3.0 style - still works)
await db.insert_with_result('hazo_chat_group', {
client_user_id: 'client-uuid',
group_type: 'support',
name: 'Customer Support'
});// Peer-to-peer chat (v3.1)
await db.insert_with_result('hazo_chat_group', {
client_user_id: null, // Not used for peer chats
group_type: 'peer',
name: 'Alice & Bob'
});
// Group conversation (v3.1)
await db.insert_with_result('hazo_chat_group', {
client_user_id: null, // Not used for group chats
group_type: 'group',
name: 'Project Team'
});
`$3
The v3.1 schema changes are transparent to the component API. Your existing code continues to work:
`tsx
// This works for all group types (support, peer, group)
chat_group_id="group-123"
reference_id="chat-456"
reference_type="support"
/>
`$3
If you use TypeScript types from hazo_chat:
`typescript
// v3.1 types (automatically available after updating package)
import type {
ChatGroup, // client_user_id is now optional
ChatGroupType, // 'support' | 'peer' | 'group'
ChatGroupUserRole // 'client' | 'staff' | 'owner' | 'admin' | 'member'
} from 'hazo_chat';const group: ChatGroup = {
id: 'group-123',
client_user_id: null, // ✅ Now optional
group_type: 'peer', // ✅ New field
name: 'Chat',
created_at: new Date().toISOString(),
changed_at: new Date().toISOString()
};
`Migration from v2.x to v3.0
Version 3.0 introduces breaking changes to support group-based chat architecture.
$3
| v2.x | v3.0 |
|------|------|
|
receiver_user_id prop | chat_group_id prop |
| 1-1 chat only | Group-based chat with multiple participants |
| receiver_user_id in API calls | chat_group_id in API calls |
| Single hazo_chat table | Three tables: hazo_chat_group, hazo_chat_group_users, hazo_chat (modified) |
| Unread count by reference_id | Unread count by chat_group_id |
| ChatMessage.receiver_profile field | Field removed |$3
1. Database Migration:
`sql
-- Step 1: Create new tables
CREATE TABLE hazo_chat_group (...); -- See Database Schema section
CREATE TABLE hazo_chat_group_users (...);-- Step 2: Migrate existing data (example)
-- For each unique sender-receiver pair, create a chat group
INSERT INTO hazo_chat_group (id, client_user_id, name)
SELECT
gen_random_uuid(),
receiver_user_id,
'Migrated Chat ' || sender_user_id
FROM hazo_chat
GROUP BY sender_user_id, receiver_user_id;
-- Step 3: Rename column
ALTER TABLE hazo_chat RENAME COLUMN receiver_user_id TO chat_group_id;
-- Step 4: Update foreign keys
ALTER TABLE hazo_chat
ADD CONSTRAINT fk_chat_group
FOREIGN KEY (chat_group_id)
REFERENCES hazo_chat_group(id);
`2. Update Component Props:
`tsx
// Before (v2.x)
receiver_user_id="user-123"
reference_id="chat-456"
/>// After (v3.0)
chat_group_id="group-123"
reference_id="chat-456"
/>
`3. Update API Calls:
`typescript
// Before (v2.x)
const response = await fetch(
/api/hazo_chat/messages?receiver_user_id=${userId}
);// After (v3.0)
const response = await fetch(
/api/hazo_chat/messages?chat_group_id=${groupId}
);
`4. Update Type References:
`typescript
// Remove receiver_profile usage
// message.receiver_profile → removed// Update CreateMessagePayload
// receiver_user_id → chat_group_id
``The v2.x architecture supported only 1-1 communication. This was limiting for scenarios where:
- Multiple support staff need to rotate on a single client chat
- Team collaboration is required within a chat context
- Client needs to see all staff responses in one unified thread
The v3.0 group-based architecture solves these by:
- Supporting multiple users in a single chat group
- Role-based access (client vs staff)
- Unified message history for all