hazo_chat 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +182 -67
  2. package/SETUP_CHECKLIST.md +773 -67
  3. package/dist/api/index.d.ts +17 -2
  4. package/dist/api/index.d.ts.map +1 -1
  5. package/dist/api/index.js +16 -1
  6. package/dist/api/index.js.map +1 -1
  7. package/dist/api/messages.d.ts +34 -1
  8. package/dist/api/messages.d.ts.map +1 -1
  9. package/dist/api/messages.js +340 -47
  10. package/dist/api/messages.js.map +1 -1
  11. package/dist/api/types.d.ts +50 -2
  12. package/dist/api/types.d.ts.map +1 -1
  13. package/dist/api/unread_count.d.ts +19 -10
  14. package/dist/api/unread_count.d.ts.map +1 -1
  15. package/dist/api/unread_count.js +54 -30
  16. package/dist/api/unread_count.js.map +1 -1
  17. package/dist/components/hazo_chat/hazo_chat.d.ts.map +1 -1
  18. package/dist/components/hazo_chat/hazo_chat.js +23 -15
  19. package/dist/components/hazo_chat/hazo_chat.js.map +1 -1
  20. package/dist/components/hazo_chat/hazo_chat_header.d.ts.map +1 -1
  21. package/dist/components/hazo_chat/hazo_chat_header.js +17 -4
  22. package/dist/components/hazo_chat/hazo_chat_header.js.map +1 -1
  23. package/dist/components/hazo_chat/hazo_chat_messages.d.ts +5 -4
  24. package/dist/components/hazo_chat/hazo_chat_messages.d.ts.map +1 -1
  25. package/dist/components/hazo_chat/hazo_chat_messages.js +48 -8
  26. package/dist/components/hazo_chat/hazo_chat_messages.js.map +1 -1
  27. package/dist/hooks/use_chat_messages.d.ts +5 -5
  28. package/dist/hooks/use_chat_messages.d.ts.map +1 -1
  29. package/dist/hooks/use_chat_messages.js +247 -148
  30. package/dist/hooks/use_chat_messages.js.map +1 -1
  31. package/dist/types/index.d.ts +162 -7
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/package.json +1 -1
@@ -1,7 +1,9 @@
1
- # hazo_chat Setup Checklist (v2.0)
1
+ # hazo_chat Setup Checklist (v3.0)
2
2
 
3
3
  A comprehensive, step-by-step guide for setting up hazo_chat in a Next.js project. This checklist is designed for both AI assistants and human developers.
4
4
 
