@proveanything/smartlinks 1.4.7 → 1.5.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.
@@ -0,0 +1,954 @@
1
+ # App Objects: Cases, Threads, and Records
2
+
3
+ This guide covers the three generic app-scoped object types that apps can use as flexible building blocks for different use cases: **Cases**, **Threads**, and **Records**.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ SmartLinks provides three generic data models scoped to your app that can be adapted for countless scenarios. Think of them as configurable primitives that you shape to fit your needs:
10
+
11
+ - **Cases** — Track issues, requests, or tasks that need resolution
12
+ - **Threads** — Manage discussions, comments, or any reply-based content
13
+ - **Records** — Store structured data with flexible lifecycles
14
+
15
+ Each object type supports:
16
+ - **JSONB zones** (`data`, `owner`, `admin`) for granular access control
17
+ - **Visibility levels** (`public`, `owner`, `admin`) for content exposure
18
+ - **Flexible schemas** — store any JSON in the zone fields
19
+ - **Admin and public endpoints** for different caller contexts
20
+ - **Rich querying** with filters, sorting, pagination, and aggregations
21
+
22
+ ```text
23
+ ┌─────────────────────────────────────────────────────────────────┐
24
+ │ Your SmartLinks App │
25
+ │ │
26
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
27
+ │ │ Cases │ │ Threads │ │ Records │ │
28
+ │ │ │ │ │ │ │ │
29
+ │ │ • Support │ │ • Comments │ │ • Bookings │ │
30
+ │ │ • Warranty │ │ • Q&A │ │ • Licenses │ │
31
+ │ │ • Feedback │ │ • Reviews │ │ • Visits │ │
32
+ │ │ • RMA │ │ • Forum │ │ • Events │ │
33
+ │ └─────────────┘ └─────────────┘ └─────────────┘ │
34
+ │ │
35
+ │ All scoped to: /collection/:cId/app/:appId │
36
+ └─────────────────────────────────────────────────────────────────┘
37
+ ```
38
+
39
+ ---
40
+
41
+ ## The JSONB Zone Model
42
+
43
+ All three object types use a three-tier access model with JSONB fields:
44
+
45
+ | Zone | Visible to | Writable by | Use Case |
46
+ |---------|-------------------|-------------------|-----------------------------------|
47
+ | `data` | public, owner, admin | public, owner, admin | Shared public information |
48
+ | `owner` | owner, admin | owner, admin | User-specific private data |
49
+ | `admin` | admin | admin | Internal notes, sensitive data |
50
+
51
+ ### How Zones Work
52
+
53
+ Zones are **automatically filtered** based on the caller's role:
54
+
55
+ ```typescript
56
+ // Public endpoint caller sees:
57
+ {
58
+ id: 'case_123',
59
+ status: 'open',
60
+ data: { issue: 'Screen cracked', photos: [...] },
61
+ // owner and admin zones stripped
62
+ }
63
+
64
+ // Owner (authenticated contact) sees:
65
+ {
66
+ id: 'case_123',
67
+ status: 'open',
68
+ data: { issue: 'Screen cracked', photos: [...] },
69
+ owner: { shippingAddress: '...', preference: 'email' },
70
+ // admin zone stripped
71
+ }
72
+
73
+ // Admin sees everything:
74
+ {
75
+ id: 'case_123',
76
+ status: 'open',
77
+ data: { issue: 'Screen cracked', photos: [...] },
78
+ owner: { shippingAddress: '...', preference: 'email' },
79
+ admin: { internalNotes: 'Escalate to tier 2', cost: 45.00 }
80
+ }
81
+ ```
82
+
83
+ **Key insight:** The server strips zones before returning objects. You don't need to worry about accidentally leaking `admin` data — it's never sent to non-admin callers.
84
+
85
+ ### Zone Writing Rules
86
+
87
+ - **Non-admin callers** attempting to write to the `admin` zone are silently ignored
88
+ - **Public callers** can write to `data` and `owner` (if visibility allows)
89
+ - **Admins** can write to all three zones
90
+
91
+ ---
92
+
93
+ ## Visibility Levels
94
+
95
+ Each object has a `visibility` field that controls who can access it on **public endpoints**:
96
+
97
+ | Visibility | Public Endpoint Behavior |
98
+ |------------|--------------------------------------------------|
99
+ | `public` | Anyone can read (even anonymous) |
100
+ | `owner` | Only the owning contact can read |
101
+ | `admin` | Never visible on public endpoints (404) |
102
+
103
+ **Admin endpoints** always return all objects regardless of visibility.
104
+
105
+ ### Typical Patterns
106
+
107
+ ```typescript
108
+ // Public discussion thread
109
+ await threads.create(collectionId, appId, {
110
+ visibility: 'public',
111
+ title: 'How do I clean this product?',
112
+ body: { text: 'Looking for cleaning instructions...' }
113
+ });
114
+
115
+ // Private support case
116
+ await cases.create(collectionId, appId, {
117
+ visibility: 'owner', // Only this contact can see it
118
+ category: 'warranty',
119
+ data: { issue: 'Defective unit' },
120
+ owner: { serialNumber: 'ABC123' }
121
+ });
122
+
123
+ // Admin-only internal record
124
+ await records.create(collectionId, appId, {
125
+ visibility: 'admin', // Never appears on public endpoints
126
+ recordType: 'audit_log',
127
+ admin: { action: 'manual_refund', amount: 50.00 }
128
+ }, true); // admin = true
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Cases
134
+
135
+ **Cases** represent trackable issues, requests, or tasks that move through states and require resolution.
136
+
137
+ ### When to Use Cases
138
+
139
+ - **Customer support tickets** — track issues from creation to resolution
140
+ - **Warranty claims** — manage claims with status, priority, and assignment
141
+ - **Feature requests** — collect and triage user feedback
142
+ - **RMA (Return Merchandise Authorization)** — handle product returns
143
+ - **Bug reports** — track defects from user submissions
144
+ - **Service requests** — manage appointments, repairs, installations
145
+
146
+ ### Key Features
147
+
148
+ - **Status lifecycle** — `'open'` → `'in-progress'` → `'resolved'` → `'closed'` (or custom statuses)
149
+ - **Priority levels** — numerical priority for sorting/escalation
150
+ - **Categories** — group cases by type (warranty, bug, feature, etc.)
151
+ - **Assignment** — `assignedTo` field for routing to team members
152
+ - **History tracking** — append timestamped entries to `admin.history` or `owner.history`
153
+ - **Closing metrics** — track `closedAt` to measure resolution time
154
+
155
+ ### Example: Warranty Claims
156
+
157
+ ```typescript
158
+ import { cases } from '@proveanything/smartlinks';
159
+
160
+ // Customer submits a warranty claim (public endpoint)
161
+ const claim = await cases.create(collectionId, appId, {
162
+ visibility: 'owner',
163
+ category: 'warranty',
164
+ status: 'open',
165
+ priority: 2,
166
+ productId: product.id,
167
+ proofId: proof.id,
168
+ contactId: user.contactId,
169
+ data: {
170
+ issue: 'Screen flickering after 3 months',
171
+ photos: ['https://...', 'https://...']
172
+ },
173
+ owner: {
174
+ purchaseDate: '2025-11-15',
175
+ serialNumber: 'SN-7738291',
176
+ preferredContact: 'email'
177
+ }
178
+ });
179
+
180
+ // Admin reviews and assigns (admin endpoint)
181
+ await cases.update(collectionId, appId, claim.id, {
182
+ assignedTo: 'user_jane_support',
183
+ priority: 3, // escalate
184
+ admin: {
185
+ internalNotes: 'Likely hardware defect, approve replacement'
186
+ }
187
+ }, true); // admin = true
188
+
189
+ // Admin appends to history
190
+ await cases.appendHistory(collectionId, appId, claim.id, {
191
+ entry: {
192
+ action: 'approved_replacement',
193
+ agent: 'Jane',
194
+ tracking: 'UPS-123456789'
195
+ },
196
+ historyTarget: 'owner', // visible to customer
197
+ status: 'resolved'
198
+ });
199
+
200
+ // Get case summary stats (admin)
201
+ const summary = await cases.summary(collectionId, appId, {
202
+ period: { from: '2026-01-01', to: '2026-02-28' }
203
+ });
204
+ // Returns: { total: 142, byStatus: { open: 12, resolved: 130 }, ... }
205
+ ```
206
+
207
+ ### Use Case: Support Dashboard
208
+
209
+ Build a live support dashboard showing open cases by priority:
210
+
211
+ ```typescript
212
+ const openCases = await cases.list(collectionId, appId, {
213
+ status: 'open',
214
+ sort: 'priority:desc',
215
+ limit: 50
216
+ }, true);
217
+
218
+ // Aggregate by category
219
+ const stats = await cases.aggregate(collectionId, appId, {
220
+ filters: { status: 'open' },
221
+ groupBy: ['category', 'priority'],
222
+ metrics: ['count']
223
+ }, true);
224
+
225
+ // Time series: cases created per week
226
+ const trend = await cases.aggregate(collectionId, appId, {
227
+ timeSeriesField: 'created_at',
228
+ timeSeriesInterval: 'week',
229
+ metrics: ['count']
230
+ }, true);
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Threads
236
+
237
+ **Threads** represent discussions, comments, or any content that accumulates replies over time.
238
+
239
+ ### When to Use Threads
240
+
241
+ - **Product Q&A** — questions and answers about products
242
+ - **Community forums** — discussions grouped by topic
243
+ - **Comments** — on products, proofs, or other resources
244
+ - **Review discussions** — follow-up questions on reviews
245
+ - **Feedback threads** — ongoing conversations about features
246
+ - **Support chat** — lightweight message threads
247
+
248
+ ### Key Features
249
+
250
+ - **Reply tracking** — `replies` array with timestamped entries
251
+ - **Reply count** — auto-incremented `replyCount` and `lastReplyAt`
252
+ - **Slugs** — optional URL-friendly slug for pretty URLs
253
+ - **Tags** — JSONB array of tags for categorization
254
+ - **Parent linking** — `parentType` + `parentId` to attach to products, proofs, etc.
255
+ - **Author metadata** — track `authorId` and `authorType`
256
+
257
+ ### Example: Product Q&A
258
+
259
+ ```typescript
260
+ import { threads } from '@proveanything/smartlinks';
261
+
262
+ // Customer asks a question (public endpoint)
263
+ const question = await threads.create(collectionId, appId, {
264
+ visibility: 'public',
265
+ slug: 'how-to-clean-leather',
266
+ title: 'How do I clean leather without damaging it?',
267
+ status: 'open',
268
+ authorId: user.contactId,
269
+ authorType: 'customer',
270
+ productId: product.id,
271
+ body: {
272
+ text: 'I spilled coffee on my leather bag. What cleaner is safe to use?'
273
+ },
274
+ tags: ['cleaning', 'leather', 'care']
275
+ });
276
+
277
+ // Another customer replies
278
+ await threads.reply(collectionId, appId, question.id, {
279
+ authorId: otherUser.contactId,
280
+ authorType: 'customer',
281
+ text: 'I use a mild soap and water solution. Works great!'
282
+ });
283
+
284
+ // Admin (brand expert) replies
285
+ await threads.reply(collectionId, appId, question.id, {
286
+ authorId: 'user_expert_sarah',
287
+ authorType: 'brand_expert',
288
+ text: 'Our official leather care kit is perfect for this. Avoid harsh chemicals.',
289
+ productLink: 'prod_leather_care_kit'
290
+ }, true); // admin endpoint
291
+
292
+ // Admin marks as resolved
293
+ await threads.update(collectionId, appId, question.id, {
294
+ status: 'resolved'
295
+ }, true);
296
+ ```
297
+
298
+ ### Use Case: Forum-Style Discussions
299
+
300
+ List recent discussions with reply counts:
301
+
302
+ ```typescript
303
+ // Get active threads
304
+ const activeThreads = await threads.list(collectionId, appId, {
305
+ status: 'open',
306
+ sort: 'lastReplyAt:desc',
307
+ limit: 20
308
+ });
309
+
310
+ // Filter by tag
311
+ const cleaningThreads = await threads.list(collectionId, appId, {
312
+ tag: 'cleaning'
313
+ });
314
+
315
+ // Aggregate: most active discussion topics
316
+ const topicStats = await threads.aggregate(collectionId, appId, {
317
+ groupBy: ['status'],
318
+ metrics: ['count', 'reply_count']
319
+ });
320
+ ```
321
+
322
+ ### Use Case: Product Comments
323
+
324
+ Attach comments to a specific product:
325
+
326
+ ```typescript
327
+ // Create a comment thread for a product
328
+ await threads.create(collectionId, appId, {
329
+ visibility: 'public',
330
+ parentType: 'product',
331
+ parentId: product.id,
332
+ authorId: user.contactId,
333
+ body: { text: 'Love this product! Best purchase ever.' },
334
+ tags: ['positive']
335
+ });
336
+
337
+ // List all comments for a product
338
+ const productComments = await threads.list(collectionId, appId, {
339
+ parentType: 'product',
340
+ parentId: product.id,
341
+ sort: 'createdAt:desc'
342
+ });
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Records
348
+
349
+ **Records** are the most flexible object type — use them for structured data with time-based lifecycles, hierarchies, or custom schemas.
350
+
351
+ ### When to Use Records
352
+
353
+ - **Bookings/Reservations** — track appointments with start/end times
354
+ - **Licenses** — manage software licenses with expiration
355
+ - **Subscriptions** — track subscription status and renewal
356
+ - **Certifications** — store certifications with expiry dates
357
+ - **Events** — track event registrations and attendance
358
+ - **Usage logs** — record product usage metrics
359
+ - **Audit trails** — immutable logs of actions
360
+ - **Loyalty points** — track points earned/redeemed
361
+
362
+ ### Key Features
363
+
364
+ - **Record types** — `recordType` field for categorization (required)
365
+ - **Time windows** — `startsAt` and `expiresAt` for time-based data
366
+ - **Parent linking** — attach to products, proofs, contacts, etc.
367
+ - **Author tracking** — `authorId` + `authorType`
368
+ - **Status lifecycle** — custom statuses (default `'active'`)
369
+ - **References** — optional `ref` field for external IDs
370
+
371
+ ### Example: Product Registration
372
+
373
+ ```typescript
374
+ import { records } from '@proveanything/smartlinks';
375
+
376
+ // Customer registers a product
377
+ const registration = await records.create(collectionId, appId, {
378
+ recordType: 'product_registration',
379
+ visibility: 'owner',
380
+ status: 'active',
381
+ productId: product.id,
382
+ proofId: proof.id,
383
+ contactId: user.contactId,
384
+ authorId: user.contactId,
385
+ authorType: 'customer',
386
+ startsAt: new Date().toISOString(),
387
+ expiresAt: new Date(Date.now() + 365*24*60*60*1000).toISOString(), // 1 year warranty
388
+ data: {
389
+ registrationNumber: 'REG-2026-1234',
390
+ purchaseDate: '2026-02-15',
391
+ retailer: 'Best Electronics'
392
+ },
393
+ owner: {
394
+ serialNumber: 'SN-9922736',
395
+ installDate: '2026-02-20',
396
+ location: 'Home office'
397
+ }
398
+ });
399
+
400
+ // List active registrations for a customer
401
+ const activeRegistrations = await records.list(collectionId, appId, {
402
+ contactId: user.contactId,
403
+ recordType: 'product_registration',
404
+ status: 'active'
405
+ });
406
+
407
+ // Find expiring registrations (admin)
408
+ const expiringSoon = await records.list(collectionId, appId, {
409
+ recordType: 'product_registration',
410
+ expiresAt: `lte:${new Date(Date.now() + 30*24*60*60*1000).toISOString()}` // next 30 days
411
+ }, true);
412
+ ```
413
+
414
+ ### Example: Appointment Booking
415
+
416
+ ```typescript
417
+ // Customer books a service appointment
418
+ const booking = await records.create(collectionId, appId, {
419
+ recordType: 'service_appointment',
420
+ visibility: 'owner',
421
+ contactId: user.contactId,
422
+ startsAt: '2026-03-15T10:00:00Z',
423
+ expiresAt: '2026-03-15T11:00:00Z', // 1-hour appointment
424
+ data: {
425
+ serviceType: 'installation',
426
+ location: 'Customer site',
427
+ technician: null // assigned later
428
+ },
429
+ owner: {
430
+ address: '123 Main St',
431
+ phone: '555-1234',
432
+ notes: 'Call before arrival'
433
+ }
434
+ });
435
+
436
+ // Admin assigns technician
437
+ await records.update(collectionId, appId, booking.id, {
438
+ data: {
439
+ serviceType: 'installation',
440
+ location: 'Customer site',
441
+ technician: 'tech_john'
442
+ },
443
+ admin: {
444
+ cost: 150.00,
445
+ travelTime: 30
446
+ }
447
+ }, true);
448
+
449
+ // List today's appointments
450
+ const today = new Date().toISOString().split('T')[0];
451
+ const todaysAppointments = await records.list(collectionId, appId, {
452
+ recordType: 'service_appointment',
453
+ startsAt: `gte:${today}T00:00:00Z`,
454
+ sort: 'startsAt:asc'
455
+ }, true);
456
+ ```
457
+
458
+ ### Example: Usage Tracking
459
+
460
+ ```typescript
461
+ // Log product usage (could be triggered by IoT device)
462
+ await records.create(collectionId, appId, {
463
+ recordType: 'usage_log',
464
+ visibility: 'admin',
465
+ productId: product.id,
466
+ proofId: proof.id,
467
+ startsAt: new Date().toISOString(),
468
+ data: {
469
+ metric: 'power_on',
470
+ duration: 3600, // seconds
471
+ location: 'geo:37.7749,-122.4194'
472
+ }
473
+ }, true);
474
+
475
+ // Aggregate usage metrics
476
+ const usageStats = await records.aggregate(collectionId, appId, {
477
+ filters: {
478
+ record_type: 'usage_log',
479
+ created_at: {
480
+ gte: '2026-02-01',
481
+ lte: '2026-02-28'
482
+ }
483
+ },
484
+ groupBy: ['product_id'],
485
+ metrics: ['count']
486
+ }, true);
487
+ ```
488
+
489
+ ---
490
+
491
+ ## Public Create Policies
492
+
493
+ Control who can create objects on **public endpoints** using Firestore-based policies at:
494
+ `sites/{collectionId}/apps/{appId}.publicCreate`
495
+
496
+ ### Policy Structure
497
+
498
+ ```typescript
499
+ interface PublicCreatePolicy {
500
+ cases?: {
501
+ allow: {
502
+ anonymous?: boolean // allow unauthenticated users
503
+ authenticated?: boolean // allow authenticated contacts
504
+ }
505
+ enforce?: {
506
+ anonymous?: Partial<CreateCaseInput> // force these values for anon
507
+ authenticated?: Partial<CreateCaseInput> // force these values for auth
508
+ }
509
+ }
510
+ threads?: { /* same structure */ }
511
+ records?: { /* same structure */ }
512
+ }
513
+ ```
514
+
515
+ ### Example Policies
516
+
517
+ **Support tickets from anyone:**
518
+
519
+ ```json
520
+ {
521
+ "cases": {
522
+ "allow": {
523
+ "anonymous": true,
524
+ "authenticated": true
525
+ },
526
+ "enforce": {
527
+ "anonymous": {
528
+ "visibility": "owner",
529
+ "status": "open",
530
+ "category": "support"
531
+ },
532
+ "authenticated": {
533
+ "visibility": "owner",
534
+ "status": "open"
535
+ }
536
+ }
537
+ }
538
+ }
539
+ ```
540
+
541
+ **Public Q&A threads, authenticated only:**
542
+
543
+ ```json
544
+ {
545
+ "threads": {
546
+ "allow": {
547
+ "anonymous": false,
548
+ "authenticated": true
549
+ },
550
+ "enforce": {
551
+ "authenticated": {
552
+ "visibility": "public",
553
+ "status": "open"
554
+ }
555
+ }
556
+ }
557
+ }
558
+ ```
559
+
560
+ **No public record creation:**
561
+
562
+ ```json
563
+ {
564
+ "records": {
565
+ "allow": {
566
+ "anonymous": false,
567
+ "authenticated": false
568
+ }
569
+ }
570
+ }
571
+ ```
572
+
573
+ The `enforce` values are **merged over** the caller's request body, so you can lock down fields like `visibility`, `status`, or `category` regardless of what clients send.
574
+
575
+ ---
576
+
577
+ ## Aggregations and Analytics
578
+
579
+ All three object types support powerful aggregation queries for dashboards and reports.
580
+
581
+ ### Aggregation Capabilities
582
+
583
+ ```typescript
584
+ interface AggregateRequest {
585
+ filters?: {
586
+ status?: string
587
+ category?: string // cases only
588
+ record_type?: string // records only
589
+ product_id?: string
590
+ created_at?: { gte?: string; lte?: string }
591
+ closed_at?: '__notnull__' | { gte?: string; lte?: string } // cases
592
+ expires_at?: { lte?: string } // records
593
+ }
594
+ groupBy?: string[] // dimension breakdown
595
+ metrics?: string[] // calculated values
596
+ timeSeriesField?: string
597
+ timeSeriesInterval?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
598
+ }
599
+ ```
600
+
601
+ ### Cases Aggregations
602
+
603
+ **Group by dimensions:**
604
+ `status`, `priority`, `category`, `assigned_to`, `product_id`, `contact_id`
605
+
606
+ **Metrics:**
607
+ `count`, `avg_close_time`, `p50_close_time`, `p95_close_time`
608
+
609
+ ```typescript
610
+ // Average resolution time by category
611
+ const metrics = await cases.aggregate(collectionId, appId, {
612
+ filters: {
613
+ closed_at: '__notnull__'
614
+ },
615
+ groupBy: ['category'],
616
+ metrics: ['count', 'avg_close_time', 'p95_close_time']
617
+ }, true);
618
+
619
+ // Result:
620
+ // {
621
+ // groups: [
622
+ // { category: 'warranty', count: 45, avg_close_time_seconds: 7200, p95_close_time_seconds: 14400 },
623
+ // { category: 'support', count: 89, avg_close_time_seconds: 3600, p95_close_time_seconds: 10800 }
624
+ // ]
625
+ // }
626
+ ```
627
+
628
+ ### Threads Aggregations
629
+
630
+ **Group by dimensions:**
631
+ `status`, `author_type`, `product_id`, `visibility`, `contact_id`
632
+
633
+ **Metrics:**
634
+ `count`, `reply_count`
635
+
636
+ ```typescript
637
+ // Most active discussion authors
638
+ const authorStats = await threads.aggregate(collectionId, appId, {
639
+ groupBy: ['author_type'],
640
+ metrics: ['count', 'reply_count']
641
+ });
642
+ ```
643
+
644
+ ### Records Aggregations
645
+
646
+ **Group by dimensions:**
647
+ `status`, `record_type`, `product_id`, `author_type`, `visibility`, `contact_id`
648
+
649
+ **Metrics:**
650
+ `count`
651
+
652
+ ```typescript
653
+ // Bookings by status
654
+ const bookingStats = await records.aggregate(collectionId, appId, {
655
+ filters: {
656
+ record_type: 'service_appointment'
657
+ },
658
+ groupBy: ['status'],
659
+ metrics: ['count']
660
+ }, true);
661
+ ```
662
+
663
+ ### Time Series
664
+
665
+ Generate time-based charts:
666
+
667
+ ```typescript
668
+ // Cases created per week
669
+ const casesTrend = await cases.aggregate(collectionId, appId, {
670
+ timeSeriesField: 'created_at',
671
+ timeSeriesInterval: 'week',
672
+ metrics: ['count']
673
+ }, true);
674
+
675
+ // Result:
676
+ // {
677
+ // timeSeries: [
678
+ // { bucket: '2026-W07', count: 23 },
679
+ // { bucket: '2026-W08', count: 31 },
680
+ // { bucket: '2026-W09', count: 28 }
681
+ // ]
682
+ // }
683
+ ```
684
+
685
+ ---
686
+
687
+ ## Common Patterns
688
+
689
+ ### Pattern: Related Data
690
+
691
+ Cases have a built-in `related()` endpoint to fetch associated threads and records:
692
+
693
+ ```typescript
694
+ // Get all related content for a case
695
+ const related = await cases.related(collectionId, appId, caseId);
696
+ // Returns: { threads: [...], records: [...] }
697
+ ```
698
+
699
+ For threads and records, use parent linking:
700
+
701
+ ```typescript
702
+ // Create a thread about a case
703
+ await threads.create(collectionId, appId, {
704
+ parentType: 'case',
705
+ parentId: caseId,
706
+ body: { text: 'Follow-up discussion about this case' }
707
+ });
708
+
709
+ // List all threads for a case
710
+ const caseThreads = await threads.list(collectionId, appId, {
711
+ parentType: 'case',
712
+ parentId: caseId
713
+ });
714
+ ```
715
+
716
+ ### Pattern: Hierarchical Records
717
+
718
+ Use `parentType` and `parentId` to build hierarchies:
719
+
720
+ ```typescript
721
+ // Parent record: subscription
722
+ const subscription = await records.create(collectionId, appId, {
723
+ recordType: 'subscription',
724
+ data: { plan: 'premium', billingCycle: 'monthly' }
725
+ });
726
+
727
+ // Child records: invoices
728
+ await records.create(collectionId, appId, {
729
+ recordType: 'invoice',
730
+ parentType: 'subscription',
731
+ parentId: subscription.id,
732
+ data: { amount: 29.99, period: '2026-02' }
733
+ });
734
+
735
+ // List all invoices for a subscription
736
+ const invoices = await records.list(collectionId, appId, {
737
+ recordType: 'invoice',
738
+ parentType: 'subscription',
739
+ parentId: subscription.id
740
+ });
741
+ ```
742
+
743
+ ### Pattern: Audit Trails
744
+
745
+ Use admin-only records to log changes:
746
+
747
+ ```typescript
748
+ async function auditLog(action: string, details: any) {
749
+ await records.create(collectionId, appId, {
750
+ recordType: 'audit_log',
751
+ visibility: 'admin',
752
+ authorId: currentUser.id,
753
+ authorType: 'admin',
754
+ data: {
755
+ action,
756
+ timestamp: new Date().toISOString(),
757
+ ...details
758
+ }
759
+ }, true);
760
+ }
761
+
762
+ // Usage
763
+ await auditLog('case_reassigned', {
764
+ caseId: 'case_123',
765
+ from: 'user_jane',
766
+ to: 'user_bob'
767
+ });
768
+ ```
769
+
770
+ ### Pattern: Notifications
771
+
772
+ Combine with the realtime API to notify users of changes:
773
+
774
+ ```typescript
775
+ import { realtime } from '@proveanything/smartlinks';
776
+
777
+ // When a case is updated
778
+ await cases.update(collectionId, appId, caseId, { status: 'resolved' }, true);
779
+
780
+ // Notify the contact
781
+ await realtime.publish(collectionId, `contact:${contactId}`, {
782
+ type: 'case_resolved',
783
+ caseId,
784
+ message: 'Your support case has been resolved'
785
+ });
786
+ ```
787
+
788
+ ---
789
+
790
+ ## Best Practices
791
+
792
+ ### Use the Right Object Type
793
+
794
+ | Need | Use |
795
+ |------|-----|
796
+ | Track something that needs resolution | **Cases** |
797
+ | Build a discussion or comment system | **Threads** |
798
+ | Store time-sensitive or hierarchical data | **Records** |
799
+
800
+ ### Zone Allocation Strategy
801
+
802
+ - **`data`** — Put information that's safe for anyone to see (even if `visibility` is `owner`)
803
+ - **`owner`** — Store user-specific preferences, addresses, contact info
804
+ - **`admin`** — Keep internal notes, costs, sensitive metadata
805
+
806
+ ### Visibility Defaults
807
+
808
+ - **User-facing content** → `visibility: 'public'` (Q&A, reviews, forums)
809
+ - **Private user data** → `visibility: 'owner'` (support cases, bookings)
810
+ - **Internal data** → `visibility: 'admin'` (audit logs, analytics)
811
+
812
+ ### Indexing and Performance
813
+
814
+ For high-volume queries, consider:
815
+ - Filter by `status`, `recordType`, or `category` to reduce result sets
816
+ - Use `limit` and `offset` for pagination (max 500 per page)
817
+ - Use aggregations instead of fetching all records and counting client-side
818
+ - Index commonly filtered fields in Firestore if you add custom indexes
819
+
820
+ ### Status Conventions
821
+
822
+ While statuses are free-form strings, consider standard conventions:
823
+
824
+ **Cases:** `open`, `in_progress`, `waiting_customer`, `resolved`, `closed`
825
+ **Threads:** `open`, `closed`, `locked`, `archived`
826
+ **Records:** `active`, `inactive`, `expired`, `cancelled`
827
+
828
+ ---
829
+
830
+ ## Example: Complete Support System
831
+
832
+ Here's a full workflow combining all three object types:
833
+
834
+ ```typescript
835
+ import { cases, threads, records } from '@proveanything/smartlinks';
836
+
837
+ // 1. Customer submits a warranty claim (case)
838
+ const claim = await cases.create(collectionId, appId, {
839
+ visibility: 'owner',
840
+ category: 'warranty',
841
+ status: 'open',
842
+ priority: 2,
843
+ productId,
844
+ proofId,
845
+ contactId,
846
+ data: { issue: 'Defective battery' },
847
+ owner: { serialNumber: 'SN-123' }
848
+ });
849
+
850
+ // 2. Customer starts a discussion about the claim (thread)
851
+ const discussion = await threads.create(collectionId, appId, {
852
+ visibility: 'owner',
853
+ parentType: 'case',
854
+ parentId: claim.id,
855
+ title: 'Questions about my warranty claim',
856
+ body: { text: 'How long will the replacement take?' }
857
+ });
858
+
859
+ // 3. Admin replies to the discussion
860
+ await threads.reply(collectionId, appId, discussion.id, {
861
+ authorId: 'admin_sarah',
862
+ authorType: 'support_agent',
863
+ text: 'We'll ship a replacement within 2 business days'
864
+ }, true);
865
+
866
+ // 4. Admin approves and creates a shipping record
867
+ const shipment = await records.create(collectionId, appId, {
868
+ recordType: 'shipment',
869
+ parentType: 'case',
870
+ parentId: claim.id,
871
+ data: {
872
+ carrier: 'UPS',
873
+ tracking: 'UPS-123456789',
874
+ estimatedDelivery: '2026-02-28'
875
+ },
876
+ owner: {
877
+ shippingAddress: '123 Main St'
878
+ },
879
+ admin: {
880
+ cost: 25.00,
881
+ warehouse: 'CA-01'
882
+ }
883
+ }, true);
884
+
885
+ // 5. Admin updates case with history
886
+ await cases.appendHistory(collectionId, appId, claim.id, {
887
+ entry: {
888
+ action: 'replacement_shipped',
889
+ tracking: 'UPS-123456789'
890
+ },
891
+ historyTarget: 'owner',
892
+ status: 'in_progress'
893
+ });
894
+
895
+ // 6. Customer receives item, admin closes case
896
+ await cases.update(collectionId, appId, claim.id, {
897
+ status: 'resolved',
898
+ admin: { resolvedBy: 'admin_sarah', satisfactionScore: 5 }
899
+ }, true);
900
+
901
+ // 7. Generate analytics
902
+ const monthlyReport = await cases.summary(collectionId, appId, {
903
+ period: { from: '2026-02-01', to: '2026-02-28' }
904
+ });
905
+ ```
906
+
907
+ ---
908
+
909
+ ## TypeScript Usage
910
+
911
+ Import types and functions:
912
+
913
+ ```typescript
914
+ import {
915
+ cases, threads, records,
916
+ AppCase, AppThread, AppRecord,
917
+ CreateCaseInput, CreateThreadInput, CreateRecordInput,
918
+ PaginatedResponse, AggregateResponse
919
+ } from '@proveanything/smartlinks';
920
+
921
+ // Fully typed
922
+ const newCase: AppCase = await cases.create(collectionId, appId, {
923
+ category: 'support',
924
+ data: { issue: 'Login problem' }
925
+ });
926
+
927
+ const threadList: PaginatedResponse<AppThread> = await threads.list(
928
+ collectionId,
929
+ appId,
930
+ { limit: 50, sort: 'createdAt:desc' }
931
+ );
932
+ ```
933
+
934
+ ---
935
+
936
+ ## API Reference
937
+
938
+ For complete endpoint documentation, query parameters, and response schemas, see:
939
+
940
+ - [API_SUMMARY.md](./API_SUMMARY.md) — Full REST API reference
941
+ - [TypeScript source](../src/types/appObjects.ts) — Type definitions
942
+ - [API wrappers](../src/api/appObjects.ts) — Implementation
943
+
944
+ ---
945
+
946
+ ## Questions?
947
+
948
+ These three object types are incredibly flexible building blocks. If you're unsure which to use for your use case, ask yourself:
949
+
950
+ - Does it need tracking to closure? → **Case**
951
+ - Is it a conversation or discussion? → **Thread**
952
+ - Is it data with a lifecycle or hierarchy? → **Record**
953
+
954
+ When in doubt, start with **Records** — they're the most generic and can be shaped to fit almost anything.