@proveanything/smartlinks
Version:
Official JavaScript/TypeScript SDK for the Smartlinks API
1,041 lines (840 loc) • 30.7 kB
Markdown
# App Objects: Cases, Threads, and Records
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**.
## Overview
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:
- **Cases** — Track issues, requests, or tasks that need resolution
- **Threads** — Manage discussions, comments, or any reply-based content
- **Records** — Store structured data with flexible lifecycles
Each object type supports:
- **JSONB zones** (`data`, `owner`, `admin`) for granular access control
- **Visibility levels** (`public`, `owner`, `admin`) for content exposure
- **Flexible schemas** — store any JSON in the zone fields
- **Admin and public endpoints** for different caller contexts
- **Rich querying** with filters, sorting, pagination, and aggregations
```text
┌─────────────────────────────────────────────────────────────────┐
│ Your SmartLinks App │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Cases │ │ Threads │ │ Records │ │
│ │ │ │ │ │ │ │
│ │ • Support │ │ • Comments │ │ • Bookings │ │
│ │ • Warranty │ │ • Q&A │ │ • Licenses │ │
│ │ • Feedback │ │ • Reviews │ │ • Visits │ │
│ │ • RMA │ │ • Forum │ │ • Events │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ All scoped to: /collection/:cId/app/:appId │
└─────────────────────────────────────────────────────────────────┘
```
## The JSONB Zone Model
All three object types use a three-tier access model with JSONB fields:
| Zone | Visible to | Writable by | Use Case |
|---------|-------------------|-------------------|-----------------------------------|
| `data` | public, owner, admin | public, owner, admin | Shared public information |
| `owner` | owner, admin | owner, admin | User-specific private data |
| `admin` | admin | admin | Internal notes, sensitive data |
### How Zones Work
Zones are **automatically filtered** based on the caller's role:
```typescript
// Public endpoint caller sees:
{
id: 'case_123',
status: 'open',
data: { issue: 'Screen cracked', photos: [...] },
// owner and admin zones stripped
}
// Owner (authenticated contact) sees:
{
id: 'case_123',
status: 'open',
data: { issue: 'Screen cracked', photos: [...] },
owner: { shippingAddress: '...', preference: 'email' },
// admin zone stripped
}
// Admin sees everything:
{
id: 'case_123',
status: 'open',
data: { issue: 'Screen cracked', photos: [...] },
owner: { shippingAddress: '...', preference: 'email' },
admin: { internalNotes: 'Escalate to tier 2', cost: 45.00 }
}
```
**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.
### Zone Writing Rules
- **Non-admin callers** attempting to write to the `admin` zone are silently ignored
- **Public callers** can write to `data` and `owner` (if visibility allows)
- **Admins** can write to all three zones
## Visibility Levels
Each object has a `visibility` field that controls who can access it on **public endpoints**:
| Visibility | Public Endpoint Behavior |
|------------|--------------------------------------------------|
| `public` | Anyone can read (even anonymous) |
| `owner` | Only the owning contact can read |
| `admin` | Never visible on public endpoints (404) |
**Admin endpoints** always return all objects regardless of visibility.
### Typical Patterns
```typescript
// Public discussion thread
await app.threads.create(collectionId, appId, {
visibility: 'public',
title: 'How do I clean this product?',
body: { text: 'Looking for cleaning instructions...' }
});
// Private support case
await app.cases.create(collectionId, appId, {
visibility: 'owner', // Only this contact can see it
category: 'warranty',
data: { issue: 'Defective unit' },
owner: { serialNumber: 'ABC123' }
});
// Admin-only internal record
await app.records.create(collectionId, appId, {
visibility: 'admin', // Never appears on public endpoints
recordType: 'audit_log',
admin: { action: 'manual_refund', amount: 50.00 }
}, true); // admin = true
```
## Paginated List Responses
Every `.list()` call returns a **`PaginatedResponse<T>`** object. The items are in the `data` array and all page-level metadata lives in a nested `pagination` object:
```json
{
"data": [
{
"id": "7ac44316-c227-4c39-bf99-a287bc08c6f5",
"collectionId": "veho-demo",
"appId": "knowledgeBase",
"visibility": "public",
"recordType": "article",
"status": "published",
"createdAt": "2026-02-25T22:13:14.310Z",
"updatedAt": "2026-02-25T22:47:36.712Z",
"data": {
"title": "Getting Started",
"slug": "getting-started",
"body": "..."
}
}
],
"pagination": {
"total": 42,
"limit": 10,
"offset": 0,
"hasMore": true
}
}
```
### Pagination fields
| Field | Type | Description |
|---|---|---|
| `data` | `T[]` | The page of items returned |
| `pagination.total` | `number` | Total number of matching records across **all** pages |
| `pagination.limit` | `number` | The `limit` that was applied to this request (default `50`, max `500`) |
| `pagination.offset` | `number` | The `offset` that was applied to this request |
| `pagination.hasMore` | `boolean` | `true` when more pages exist — use this instead of computing `offset + limit < total` yourself |
> **Note:** The items are always in `response.data`, **not** at the top level. A common mistake is reading `response.total` — the correct path is `response.pagination.total`.
### Fetching all pages
```typescript
import { app, PaginatedResponse, AppRecord } from '@proveanything/smartlinks';
async function fetchAllRecords(collectionId: string, appId: string) {
const results: AppRecord[] = [];
let offset = 0;
const limit = 100;
while (true) {
const page: PaginatedResponse<AppRecord> = await app.records.list(
collectionId,
appId,
{ limit, offset, sort: 'createdAt:desc' }
);
results.push(...page.data);
if (!page.pagination.hasMore) break; // no more pages
offset += limit;
}
console.log(`Fetched ${results.length} of ${/* saved from first page */ 0} total`);
return results;
}
```
### Reading the count and checking for more
```typescript
const page = await app.cases.list(collectionId, appId, { status: 'open', limit: 10 });
console.log(page.data); // array of AppCase objects
console.log(page.pagination.total); // e.g. 142 — total open cases
console.log(page.pagination.hasMore); // true / false
console.log(page.pagination.offset); // current page start
console.log(page.pagination.limit); // items per page
```
## Cases
**Cases** represent trackable issues, requests, or tasks that move through states and require resolution.
### When to Use Cases
- **Customer support tickets** — track issues from creation to resolution
- **Warranty claims** — manage claims with status, priority, and assignment
- **Feature requests** — collect and triage user feedback
- **RMA (Return Merchandise Authorization)** — handle product returns
- **Bug reports** — track defects from user submissions
- **Service requests** — manage appointments, repairs, installations
### Key Features
- **Status lifecycle** — `'open'` → `'in-progress'` → `'resolved'` → `'closed'` (or custom statuses)
- **Priority levels** — numerical priority for sorting/escalation
- **Categories** — group cases by type (warranty, bug, feature, etc.)
- **Assignment** — `assignedTo` field for routing to team members
- **History tracking** — append timestamped entries to `admin.history` or `owner.history`
- **Closing metrics** — track `closedAt` to measure resolution time
### Example: Warranty Claims
```typescript
import { app } from '@proveanything/smartlinks';
// Customer submits a warranty claim (public endpoint)
const claim = await app.cases.create(collectionId, appId, {
visibility: 'owner',
category: 'warranty',
status: 'open',
priority: 2,
productId: product.id,
proofId: proof.id,
contactId: user.contactId,
data: {
issue: 'Screen flickering after 3 months',
photos: ['https://...', 'https://...']
},
owner: {
purchaseDate: '2025-11-15',
serialNumber: 'SN-7738291',
preferredContact: 'email'
}
});
// Admin reviews and assigns (admin endpoint)
await app.cases.update(collectionId, appId, claim.id, {
assignedTo: 'user_jane_support',
priority: 3, // escalate
admin: {
internalNotes: 'Likely hardware defect, approve replacement'
}
}, true); // admin = true
// Admin appends to history
await app.cases.appendHistory(collectionId, appId, claim.id, {
entry: {
action: 'approved_replacement',
agent: 'Jane',
tracking: 'UPS-123456789'
},
historyTarget: 'owner', // visible to customer
status: 'resolved'
});
// Get case summary stats (admin)
const summary = await app.cases.summary(collectionId, appId, {
period: { from: '2026-01-01', to: '2026-02-28' }
});
// Returns: { total: 142, byStatus: { open: 12, resolved: 130 }, ... }
```
### Use Case: Support Dashboard
Build a live support dashboard showing open cases by priority:
```typescript
const openCases = await app.cases.list(collectionId, appId, {
status: 'open',
sort: 'priority:desc',
limit: 50
}, true);
// Aggregate by category
const stats = await app.cases.aggregate(collectionId, appId, {
filters: { status: 'open' },
groupBy: ['category', 'priority'],
metrics: ['count']
}, true);
// Time series: cases created per week
const trend = await app.cases.aggregate(collectionId, appId, {
timeSeriesField: 'created_at',
timeSeriesInterval: 'week',
metrics: ['count']
}, true);
```
## Threads
**Threads** represent discussions, comments, or any content that accumulates replies over time.
### When to Use Threads
- **Product Q&A** — questions and answers about products
- **Community forums** — discussions grouped by topic
- **Comments** — on products, proofs, or other resources
- **Review discussions** — follow-up questions on reviews
- **Feedback threads** — ongoing conversations about features
- **Support chat** — lightweight message threads
### Key Features
- **Reply tracking** — `replies` array with timestamped entries
- **Reply count** — auto-incremented `replyCount` and `lastReplyAt`
- **Slugs** — optional URL-friendly slug for pretty URLs
- **Tags** — JSONB array of tags for categorization
- **Parent linking** — `parentType` + `parentId` to attach to products, proofs, etc.
- **Author metadata** — track `authorId` and `authorType`
### Example: Product Q&A
```typescript
import { app } from '@proveanything/smartlinks';
// Customer asks a question (public endpoint)
const question = await app.threads.create(collectionId, appId, {
visibility: 'public',
slug: 'how-to-clean-leather',
title: 'How do I clean leather without damaging it?',
status: 'open',
authorId: user.contactId,
authorType: 'customer',
productId: product.id,
body: {
text: 'I spilled coffee on my leather bag. What cleaner is safe to use?'
},
tags: ['cleaning', 'leather', 'care']
});
// Another customer replies
await app.threads.reply(collectionId, appId, question.id, {
authorId: otherUser.contactId,
authorType: 'customer',
text: 'I use a mild soap and water solution. Works great!'
});
// Admin (brand expert) replies
await app.threads.reply(collectionId, appId, question.id, {
authorId: 'user_expert_sarah',
authorType: 'brand_expert',
text: 'Our official leather care kit is perfect for this. Avoid harsh chemicals.',
productLink: 'prod_leather_care_kit'
}, true); // admin endpoint
// Admin marks as resolved
await app.threads.update(collectionId, appId, question.id, {
status: 'resolved'
}, true);
```
### Use Case: Forum-Style Discussions
List recent discussions with reply counts:
```typescript
// Get active threads
const activeThreads = await app.threads.list(collectionId, appId, {
status: 'open',
sort: 'lastReplyAt:desc',
limit: 20
});
// Filter by tag
const cleaningThreads = await app.threads.list(collectionId, appId, {
tag: 'cleaning'
});
// Aggregate: most active discussion topics
const topicStats = await app.threads.aggregate(collectionId, appId, {
groupBy: ['status'],
metrics: ['count', 'reply_count']
});
```
### Use Case: Product Comments
Attach comments to a specific product:
```typescript
// Create a comment thread for a product
await app.threads.create(collectionId, appId, {
visibility: 'public',
parentType: 'product',
parentId: product.id,
authorId: user.contactId,
body: { text: 'Love this product! Best purchase ever.' },
tags: ['positive']
});
// List all comments for a product
const productComments = await app.threads.list(collectionId, appId, {
parentType: 'product',
parentId: product.id,
sort: 'createdAt:desc'
});
```
## Records
**Records** are the most flexible object type — use them for structured data with time-based lifecycles, hierarchies, or custom schemas.
### When to Use Records
- **Bookings/Reservations** — track appointments with start/end times
- **Licenses** — manage software licenses with expiration
- **Subscriptions** — track subscription status and renewal
- **Certifications** — store certifications with expiry dates
- **Events** — track event registrations and attendance
- **Usage logs** — record product usage metrics
- **Audit trails** — immutable logs of actions
- **Loyalty points** — track points earned/redeemed
### Key Features
- **Record types** — `recordType` field for categorization (required)
- **Time windows** — `startsAt` and `expiresAt` for time-based data
- **Parent linking** — attach to products, proofs, contacts, etc.
- **Author tracking** — `authorId` + `authorType`
- **Status lifecycle** — custom statuses (default `'active'`)
- **References** — optional `ref` field for external IDs
### Example: Product Registration
```typescript
import { app } from '@proveanything/smartlinks';
// Customer registers a product
const registration = await app.records.create(collectionId, appId, {
recordType: 'product_registration',
visibility: 'owner',
status: 'active',
productId: product.id,
proofId: proof.id,
contactId: user.contactId,
authorId: user.contactId,
authorType: 'customer',
startsAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 365*24*60*60*1000).toISOString(), // 1 year warranty
data: {
registrationNumber: 'REG-2026-1234',
purchaseDate: '2026-02-15',
retailer: 'Best Electronics'
},
owner: {
serialNumber: 'SN-9922736',
installDate: '2026-02-20',
location: 'Home office'
}
});
// List active registrations for a customer
const activeRegistrations = await app.records.list(collectionId, appId, {
contactId: user.contactId,
recordType: 'product_registration',
status: 'active'
});
// Find expiring registrations (admin)
const expiringSoon = await app.records.list(collectionId, appId, {
recordType: 'product_registration',
expiresAt: `lte:${new Date(Date.now() + 30*24*60*60*1000).toISOString()}` // next 30 days
}, true);
```
### Example: Appointment Booking
```typescript
// Customer books a service appointment
const booking = await app.records.create(collectionId, appId, {
recordType: 'service_appointment',
visibility: 'owner',
contactId: user.contactId,
startsAt: '2026-03-15T10:00:00Z',
expiresAt: '2026-03-15T11:00:00Z', // 1-hour appointment
data: {
serviceType: 'installation',
location: 'Customer site',
technician: null // assigned later
},
owner: {
address: '123 Main St',
phone: '555-1234',
notes: 'Call before arrival'
}
});
// Admin assigns technician
await app.records.update(collectionId, appId, booking.id, {
data: {
serviceType: 'installation',
location: 'Customer site',
technician: 'tech_john'
},
admin: {
cost: 150.00,
travelTime: 30
}
}, true);
// List today's appointments
const today = new Date().toISOString().split('T')[0];
const todaysAppointments = await app.records.list(collectionId, appId, {
recordType: 'service_appointment',
startsAt: `gte:${today}T00:00:00Z`,
sort: 'startsAt:asc'
}, true);
```
### Example: Usage Tracking
```typescript
// Log product usage (could be triggered by IoT device)
await app.records.create(collectionId, appId, {
recordType: 'usage_log',
visibility: 'admin',
productId: product.id,
proofId: proof.id,
startsAt: new Date().toISOString(),
data: {
metric: 'power_on',
duration: 3600, // seconds
location: 'geo:37.7749,-122.4194'
}
}, true);
// Aggregate usage metrics
const usageStats = await app.records.aggregate(collectionId, appId, {
filters: {
record_type: 'usage_log',
created_at: {
gte: '2026-02-01',
lte: '2026-02-28'
}
},
groupBy: ['product_id'],
metrics: ['count']
}, true);
```
## Public Create Policies
Control who can create objects on **public endpoints** using Firestore-based policies at:
`sites/{collectionId}/apps/{appId}.publicCreate`
### Policy Structure
```typescript
interface PublicCreatePolicy {
cases?: {
allow: {
anonymous?: boolean // allow unauthenticated users
authenticated?: boolean // allow authenticated contacts
}
enforce?: {
anonymous?: Partial<CreateCaseInput> // force these values for anon
authenticated?: Partial<CreateCaseInput> // force these values for auth
}
}
threads?: { /* same structure */ }
records?: { /* same structure */ }
}
```
### Example Policies
**Support tickets from anyone:**
```json
{
"cases": {
"allow": {
"anonymous": true,
"authenticated": true
},
"enforce": {
"anonymous": {
"visibility": "owner",
"status": "open",
"category": "support"
},
"authenticated": {
"visibility": "owner",
"status": "open"
}
}
}
}
```
**Public Q&A threads, authenticated only:**
```json
{
"threads": {
"allow": {
"anonymous": false,
"authenticated": true
},
"enforce": {
"authenticated": {
"visibility": "public",
"status": "open"
}
}
}
}
```
**No public record creation:**
```json
{
"records": {
"allow": {
"anonymous": false,
"authenticated": false
}
}
}
```
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.
## Aggregations and Analytics
All three object types support powerful aggregation queries for dashboards and reports.
### Aggregation Capabilities
```typescript
interface AggregateRequest {
filters?: {
status?: string
category?: string // cases only
record_type?: string // records only
product_id?: string
created_at?: { gte?: string; lte?: string }
closed_at?: '__notnull__' | { gte?: string; lte?: string } // cases
expires_at?: { lte?: string } // records
}
groupBy?: string[] // dimension breakdown
metrics?: string[] // calculated values
timeSeriesField?: string
timeSeriesInterval?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
}
```
### Cases Aggregations
**Group by dimensions:**
`status`, `priority`, `category`, `assigned_to`, `product_id`, `contact_id`
**Metrics:**
`count`, `avg_close_time`, `p50_close_time`, `p95_close_time`
```typescript
// Average resolution time by category
const metrics = await app.cases.aggregate(collectionId, appId, {
filters: {
closed_at: '__notnull__'
},
groupBy: ['category'],
metrics: ['count', 'avg_close_time', 'p95_close_time']
}, true);
// Result:
// {
// groups: [
// { category: 'warranty', count: 45, avg_close_time_seconds: 7200, p95_close_time_seconds: 14400 },
// { category: 'support', count: 89, avg_close_time_seconds: 3600, p95_close_time_seconds: 10800 }
// ]
// }
```
### Threads Aggregations
**Group by dimensions:**
`status`, `author_type`, `product_id`, `visibility`, `contact_id`
**Metrics:**
`count`, `reply_count`
```typescript
// Most active discussion authors
const authorStats = await app.threads.aggregate(collectionId, appId, {
groupBy: ['author_type'],
metrics: ['count', 'reply_count']
});
```
### Records Aggregations
**Group by dimensions:**
`status`, `record_type`, `product_id`, `author_type`, `visibility`, `contact_id`
**Metrics:**
`count`
```typescript
// Bookings by status
const bookingStats = await app.records.aggregate(collectionId, appId, {
filters: {
record_type: 'service_appointment'
},
groupBy: ['status'],
metrics: ['count']
}, true);
```
### Time Series
Generate time-based charts:
```typescript
// Cases created per week
const casesTrend = await app.cases.aggregate(collectionId, appId, {
timeSeriesField: 'created_at',
timeSeriesInterval: 'week',
metrics: ['count']
}, true);
// Result:
// {
// timeSeries: [
// { bucket: '2026-W07', count: 23 },
// { bucket: '2026-W08', count: 31 },
// { bucket: '2026-W09', count: 28 }
// ]
// }
```
## Common Patterns
### Pattern: Related Data
Cases have a built-in `related()` endpoint to fetch associated threads and records:
```typescript
// Get all related content for a case
const related = await app.cases.related(collectionId, appId, caseId);
// Returns: { threads: [...], records: [...] }
```
For threads and records, use parent linking:
```typescript
// Create a thread about a case
await app.threads.create(collectionId, appId, {
parentType: 'case',
parentId: caseId,
body: { text: 'Follow-up discussion about this case' }
});
// List all threads for a case
const caseThreads = await app.threads.list(collectionId, appId, {
parentType: 'case',
parentId: caseId
});
```
### Pattern: Hierarchical Records
Use `parentType` and `parentId` to build hierarchies:
```typescript
// Parent record: subscription
const subscription = await app.records.create(collectionId, appId, {
recordType: 'subscription',
data: { plan: 'premium', billingCycle: 'monthly' }
});
// Child records: invoices
await app.records.create(collectionId, appId, {
recordType: 'invoice',
parentType: 'subscription',
parentId: subscription.id,
data: { amount: 29.99, period: '2026-02' }
});
// List all invoices for a subscription
const invoices = await app.records.list(collectionId, appId, {
recordType: 'invoice',
parentType: 'subscription',
parentId: subscription.id
});
```
### Pattern: Audit Trails
Use admin-only records to log changes:
```typescript
async function auditLog(action: string, details: any) {
await app.records.create(collectionId, appId, {
recordType: 'audit_log',
visibility: 'admin',
authorId: currentUser.id,
authorType: 'admin',
data: {
action,
timestamp: new Date().toISOString(),
...details
}
}, true);
}
// Usage
await auditLog('case_reassigned', {
caseId: 'case_123',
from: 'user_jane',
to: 'user_bob'
});
```
### Pattern: Notifications
Combine with the realtime API to notify users of changes:
```typescript
import { app, realtime } from '@proveanything/smartlinks';
// When a case is updated
await app.cases.update(collectionId, appId, caseId, { status: 'resolved' }, true);
// Notify the contact
await realtime.publish(collectionId, `contact:${contactId}`, {
type: 'case_resolved',
caseId,
message: 'Your support case has been resolved'
});
```
## Best Practices
### Use the Right Object Type
| Need | Use |
|------|-----|
| Track something that needs resolution | **Cases** |
| Build a discussion or comment system | **Threads** |
| Store time-sensitive or hierarchical data | **Records** |
### Zone Allocation Strategy
- **`data`** — Put information that's safe for anyone to see (even if `visibility` is `owner`)
- **`owner`** — Store user-specific preferences, addresses, contact info
- **`admin`** — Keep internal notes, costs, sensitive metadata
### Visibility Defaults
- **User-facing content** → `visibility: 'public'` (Q&A, reviews, forums)
- **Private user data** → `visibility: 'owner'` (support cases, bookings)
- **Internal data** → `visibility: 'admin'` (audit logs, analytics)
### Indexing and Performance
For high-volume queries, consider:
- Filter by `status`, `recordType`, or `category` to reduce result sets
- Use `limit` and `offset` for pagination (max 500 per page)
- Use aggregations instead of fetching all records and counting client-side
- Index commonly filtered fields in Firestore if you add custom indexes
### Status Conventions
While statuses are free-form strings, consider standard conventions:
**Cases:** `open`, `in_progress`, `waiting_customer`, `resolved`, `closed`
**Threads:** `open`, `closed`, `locked`, `archived`
**Records:** `active`, `inactive`, `expired`, `cancelled`
## Example: Complete Support System
Here's a full workflow combining all three object types:
```typescript
import { app } from '@proveanything/smartlinks';
// 1. Customer submits a warranty claim (case)
const claim = await app.cases.create(collectionId, appId, {
visibility: 'owner',
category: 'warranty',
status: 'open',
priority: 2,
productId,
proofId,
contactId,
data: { issue: 'Defective battery' },
owner: { serialNumber: 'SN-123' }
});
// 2. Customer starts a discussion about the claim (thread)
const discussion = await app.threads.create(collectionId, appId, {
visibility: 'owner',
parentType: 'case',
parentId: claim.id,
title: 'Questions about my warranty claim',
body: { text: 'How long will the replacement take?' }
});
// 3. Admin replies to the discussion
await app.threads.reply(collectionId, appId, discussion.id, {
authorId: 'admin_sarah',
authorType: 'support_agent',
text: 'We'll ship a replacement within 2 business days'
}, true);
// 4. Admin approves and creates a shipping record
const shipment = await app.records.create(collectionId, appId, {
recordType: 'shipment',
parentType: 'case',
parentId: claim.id,
data: {
carrier: 'UPS',
tracking: 'UPS-123456789',
estimatedDelivery: '2026-02-28'
},
owner: {
shippingAddress: '123 Main St'
},
admin: {
cost: 25.00,
warehouse: 'CA-01'
}
}, true);
// 5. Admin updates case with history
await app.cases.appendHistory(collectionId, appId, claim.id, {
entry: {
action: 'replacement_shipped',
tracking: 'UPS-123456789'
},
historyTarget: 'owner',
status: 'in_progress'
});
// 6. Customer receives item, admin closes case
await app.cases.update(collectionId, appId, claim.id, {
status: 'resolved',
admin: { resolvedBy: 'admin_sarah', satisfactionScore: 5 }
}, true);
// 7. Generate analytics
const monthlyReport = await app.cases.summary(collectionId, appId, {
period: { from: '2026-02-01', to: '2026-02-28' }
});
```
## TypeScript Usage
Import types and functions:
```typescript
import {
app,
AppCase, AppThread, AppRecord,
CreateCaseInput, CreateThreadInput, CreateRecordInput,
PaginatedResponse, AggregateResponse
} from '@proveanything/smartlinks';
// Fully typed
const newCase: AppCase = await app.cases.create(collectionId, appId, {
category: 'support',
data: { issue: 'Login problem' }
});
const threadList: PaginatedResponse<AppThread> = await app.threads.list(
collectionId,
appId,
{ limit: 50, sort: 'createdAt:desc' }
);
```
## API Reference
For complete endpoint documentation, query parameters, and response schemas, see:
- [API_SUMMARY.md](./API_SUMMARY.md) — Full REST API reference
- [TypeScript source](../src/types/appObjects.ts) — Type definitions
- [API wrappers](../src/api/appObjects.ts) — Implementation
## Questions?
These three object types are incredibly flexible building blocks. If you're unsure which to use for your use case, ask yourself:
- Does it need tracking to closure? → **Case**
- Is it a conversation or discussion? → **Thread**
- Is it data with a lifecycle or hierarchy? → **Record**
When in doubt, start with **Records** — they're the most generic and can be shaped to fit almost anything.