5
+ **Version 3.0 Changes:** This version introduces group-based chat architecture. See [Migration from v2.x](#migration-from-v2x-to-v30) section for upgrade instructions.
6
+
5
7
  ---
6
8
 
7
9
  ## Table of Contents
@@ -16,6 +18,8 @@ A comprehensive, step-by-step guide for setting up hazo_chat in a Next.js projec
16
18
  8. [UI Design Standards Compliance](#8-ui-design-standards-compliance)
17
19
  9. [Verification Checklist](#9-verification-checklist)
18
20
  10. [Troubleshooting](#10-troubleshooting)
21
+ 11. [Migration from v2.x to v3.0](#migration-from-v2x-to-v30)
22
+ 12. [Migration from v3.0 to v3.1](#migration-from-v30-to-v31-generic-schema)
19
23
 
20
24
  ---
21
25
 
@@ -56,52 +60,426 @@ npm install react react-dom next
56
60
 
57
61
  ## 3. Database Setup
58
62
 
59
- ### Step 3.1: Create hazo_chat Table
63
+ ### Step 3.1: PostgreSQL Setup (Recommended)
64
+
65
+ For PostgreSQL databases, follow these steps to create the schema with UUID types, enums, and proper defaults.
66
+
67
+ #### Step 3.1.1: Enable UUID Extension
68
+
69
+ First, enable the UUID extension for generating UUIDs:
70
+
71
+ ```sql
72
+ -- Enable UUID extension (required for gen_random_uuid())
73
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
74
+ -- OR for PostgreSQL 13+, use pgcrypto extension
75
+ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
76
+ ```
77
+
78
+ #### Step 3.1.2: Create Enum Types (Recommended)
79
+
80
+ Create enum types for data integrity. All enums are prefixed with `hazo_enum_`:
81
+
82
+ ```sql
83
+ -- ============================================================================
84
+ -- ENUM TYPE DEFINITIONS (PostgreSQL)
85
+ -- ============================================================================
86
+
87
+ -- 1. Reference type enum - for categorizing chat contexts
88
+ CREATE TYPE hazo_enum_chat_type AS ENUM (
89
+ 'chat', -- General chat
90
+ 'field', -- Form field reference
91
+ 'project', -- Project-related
92
+ 'support', -- Support ticket
93
+ 'general' -- General purpose
94
+ );
95
+
96
+ -- 2. Group type enum (v3.1) - for identifying conversation patterns
97
+ CREATE TYPE hazo_enum_group_type AS ENUM (
98
+ 'support', -- Client-to-staff support conversation
99
+ 'peer', -- Peer-to-peer direct message (1:1)
100
+ 'group' -- Multi-user group conversation
101
+ );
102
+
103
+ -- 3. Group user role enum (v3.1) - for membership roles
104
+ CREATE TYPE hazo_enum_group_role AS ENUM (
105
+ 'client', -- Customer/end-user in support scenarios
106
+ 'staff', -- Support personnel in support scenarios
107
+ 'owner', -- Creator of peer/group chats
108
+ 'admin', -- Delegated administrator in group chats
109
+ 'member' -- Standard participant in peer/group chats
110
+ );
111
+
112
+ -- Note: To add new enum values later, use:
113
+ -- ALTER TYPE hazo_enum_chat_type ADD VALUE 'new_value';
114
+ -- ALTER TYPE hazo_enum_group_type ADD VALUE 'new_value';
115
+ -- ALTER TYPE hazo_enum_group_role ADD VALUE 'new_value';
116
+ ```
117
+
118
+ #### Step 3.1.3: Create hazo_chat_group Table (UPDATED in v3.1)
119
+
120
+ Create the chat group container table:
121
+
122
+ ```sql
123
+
124
+ -- hazo_chat_group table for group-based chat (PostgreSQL)
125
+ -- UPDATED in v3.1: client_user_id is now nullable, added group_type
126
+ CREATE TABLE IF NOT EXISTS hazo_chat_group (
127
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
128
+ client_user_id UUID REFERENCES hazo_users(id), -- Nullable for peer/group chats
129
+ group_type hazo_enum_group_type DEFAULT 'support', -- Type of conversation
130
+ name VARCHAR(255),
131
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
132
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
133
+ );
134
+
135
+ -- Performance indexes
136
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_client ON hazo_chat_group(client_user_id);
137
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_type ON hazo_chat_group(group_type);
138
+ ```
139
+
140
+ **Group Types:**
141
+ - `'support'`: Client-to-staff support conversation (has designated `client_user_id`)
142
+ - `'peer'`: Peer-to-peer direct message (1:1, no `client_user_id`)
143
+ - `'group'`: Multi-user group conversation (no `client_user_id`)
144
+
145
+ #### Step 3.1.4: Create hazo_chat_group_users Table (UPDATED in v3.1)
146
+
147
+ Create the group membership table:
148
+
149
+ ```sql
150
+ -- hazo_chat_group_users table for group membership (PostgreSQL)
151
+ -- UPDATED in v3.1: uses hazo_enum_group_role enum type
152
+ CREATE TABLE IF NOT EXISTS hazo_chat_group_users (
153
+ chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
154
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
155
+ role hazo_enum_group_role NOT NULL DEFAULT 'member',
156
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
157
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
158
+ PRIMARY KEY (chat_group_id, user_id)
159
+ );
160
+
161
+ -- Performance indexes
162
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_users_user ON hazo_chat_group_users(user_id);
163
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_users_group ON hazo_chat_group_users(chat_group_id);
164
+ ```
60
165
 
61
- Run this SQL to create the chat messages table:
166
+ **Alternative: Without Enum Type (More Flexible)**
167
+
168
+ If you prefer flexibility over strict enum validation:
62
169
 
63
170
  ```sql
64
- -- hazo_chat table for storing chat messages
171
+ -- hazo_chat_group_users table with VARCHAR role (more flexible)
172
+ CREATE TABLE IF NOT EXISTS hazo_chat_group_users (
173
+ chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
174
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
175
+ role VARCHAR(20) NOT NULL CHECK (role IN ('client', 'staff', 'owner', 'admin', 'member')),
176
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
177
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
178
+ PRIMARY KEY (chat_group_id, user_id)
179
+ );
180
+ ```
181
+
182
+ **Role Types:**
183
+ - `'client'`: Customer/end-user in support scenarios
184
+ - `'staff'`: Support personnel in support scenarios
185
+ - `'owner'`: Creator of peer/group chats
186
+ - `'admin'`: Delegated administrator in group chats
187
+ - `'member'`: Standard participant in peer/group chats
188
+
189
+ #### Step 3.1.5: Create hazo_chat Table (MODIFIED in v3.0)
190
+
191
+ Create the chat messages table with UUID types and proper defaults:
192
+
193
+ ```sql
194
+ -- hazo_chat table for storing chat messages (PostgreSQL)
195
+ -- MODIFIED in v3.0: receiver_user_id replaced with chat_group_id
65
196
  CREATE TABLE IF NOT EXISTS hazo_chat (
66
- id TEXT PRIMARY KEY,
67
- reference_id TEXT NOT NULL,
68
- reference_type TEXT DEFAULT 'chat',
69
- sender_user_id TEXT NOT NULL,
70
- receiver_user_id TEXT NOT NULL,
197
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
198
+ reference_id UUID NOT NULL,
199
+ reference_type hazo_enum_chat_type DEFAULT 'chat' NOT NULL,
200
+ sender_user_id UUID NOT NULL,
201
+ chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id), -- CHANGED from receiver_user_id
71
202
  message_text TEXT,
72
- reference_list TEXT,
73
- read_at TEXT,
74
- deleted_at TEXT,
75
- created_at TEXT NOT NULL,
76
- changed_at TEXT NOT NULL
203
+ reference_list JSONB,
204
+ read_at TIMESTAMPTZ,
205
+ deleted_at TIMESTAMPTZ,
206
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
207
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
77
208
  );
78
209
 
79
210
  -- Performance indexes
80
211
  CREATE INDEX IF NOT EXISTS idx_hazo_chat_reference_id ON hazo_chat(reference_id);
81
212
  CREATE INDEX IF NOT EXISTS idx_hazo_chat_sender ON hazo_chat(sender_user_id);
82
- CREATE INDEX IF NOT EXISTS idx_hazo_chat_receiver ON hazo_chat(receiver_user_id);
213
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group ON hazo_chat(chat_group_id); -- CHANGED from receiver index
83
214
  CREATE INDEX IF NOT EXISTS idx_hazo_chat_created ON hazo_chat(created_at DESC);
215
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_reference_type ON hazo_chat(reference_type);
216
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_read_at ON hazo_chat(read_at) WHERE read_at IS NOT NULL;
217
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_deleted_at ON hazo_chat(deleted_at) WHERE deleted_at IS NOT NULL;
84
218
  ```
85
219
 
86
- ### Step 3.2: Ensure Users Table Exists
220
+ **Alternative: Without Enum Type (More Flexible)**
221
+
222
+ If you prefer flexibility over strict enum validation:
223
+
224
+ ```sql
225
+ -- hazo_chat table with TEXT reference_type (more flexible)
226
+ -- MODIFIED in v3.0: receiver_user_id replaced with chat_group_id
227
+ CREATE TABLE IF NOT EXISTS hazo_chat (
228
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
229
+ reference_id UUID NOT NULL,
230
+ reference_type TEXT DEFAULT 'chat' NOT NULL,
231
+ sender_user_id UUID NOT NULL,
232
+ chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id), -- CHANGED from receiver_user_id
233
+ message_text TEXT,
234
+ reference_list JSONB,
235
+ read_at TIMESTAMPTZ,
236
+ deleted_at TIMESTAMPTZ,
237
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
238
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
239
+ );
240
+
241
+ -- Performance indexes
242
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_reference_id ON hazo_chat(reference_id);
243
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_sender ON hazo_chat(sender_user_id);
244
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group ON hazo_chat(chat_group_id); -- CHANGED from receiver index
245
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_created ON hazo_chat(created_at DESC);
246
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_reference_type ON hazo_chat(reference_type);
247
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_read_at ON hazo_chat(read_at) WHERE read_at IS NOT NULL;
248
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_deleted_at ON hazo_chat(deleted_at) WHERE deleted_at IS NOT NULL;
249
+ ```
250
+
251
+ #### Step 3.1.4: Ensure Users Table Exists (PostgreSQL)
87
252
 
88
253
  You need a users table with at least these fields:
89
254
 
90
255
  ```sql
256
+ -- Users table for PostgreSQL
257
+ CREATE TABLE IF NOT EXISTS hazo_users (
258
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
259
+ email_address VARCHAR(255) UNIQUE NOT NULL,
260
+ name VARCHAR(255),
261
+ profile_picture_url TEXT,
262
+ is_active BOOLEAN DEFAULT TRUE NOT NULL,
263
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
264
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
265
+ );
266
+
267
+ -- Indexes for users table
268
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
269
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_active ON hazo_users(is_active) WHERE is_active = TRUE;
270
+ ```
271
+
272
+ #### Step 3.1.6: Add Foreign Key Constraints (Optional)
273
+
274
+ Add foreign key constraints for referential integrity:
275
+
276
+ ```sql
277
+ -- Add foreign key constraints (optional but recommended)
278
+ -- MODIFIED in v3.0: receiver_user_id constraint replaced with chat_group_id
279
+ ALTER TABLE hazo_chat
280
+ ADD CONSTRAINT fk_hazo_chat_sender
281
+ FOREIGN KEY (sender_user_id)
282
+ REFERENCES hazo_users(id)
283
+ ON DELETE RESTRICT,
284
+ ADD CONSTRAINT fk_hazo_chat_group
285
+ FOREIGN KEY (chat_group_id)
286
+ REFERENCES hazo_chat_group(id)
287
+ ON DELETE CASCADE;
288
+ ```
289
+
290
+ ### Step 3.2: SQLite Setup (Alternative)
291
+
292
+ For SQLite databases (development/testing), use this simplified schema.
293
+
294
+ #### Step 3.2.1: Create hazo_users Table (SQLite)
295
+
296
+ ```sql
297
+ -- Users table for SQLite
91
298
  CREATE TABLE IF NOT EXISTS hazo_users (
92
299
  id TEXT PRIMARY KEY,
93
300
  email_address TEXT UNIQUE NOT NULL,
94
301
  name TEXT,
95
302
  profile_picture_url TEXT,
96
303
  is_active INTEGER DEFAULT 1,
97
- created_at TEXT NOT NULL,
98
- changed_at TEXT NOT NULL
304
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
305
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
99
306
  );
307
+
308
+ -- Indexes for users table
309
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_email ON hazo_users(email_address);
310
+ ```
311
+
312
+ #### Step 3.2.2: Create hazo_chat_group Table (SQLite - UPDATED in v3.1)
313
+
314
+ ```sql
315
+ -- hazo_chat_group table for group-based chat (SQLite)
316
+ -- UPDATED in v3.1: client_user_id is now nullable, added group_type
317
+ CREATE TABLE IF NOT EXISTS hazo_chat_group (
318
+ id TEXT PRIMARY KEY,
319
+ client_user_id TEXT, -- Nullable for peer/group chats
320
+ group_type TEXT DEFAULT 'support' CHECK (group_type IN ('support', 'peer', 'group')),
321
+ name TEXT,
322
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
323
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
324
+ FOREIGN KEY (client_user_id) REFERENCES hazo_users(id)
325
+ );
326
+
327
+ -- Performance indexes
328
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_client ON hazo_chat_group(client_user_id);
329
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_type ON hazo_chat_group(group_type);
330
+ ```
331
+
332
+ **Group Types:**
333
+ - `'support'`: Client-to-staff support conversation
334
+ - `'peer'`: Peer-to-peer direct message (1:1)
335
+ - `'group'`: Multi-user group conversation
336
+
337
+ #### Step 3.2.3: Create hazo_chat_group_users Table (SQLite - UPDATED in v3.1)
338
+
339
+ ```sql
340
+ -- hazo_chat_group_users table for group membership (SQLite)
341
+ -- UPDATED in v3.1: expanded role options
342
+ CREATE TABLE IF NOT EXISTS hazo_chat_group_users (
343
+ chat_group_id TEXT NOT NULL,
344
+ user_id TEXT NOT NULL,
345
+ role TEXT NOT NULL CHECK (role IN ('client', 'staff', 'owner', 'admin', 'member')),
346
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
347
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
348
+ PRIMARY KEY (chat_group_id, user_id),
349
+ FOREIGN KEY (chat_group_id) REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
350
+ FOREIGN KEY (user_id) REFERENCES hazo_users(id) ON DELETE CASCADE
351
+ );
352
+
353
+ -- Performance indexes
354
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_users_user ON hazo_chat_group_users(user_id);
355
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_users_group ON hazo_chat_group_users(chat_group_id);
356
+ ```
357
+
358
+ **Role Types:**
359
+ - `'client'`: Customer/end-user in support scenarios
360
+ - `'staff'`: Support personnel in support scenarios
361
+ - `'owner'`: Creator of peer/group chats
362
+ - `'admin'`: Delegated administrator in group chats
363
+ - `'member'`: Standard participant in peer/group chats
364
+
365
+ #### Step 3.2.4: Create hazo_chat Table (SQLite - MODIFIED in v3.0)
366
+
367
+ ```sql
368
+ -- hazo_chat table for storing chat messages (SQLite)
369
+ -- MODIFIED in v3.0: receiver_user_id replaced with chat_group_id
370
+ CREATE TABLE IF NOT EXISTS hazo_chat (
371
+ id TEXT PRIMARY KEY,
372
+ reference_id TEXT NOT NULL,
373
+ reference_type TEXT DEFAULT 'chat',
374
+ sender_user_id TEXT NOT NULL,
375
+ chat_group_id TEXT NOT NULL,
376
+ message_text TEXT,
377
+ reference_list TEXT,
378
+ read_at TEXT,
379
+ deleted_at TEXT,
380
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
381
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
382
+ FOREIGN KEY (sender_user_id) REFERENCES hazo_users(id),
383
+ FOREIGN KEY (chat_group_id) REFERENCES hazo_chat_group(id)
384
+ );
385
+
386
+ -- Performance indexes
387
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_reference_id ON hazo_chat(reference_id);
388
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_sender ON hazo_chat(sender_user_id);
389
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group ON hazo_chat(chat_group_id);
390
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_created ON hazo_chat(created_at DESC);
391
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_reference_type ON hazo_chat(reference_type);
392
+ ```
393
+
394
+ ### Step 3.3: Key Differences Between PostgreSQL and SQLite
395
+
396
+ | Feature | PostgreSQL | SQLite |
397
+ |---------|-----------|--------|
398
+ | **ID Type** | `UUID` with `gen_random_uuid()` default | `TEXT` (manual UUID generation) |
399
+ | **Boolean** | `BOOLEAN` type with `TRUE`/`FALSE` | `INTEGER` with `0`/`1` |
400
+ | **Timestamp** | `TIMESTAMPTZ` with `NOW()` default | `TEXT` with `datetime('now')` |
401
+ | **JSON** | `JSONB` type (binary JSON) | `TEXT` (JSON string) |
402
+ | **Enum** | Native `ENUM` type available | Not supported (use TEXT) |
403
+ | **Extensions** | Requires UUID extension | No extensions needed |
404
+
405
+ #### Step 3.3.1: Verify PostgreSQL Setup
406
+
407
+ Run these queries to verify your PostgreSQL setup:
408
+
409
+ ```sql
410
+ -- Verify UUID extension is enabled
411
+ SELECT extname, extversion FROM pg_extension WHERE extname IN ('uuid-ossp', 'pgcrypto');
412
+
413
+ -- Verify enum type exists (if using enum)
414
+ SELECT typname FROM pg_type WHERE typname = 'hazo_chat_reference_type';
415
+
416
+ -- Verify table structure
417
+ SELECT
418
+ column_name,
419
+ data_type,
420
+ column_default,
421
+ is_nullable
422
+ FROM information_schema.columns
423
+ WHERE table_name = 'hazo_chat'
424
+ ORDER BY ordinal_position;
425
+
426
+ -- Verify indexes
427
+ SELECT indexname, indexdef
428
+ FROM pg_indexes
429
+ WHERE tablename = 'hazo_chat';
430
+
431
+ -- Test UUID generation
432
+ SELECT gen_random_uuid() AS test_uuid;
433
+
434
+ -- Test table insert with defaults (v3.0 - uses chat_group_id)
435
+ -- First ensure you have a chat group and the user is a member
436
+ INSERT INTO hazo_chat (
437
+ reference_id,
438
+ sender_user_id,
439
+ chat_group_id,
440
+ message_text
441
+ ) VALUES (
442
+ gen_random_uuid(),
443
+ (SELECT id FROM hazo_users LIMIT 1), -- Use existing user ID
444
+ (SELECT id FROM hazo_chat_group LIMIT 1), -- Use existing chat group ID
445
+ 'Test message'
446
+ );
447
+
448
+ -- Verify auto-generated values
449
+ SELECT
450
+ id,
451
+ created_at,
452
+ changed_at,
453
+ reference_type
454
+ FROM hazo_chat
455
+ WHERE message_text = 'Test message'
456
+ LIMIT 1;
457
+
458
+ -- Clean up test data
459
+ DELETE FROM hazo_chat WHERE message_text = 'Test message';
100
460
  ```
101
461
 
102
462
  ### Verification
103
- - [ ] `hazo_chat` table exists in database
104
- - [ ] Users table exists with at least one user
463
+
464
+ **PostgreSQL:**
465
+ - [ ] UUID extension enabled (`uuid-ossp` or `pgcrypto`)
466
+ - [ ] Enum type created (if using enum approach)
467
+ - [ ] `hazo_users` table exists with UUID primary key
468
+ - [ ] `hazo_chat_group` table exists (v3.0)
469
+ - [ ] `hazo_chat_group_users` table exists (v3.0)
470
+ - [ ] `hazo_chat` table exists with `chat_group_id` column (v3.0)
471
+ - [ ] All indexes created successfully
472
+ - [ ] Can query: `SELECT * FROM hazo_chat LIMIT 1`
473
+ - [ ] UUID default generation works: `INSERT INTO hazo_chat (...) VALUES (...)` generates UUID automatically
474
+ - [ ] Timestamp defaults work: `created_at` and `changed_at` auto-populate
475
+
476
+ **SQLite:**
477
+ - [ ] `hazo_users` table exists
478
+ - [ ] `hazo_chat_group` table exists (v3.0)
479
+ - [ ] `hazo_chat_group_users` table exists (v3.0)
480
+ - [ ] `hazo_chat` table exists with `chat_group_id` column (v3.0)
481
+ - [ ] At least one user exists in database
482
+ - [ ] At least one chat group exists with memberships
105
483
  - [ ] Can query: `SELECT * FROM hazo_chat LIMIT 1`
106
484
 
107
485
  ---
@@ -276,8 +654,9 @@ This endpoint is optional but useful for displaying unread message counts or bad
276
654
 
277
655
  ```typescript
278
656
  /**
279
- * API route to get unread message counts grouped by reference_id
657
+ * API route to get unread message counts grouped by chat_group_id
280
658
  * Uses the exportable library function from hazo_chat
659
+ * MODIFIED in v3.0: Now uses user_id and optional chat_group_ids
281
660
  */
282
661
 
283
662
  export const dynamic = 'force-dynamic';
@@ -294,36 +673,45 @@ const hazo_chat_get_unread_count = createUnreadCountFunction({
294
673
  export async function GET(request: NextRequest) {
295
674
  try {
296
675
  const { searchParams } = new URL(request.url);
297
- const receiver_user_id = searchParams.get('receiver_user_id');
676
+ const user_id = searchParams.get('user_id');
677
+ const chat_group_ids_param = searchParams.get('chat_group_ids');
298
678
 
299
- if (!receiver_user_id) {
679
+ if (!user_id) {
300
680
  return NextResponse.json(
301
- {
302
- success: false,
303
- error: 'receiver_user_id is required',
681
+ {
682
+ success: false,
683
+ error: 'user_id is required',
304
684
  unread_counts: []
305
685
  },
306
686
  { status: 400 }
307
687
  );
308
688
  }
309
689
 
690
+ // Parse optional chat_group_ids (comma-separated)
691
+ const chat_group_ids = chat_group_ids_param
692
+ ? chat_group_ids_param.split(',').filter(Boolean)
693
+ : undefined;
694
+
310
695
  // Call the library function
311
- const unread_counts = await hazo_chat_get_unread_count(receiver_user_id);
696
+ const unread_counts = await hazo_chat_get_unread_count({
697
+ user_id,
698
+ chat_group_ids
699
+ });
312
700
 
313
701
  return NextResponse.json({
314
702
  success: true,
315
- receiver_user_id,
703
+ user_id,
316
704
  unread_counts,
317
- total_references: unread_counts.length,
705
+ total_groups: unread_counts.length,
318
706
  total_unread: unread_counts.reduce((sum, item) => sum + item.count, 0)
319
707
  });
320
708
  } catch (error) {
321
709
  const error_message = error instanceof Error ? error.message : 'Unknown error';
322
710
  console.error('[hazo_chat/unread_count] Error:', error_message, error);
323
-
711
+
324
712
  return NextResponse.json(
325
- {
326
- success: false,
713
+ {
714
+ success: false,
327
715
  error: error_message,
328
716
  unread_counts: []
329
717
  },
@@ -333,25 +721,27 @@ export async function GET(request: NextRequest) {
333
721
  }
334
722
  ```
335
723
 
336
- **What this endpoint does:**
337
- - Takes `receiver_user_id` as a query parameter
338
- - Returns an array of objects with `reference_id` and `count` of unread messages
724
+ **What this endpoint does (v3.0):**
725
+ - Takes `user_id` as a required query parameter
726
+ - Takes optional `chat_group_ids` as a comma-separated list to filter specific groups
727
+ - Returns an array of objects with `chat_group_id` and `count` of unread messages
728
+ - Only counts messages in groups where the user is a member
729
+ - Excludes messages sent by the user themselves
339
730
  - Only counts messages where `read_at` is `null` and `deleted_at` is `null`
340
- - Groups results by `reference_id`
731
+ - Groups results by `chat_group_id`
341
732
  - Sorts by count (descending - most unread first)
342
733
 
343
734
  **Response format:**
344
735
  ```json
345
736
  {
346
737
  "success": true,
347
- "receiver_user_id": "user-123",
738
+ "user_id": "user-123",
348
739
  "unread_counts": [
349
- { "reference_id": "ref-1", "count": 5 },
350
- { "reference_id": "ref-2", "count": 3 },
351
- { "reference_id": "", "count": 1 }
740
+ { "chat_group_id": "group-1", "count": 5 },
741
+ { "chat_group_id": "group-2", "count": 3 }
352
742
  ],
353
- "total_references": 3,
354
- "total_unread": 9
743
+ "total_groups": 2,
744
+ "total_unread": 8
355
745
  }
356
746
  ```
357
747
 
@@ -371,9 +761,9 @@ export async function GET(request: NextRequest) {
371
761
  - [ ] All API route files exist
372
762
  - [ ] `GET /api/hazo_auth/me` returns user data when logged in
373
763
  - [ ] `POST /api/hazo_auth/profiles` returns profiles for given IDs
374
- - [ ] `GET /api/hazo_chat/messages?receiver_user_id=xxx` works
764
+ - [ ] `GET /api/hazo_chat/messages?chat_group_id=xxx` works (v3.0)
375
765
  - [ ] `PATCH /api/hazo_chat/messages/[message-id]/read` marks message as read
376
- - [ ] `GET /api/hazo_chat/unread_count?receiver_user_id=xxx` returns unread counts (if implemented)
766
+ - [ ] `GET /api/hazo_chat/unread_count?user_id=xxx` returns unread counts (v3.0, if implemented)
377
767
 
378
768
  ---
379
769
 
@@ -392,7 +782,7 @@ export default function ChatPage() {
392
782
  return (
393
783
  <div className="h-screen">
394
784
  <HazoChat
395
- receiver_user_id="recipient-uuid-here"
785
+ chat_group_id="group-uuid-here"
396
786
  title="Chat"
397
787
  subtitle="Direct Message"
398
788
  />
@@ -411,7 +801,7 @@ import { HazoChat } from 'hazo_chat';
411
801
  export default function ChatPage() {
412
802
  return (
413
803
  <HazoChat
414
- receiver_user_id="user-123"
804
+ chat_group_id="group-123"
415
805
  reference_id="project-456"
416
806
  reference_type="project_chat"
417
807
  api_base_url="/api/hazo_chat"
@@ -419,11 +809,11 @@ export default function ChatPage() {
419
809
  title="Project Discussion"
420
810
  subtitle="Design Review"
421
811
  additional_references={[
422
- {
423
- id: 'doc-1',
424
- type: 'document',
812
+ {
813
+ id: 'doc-1',
814
+ type: 'document',
425
815
  scope: 'field',
426
- name: 'Design.pdf',
816
+ name: 'Design.pdf',
427
817
  url: '/files/design.pdf'
428
818
  }
429
819
  ]}
@@ -446,18 +836,18 @@ The component uses `h-full`, which requires its parent container to have a **def
446
836
 
447
837
  ```typescript
448
838
  <div className="h-[600px]"> {/* ✅ Required: parent must have height */}
449
- <HazoChat receiver_user_id={...} />
839
+ <HazoChat chat_group_id={...} />
450
840
  </div>
451
841
 
452
842
  // OR
453
843
 
454
844
  <div className="h-screen"> {/* ✅ Full screen height */}
455
- <HazoChat receiver_user_id={...} />
845
+ <HazoChat chat_group_id={...} />
456
846
  </div>
457
847
 
458
848
  // ❌ WRONG - will cause layout issues
459
849
  <div> {/* No height defined */}
460
- <HazoChat receiver_user_id={...} />
850
+ <HazoChat chat_group_id={...} />
461
851
  </div>
462
852
  ```
463
853
 
@@ -470,12 +860,12 @@ The component uses `h-full`, which requires its parent container to have a **def
470
860
  ```typescript
471
861
  // ✅ Recommended: at least 500px width
472
862
  <div className="w-[600px] h-[600px]">
473
- <HazoChat receiver_user_id={...} />
863
+ <HazoChat chat_group_id={...} />
474
864
  </div>
475
865
 
476
866
  // ✅ Narrow container: documents will open in new tab
477
867
  <div className="w-[400px] h-[600px]">
478
- <HazoChat receiver_user_id={...} />
868
+ <HazoChat chat_group_id={...} />
479
869
  </div>
480
870
  ```
481
871
 
@@ -490,8 +880,8 @@ export default function ChatPage() {
490
880
  return (
491
881
  <div className="w-[600px] h-[600px] flex-shrink-0">
492
882
  <div className="rounded-xl border shadow-lg p-0 h-full">
493
- <HazoChat
494
- receiver_user_id="user-123"
883
+ <HazoChat
884
+ chat_group_id="group-123"
495
885
  title="Chat"
496
886
  className="h-full"
497
887
  />
@@ -917,8 +1307,28 @@ For detailed specifications, see the [UI Design Standards](#ui-design-standards)
917
1307
  - [ ] No TypeScript errors
918
1308
 
919
1309
  ### Database Verification
920
- - [ ] `hazo_chat` table exists
921
- - [ ] Users table exists with test users
1310
+
1311
+ **PostgreSQL:**
1312
+ - [ ] UUID extension enabled (check with: `SELECT * FROM pg_extension WHERE extname = 'uuid-ossp' OR extname = 'pgcrypto';`)
1313
+ - [ ] Enum type created (if using): `SELECT typname FROM pg_type WHERE typname = 'hazo_chat_reference_type';`
1314
+ - [ ] `hazo_users` table exists
1315
+ - [ ] `hazo_chat_group` table exists (v3.0)
1316
+ - [ ] `hazo_chat_group_users` table exists (v3.0)
1317
+ - [ ] `hazo_chat` table exists with `chat_group_id` column (v3.0)
1318
+ - [ ] All indexes created (check with: `SELECT indexname FROM pg_indexes WHERE tablename = 'hazo_chat';`)
1319
+ - [ ] UUID default generation works: Test insert without providing ID
1320
+ - [ ] Timestamp defaults work: Test insert without providing timestamps
1321
+ - [ ] Boolean columns accept TRUE/FALSE values
1322
+ - [ ] Can insert and query messages
1323
+ - [ ] Foreign key constraints work (if enabled)
1324
+
1325
+ **SQLite:**
1326
+ - [ ] `hazo_users` table exists
1327
+ - [ ] `hazo_chat_group` table exists (v3.0)
1328
+ - [ ] `hazo_chat_group_users` table exists (v3.0)
1329
+ - [ ] `hazo_chat` table exists with `chat_group_id` column (v3.0)
1330
+ - [ ] At least one user exists in database
1331
+ - [ ] At least one chat group exists with memberships
922
1332
  - [ ] Can insert and query messages
923
1333
 
924
1334
  ### API Verification
@@ -934,11 +1344,11 @@ curl -X POST http://localhost:3000/api/hazo_auth/profiles \
934
1344
  -H "Content-Type: application/json" \
935
1345
  -d '{"user_ids": ["user-id-here"]}'
936
1346
 
937
- # Test messages endpoint
938
- curl "http://localhost:3000/api/hazo_chat/messages?receiver_user_id=user-id"
1347
+ # Test messages endpoint (v3.0 - uses chat_group_id)
1348
+ curl "http://localhost:3000/api/hazo_chat/messages?chat_group_id=group-id"
939
1349
 
940
- # Test unread count endpoint (if implemented)
941
- curl "http://localhost:3000/api/hazo_chat/unread_count?receiver_user_id=user-id"
1350
+ # Test unread count endpoint (v3.0 - uses user_id, if implemented)
1351
+ curl "http://localhost:3000/api/hazo_chat/unread_count?user_id=user-id"
942
1352
  ```
943
1353
 
944
1354
  ### UI Verification & Responsive Behavior
@@ -1009,10 +1419,11 @@ curl "http://localhost:3000/api/hazo_chat/unread_count?receiver_user_id=user-id"
1009
1419
 
1010
1420
  **Checklist:**
1011
1421
  1. Is user authenticated? Check `/api/hazo_auth/me`
1012
- 2. Is `receiver_user_id` provided?
1013
- 3. Check browser console for errors
1014
- 4. Check network tab for API responses
1015
- 5. Check server logs for API errors
1422
+ 2. Is `chat_group_id` provided? (v3.0)
1423
+ 3. Is user a member of the chat group?
1424
+ 4. Check browser console for errors
1425
+ 5. Check network tab for API responses
1426
+ 6. Check server logs for API errors
1016
1427
 
1017
1428
  ### Issue: Hamburger button visible on desktop
1018
1429
 
@@ -1108,6 +1519,301 @@ curl "http://localhost:3000/api/hazo_chat/unread_count?receiver_user_id=user-id"
1108
1519
 
1109
1520
  ---
1110
1521
 
1522
+ ## Migration from v2.x to v3.0
1523
+
1524
+ This section provides instructions for upgrading from hazo_chat v2.x (1-to-1 chat with `receiver_user_id`) to v3.0 (group-based chat with `chat_group_id`).
1525
+
1526
+ ### Breaking Changes
1527
+
1528
+ 1. **Database Schema:**
1529
+ - `hazo_chat.receiver_user_id` column replaced with `chat_group_id`
1530
+ - New tables: `hazo_chat_group` and `hazo_chat_group_users`
1531
+
1532
+ 2. **Component Props:**
1533
+ - `receiver_user_id` prop replaced with `chat_group_id`
1534
+
1535
+ 3. **API Endpoints:**
1536
+ - GET/POST `/api/hazo_chat/messages` now uses `chat_group_id` query param
1537
+ - GET `/api/hazo_chat/unread_count` now uses `user_id` param instead of `receiver_user_id`
1538
+
1539
+ 4. **Types:**
1540
+ - `ChatMessage.receiver_profile` removed
1541
+ - `CreateMessagePayload.receiver_user_id` replaced with `chat_group_id`
1542
+
1543
+ ### Migration Steps (PostgreSQL)
1544
+
1545
+ ```sql
1546
+ -- Step 1: Create new tables
1547
+ CREATE TABLE IF NOT EXISTS hazo_chat_group (
1548
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1549
+ client_user_id UUID NOT NULL REFERENCES hazo_users(id),
1550
+ name VARCHAR(255),
1551
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1552
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1553
+ );
1554
+
1555
+ CREATE TABLE IF NOT EXISTS hazo_chat_group_users (
1556
+ chat_group_id UUID NOT NULL REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
1557
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
1558
+ role VARCHAR(20) NOT NULL CHECK (role IN ('client', 'staff')),
1559
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1560
+ changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1561
+ PRIMARY KEY (chat_group_id, user_id)
1562
+ );
1563
+
1564
+ -- Step 2: Create groups for existing 1-to-1 conversations
1565
+ -- This example creates one group per unique receiver_user_id
1566
+ INSERT INTO hazo_chat_group (id, client_user_id, name, created_at, changed_at)
1567
+ SELECT DISTINCT
1568
+ gen_random_uuid(),
1569
+ receiver_user_id,
1570
+ 'Migrated Chat',
1571
+ NOW(),
1572
+ NOW()
1573
+ FROM hazo_chat
1574
+ WHERE receiver_user_id IS NOT NULL;
1575
+
1576
+ -- Step 3: Add sender as staff member to each group
1577
+ INSERT INTO hazo_chat_group_users (chat_group_id, user_id, role, created_at, changed_at)
1578
+ SELECT DISTINCT
1579
+ g.id,
1580
+ c.sender_user_id,
1581
+ 'staff',
1582
+ NOW(),
1583
+ NOW()
1584
+ FROM hazo_chat c
1585
+ JOIN hazo_chat_group g ON g.client_user_id = c.receiver_user_id
1586
+ WHERE c.sender_user_id != c.receiver_user_id
1587
+ ON CONFLICT (chat_group_id, user_id) DO NOTHING;
1588
+
1589
+ -- Step 4: Add client as client member to each group
1590
+ INSERT INTO hazo_chat_group_users (chat_group_id, user_id, role, created_at, changed_at)
1591
+ SELECT
1592
+ id,
1593
+ client_user_id,
1594
+ 'client',
1595
+ NOW(),
1596
+ NOW()
1597
+ FROM hazo_chat_group
1598
+ ON CONFLICT (chat_group_id, user_id) DO NOTHING;
1599
+
1600
+ -- Step 5: Add chat_group_id column to hazo_chat
1601
+ ALTER TABLE hazo_chat ADD COLUMN chat_group_id UUID;
1602
+
1603
+ -- Step 6: Populate chat_group_id from receiver_user_id
1604
+ UPDATE hazo_chat c
1605
+ SET chat_group_id = g.id
1606
+ FROM hazo_chat_group g
1607
+ WHERE g.client_user_id = c.receiver_user_id;
1608
+
1609
+ -- Step 7: Make chat_group_id NOT NULL and add foreign key
1610
+ ALTER TABLE hazo_chat
1611
+ ALTER COLUMN chat_group_id SET NOT NULL,
1612
+ ADD CONSTRAINT fk_hazo_chat_group
1613
+ FOREIGN KEY (chat_group_id)
1614
+ REFERENCES hazo_chat_group(id);
1615
+
1616
+ -- Step 8: Drop old column and index
1617
+ DROP INDEX IF EXISTS idx_hazo_chat_receiver;
1618
+ ALTER TABLE hazo_chat DROP COLUMN receiver_user_id;
1619
+
1620
+ -- Step 9: Create new index
1621
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group ON hazo_chat(chat_group_id);
1622
+ ```
1623
+
1624
+ ### Migration Steps (SQLite)
1625
+
1626
+ ```sql
1627
+ -- Step 1: Create new tables
1628
+ CREATE TABLE IF NOT EXISTS hazo_chat_group (
1629
+ id TEXT PRIMARY KEY,
1630
+ client_user_id TEXT NOT NULL,
1631
+ name TEXT,
1632
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1633
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
1634
+ FOREIGN KEY (client_user_id) REFERENCES hazo_users(id)
1635
+ );
1636
+
1637
+ CREATE TABLE IF NOT EXISTS hazo_chat_group_users (
1638
+ chat_group_id TEXT NOT NULL,
1639
+ user_id TEXT NOT NULL,
1640
+ role TEXT NOT NULL CHECK (role IN ('client', 'staff')),
1641
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1642
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
1643
+ PRIMARY KEY (chat_group_id, user_id),
1644
+ FOREIGN KEY (chat_group_id) REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
1645
+ FOREIGN KEY (user_id) REFERENCES hazo_users(id) ON DELETE CASCADE
1646
+ );
1647
+
1648
+ -- Step 2: Create groups for existing conversations (manually generate UUIDs)
1649
+ -- Note: SQLite doesn't have gen_random_uuid(), use your app or external tool
1650
+
1651
+ -- Step 3: Recreate hazo_chat table with new schema (SQLite doesn't support ALTER COLUMN)
1652
+ ALTER TABLE hazo_chat RENAME TO hazo_chat_old;
1653
+
1654
+ CREATE TABLE hazo_chat (
1655
+ id TEXT PRIMARY KEY,
1656
+ reference_id TEXT NOT NULL,
1657
+ reference_type TEXT DEFAULT 'chat',
1658
+ sender_user_id TEXT NOT NULL,
1659
+ chat_group_id TEXT NOT NULL,
1660
+ message_text TEXT,
1661
+ reference_list TEXT,
1662
+ read_at TEXT,
1663
+ deleted_at TEXT,
1664
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1665
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
1666
+ FOREIGN KEY (sender_user_id) REFERENCES hazo_users(id),
1667
+ FOREIGN KEY (chat_group_id) REFERENCES hazo_chat_group(id)
1668
+ );
1669
+
1670
+ -- Step 4: Migrate data (you'll need to map receiver_user_id to chat_group_id)
1671
+ INSERT INTO hazo_chat (id, reference_id, reference_type, sender_user_id, chat_group_id,
1672
+ message_text, reference_list, read_at, deleted_at, created_at, changed_at)
1673
+ SELECT o.id, o.reference_id, o.reference_type, o.sender_user_id, g.id,
1674
+ o.message_text, o.reference_list, o.read_at, o.deleted_at, o.created_at, o.changed_at
1675
+ FROM hazo_chat_old o
1676
+ JOIN hazo_chat_group g ON g.client_user_id = o.receiver_user_id;
1677
+
1678
+ -- Step 5: Drop old table
1679
+ DROP TABLE hazo_chat_old;
1680
+
1681
+ -- Step 6: Create indexes
1682
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_reference_id ON hazo_chat(reference_id);
1683
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_sender ON hazo_chat(sender_user_id);
1684
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group ON hazo_chat(chat_group_id);
1685
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_created ON hazo_chat(created_at DESC);
1686
+ ```
1687
+
1688
+ ### Code Changes Required
1689
+
1690
+ 1. **Update HazoChat component usage:**
1691
+ ```typescript
1692
+ // Before (v2.x)
1693
+ <HazoChat receiver_user_id="user-123" />
1694
+
1695
+ // After (v3.0)
1696
+ <HazoChat chat_group_id="group-123" />
1697
+ ```
1698
+
1699
+ 2. **Update API route for unread count:**
1700
+ ```typescript
1701
+ // Before (v2.x)
1702
+ const unread = await hazo_chat_get_unread_count(receiver_user_id);
1703
+
1704
+ // After (v3.0)
1705
+ const unread = await hazo_chat_get_unread_count({ user_id, chat_group_ids });
1706
+ ```
1707
+
1708
+ 3. **Update any direct database queries** to use `chat_group_id` instead of `receiver_user_id`.
1709
+
1710
+ ---
1711
+
1712
+ ## Migration from v3.0 to v3.1 (Generic Schema)
1713
+
1714
+ This section provides instructions for upgrading from hazo_chat v3.0 to v3.1 to support flexible chat patterns (support, peer, and group conversations).
1715
+
1716
+ ### What Changed in v3.1
1717
+
1718
+ 1. **`client_user_id`**: Now nullable (optional) for peer/group chats
1719
+ 2. **`group_type`**: New field to identify conversation type ('support', 'peer', 'group')
1720
+ 3. **Expanded roles**: 'owner', 'admin', 'member' added alongside 'client' and 'staff'
1721
+
1722
+ ### PostgreSQL Migration
1723
+
1724
+ ```sql
1725
+ -- Step 1: Create enum types (if not already created)
1726
+ CREATE TYPE hazo_enum_group_type AS ENUM ('support', 'peer', 'group');
1727
+ CREATE TYPE hazo_enum_group_role AS ENUM ('client', 'staff', 'owner', 'admin', 'member');
1728
+
1729
+ -- Step 2: Add group_type column with default 'support' for existing groups
1730
+ ALTER TABLE hazo_chat_group
1731
+ ADD COLUMN group_type hazo_enum_group_type DEFAULT 'support';
1732
+
1733
+ -- Step 3: Make client_user_id nullable
1734
+ ALTER TABLE hazo_chat_group
1735
+ ALTER COLUMN client_user_id DROP NOT NULL;
1736
+
1737
+ -- Step 4: Update role column to use enum type (if using enum approach)
1738
+ -- Option A: Convert to enum type (recommended)
1739
+ ALTER TABLE hazo_chat_group_users
1740
+ DROP CONSTRAINT IF EXISTS hazo_chat_group_users_role_check;
1741
+
1742
+ ALTER TABLE hazo_chat_group_users
1743
+ ALTER COLUMN role TYPE hazo_enum_group_role
1744
+ USING role::hazo_enum_group_role;
1745
+
1746
+ -- Option B: Keep VARCHAR with expanded CHECK constraint
1747
+ -- ALTER TABLE hazo_chat_group_users
1748
+ -- DROP CONSTRAINT IF EXISTS hazo_chat_group_users_role_check;
1749
+ --
1750
+ -- ALTER TABLE hazo_chat_group_users
1751
+ -- ADD CONSTRAINT hazo_chat_group_users_role_check
1752
+ -- CHECK (role IN ('client', 'staff', 'owner', 'admin', 'member'));
1753
+
1754
+ -- Step 5: Create index for group_type
1755
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_type ON hazo_chat_group(group_type);
1756
+ ```
1757
+
1758
+ ### SQLite Migration
1759
+
1760
+ SQLite doesn't support ALTER COLUMN, so table recreation is needed:
1761
+
1762
+ ```sql
1763
+ -- Step 1: Rename existing tables
1764
+ ALTER TABLE hazo_chat_group RENAME TO hazo_chat_group_old;
1765
+ ALTER TABLE hazo_chat_group_users RENAME TO hazo_chat_group_users_old;
1766
+
1767
+ -- Step 2: Create new hazo_chat_group table with updated schema
1768
+ CREATE TABLE hazo_chat_group (
1769
+ id TEXT PRIMARY KEY,
1770
+ client_user_id TEXT, -- Now nullable
1771
+ group_type TEXT DEFAULT 'support' CHECK (group_type IN ('support', 'peer', 'group')),
1772
+ name TEXT,
1773
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1774
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
1775
+ FOREIGN KEY (client_user_id) REFERENCES hazo_users(id)
1776
+ );
1777
+
1778
+ -- Step 3: Migrate group data (existing groups become 'support' type)
1779
+ INSERT INTO hazo_chat_group (id, client_user_id, group_type, name, created_at, changed_at)
1780
+ SELECT id, client_user_id, 'support', name, created_at, changed_at
1781
+ FROM hazo_chat_group_old;
1782
+
1783
+ -- Step 4: Drop old group table
1784
+ DROP TABLE hazo_chat_group_old;
1785
+
1786
+ -- Step 5: Create new hazo_chat_group_users table with expanded roles
1787
+ CREATE TABLE hazo_chat_group_users (
1788
+ chat_group_id TEXT NOT NULL,
1789
+ user_id TEXT NOT NULL,
1790
+ role TEXT NOT NULL CHECK (role IN ('client', 'staff', 'owner', 'admin', 'member')),
1791
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1792
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
1793
+ PRIMARY KEY (chat_group_id, user_id),
1794
+ FOREIGN KEY (chat_group_id) REFERENCES hazo_chat_group(id) ON DELETE CASCADE,
1795
+ FOREIGN KEY (user_id) REFERENCES hazo_users(id) ON DELETE CASCADE
1796
+ );
1797
+
1798
+ -- Step 6: Migrate membership data
1799
+ INSERT INTO hazo_chat_group_users SELECT * FROM hazo_chat_group_users_old;
1800
+
1801
+ -- Step 7: Drop old membership table
1802
+ DROP TABLE hazo_chat_group_users_old;
1803
+
1804
+ -- Step 8: Recreate indexes
1805
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_client ON hazo_chat_group(client_user_id);
1806
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_type ON hazo_chat_group(group_type);
1807
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_users_user ON hazo_chat_group_users(user_id);
1808
+ CREATE INDEX IF NOT EXISTS idx_hazo_chat_group_users_group ON hazo_chat_group_users(chat_group_id);
1809
+ ```
1810
+
1811
+ ### No Code Changes Required
1812
+
1813
+ The TypeScript types have been updated, but the API handlers do not enforce role-based permissions. Existing code will continue to work without modification.
1814
+
1815
+ ---
1816
+
1111
1817
  ## Quick Setup Summary
1112
1818
 
1113
1819
  ```bash