flowstack-sdk 0.2.1

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.
package/README.md ADDED
@@ -0,0 +1,2278 @@
1
+ # Flowstack SDK
2
+
3
+ Complete Backend-as-a-Service SDK for AI-powered applications. Build full-stack AI apps with authentication, workspace management, dataset handling, AI agent integration, published sites, and wallet-based payments β€” all in one package.
4
+
5
+ ```bash
6
+ npm install flowstack-sdk
7
+ ```
8
+
9
+ ## Documentation
10
+
11
+ - πŸ“– **Docs site:** https://flowstack.fun/docs β€” browsable reference for every hook, component, and utility.
12
+ - πŸ”Œ **REST & SSE API:** https://flowstack.fun/openapi.json (OpenAPI 3.1) Β· human view: https://flowstack.fun/docs/rest-api
13
+ - πŸ€– **For LLMs & coding agents:** https://flowstack.fun/llms.txt (index) Β· https://flowstack.fun/llms-full.txt (full corpus)
14
+ - 🧠 **Machine-readable:** [semantic graph](https://flowstack.fun/graph.jsonld) · [search index](https://flowstack.fun/search-index.json) · [registry JSON](https://flowstack.fun/api/registry.json)
15
+
16
+ The docs site is generated from a canonical registry, so the human docs, the agent-readable corpus, the semantic graph, and the API contract never drift apart. This README is the canonical source for the SDK reference.
17
+
18
+ ---
19
+
20
+ ## MCP Build Agent β€” Required Pre-Flight Checklist
21
+
22
+ > **If you are an AI agent using the Casino MCP tools (`casino_upload_source`, `casino_build_staged`, etc.), read this section before writing any code. These are hard constraints from production failures.**
23
+
24
+ ### File upload rules
25
+ - `upload_source` requires ALL files in one call. The sandbox is fresh every build β€” no files persist from previous runs.
26
+ - Required minimum: `package.json`, `index.html`, `vite.config.ts`, `tsconfig.json`, `tsconfig.node.json`, `src/main.tsx`, `src/App.tsx`, and every file they import.
27
+ - `casino_get_app_source` returns `{}` (empty) after most builds β€” do NOT rely on it to retrieve previous source. Regenerate from scratch.
28
+ - `casino_stage_file` (singular) is removed. Use `casino_upload_source` with all files in a single dict.
29
+
30
+ ### Buffer polyfill β€” FIRST lines of `src/main.tsx`
31
+ ```ts
32
+ import { Buffer } from 'buffer';
33
+ if (typeof (globalThis as any).Buffer === 'undefined') { (globalThis as any).Buffer = Buffer; }
34
+ // all other imports below
35
+ ```
36
+ Without this: `"Can't find variable: Buffer"` crash at runtime. Add `"buffer": "^6.0.3"` to `package.json` dependencies.
37
+
38
+ ### `vite.config.ts` β€” always include
39
+ ```ts
40
+ export default defineConfig({ base: '/', plugins: [react()], define: { global: 'globalThis' } });
41
+ ```
42
+
43
+ ### `package.json` β€” always include `mermaid` and `buffer`
44
+ ```json
45
+ "mermaid": "^11.4.1", "buffer": "^6.0.3"
46
+ ```
47
+ Missing `mermaid` β†’ Rollup dynamic-import error. Missing `buffer` β†’ Buffer polyfill fails.
48
+
49
+ ### Authentication β€” `BrokeredLoginButton` only
50
+ ```tsx
51
+ import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';
52
+ // Never use useAuth().login(), LoginButton from flowstack-sdk/wallet, or privyConfig.appId
53
+ ```
54
+
55
+ ### Styles β€” inline or upload, never import without uploading
56
+ If `main.tsx` has `import './styles.css'`, upload `src/styles.css` too. Or inline styles as a JS style element in `main.tsx` and remove the import.
57
+
58
+ ### `AuthGuard` β€” never use `redirectTo`
59
+ Use `fallback={<BrokeredLoginButton />}` instead. `redirectTo="/"` kicks users out before they can sign in. For built apps with an `appScope`, `AuthGuard` can also transparently issue a guest session (`allowGuest`, default `true`) when the site has opted into guest chat β€” see [Add authentication](#2-add-authentication).
60
+
61
+ ### CRITICAL β€” `upload_source` creates a DRAFT, not a live update
62
+
63
+ `casino_upload_source` returns `version_status: "draft"`. **The live site does NOT change until you call `casino_promote_version`.** Users are still on the old version. Always complete the two-step workflow:
64
+
65
+ ```
66
+ 1. casino_upload_source(site_id, files) β†’ check status="success", note version number
67
+ 2. casino_promote_version(site_id, version) β†’ NOW the site goes live
68
+ ```
69
+
70
+ If you skip step 2, the build succeeded but nobody sees it. There is no automatic promotion.
71
+
72
+ **Simpler alternative:** Use `casino_build_staged` which builds AND goes live in one call (no promote needed):
73
+ ```
74
+ 1. casino_upload_source(...) ← build + validate as draft
75
+ OR
76
+ casino_build_staged(...) ← build + go live immediately (preferred for updates)
77
+ ```
78
+
79
+ ### If build fails, read `build_log`:
80
+ - `"No package.json"` β†’ add it to the files dict
81
+ - `"Could not resolve './X'"` β†’ add the missing file
82
+ - `"Cannot find variable: Buffer"` β†’ Buffer polyfill missing or not the first lines
83
+
84
+ ---
85
+
86
+ ## Table of Contents
87
+
88
+ - [Installation](#installation)
89
+ - [**Built Apps β€” Generated Site Pattern**](#built-apps--generated-site-pattern)
90
+ - [Quick Start (Casino Platform)](#quick-start-casino-platform)
91
+ - [Complete Example App (Casino Platform)](#complete-example-app-casino-platform)
92
+ - [Hooks Reference](#hooks-reference)
93
+ - [useAuth](#useauth)
94
+ - [useCollection](#usecollection)
95
+ - [usePublicCollection](#usepubliccollection)
96
+ - [useWorkspace](#useworkspace)
97
+ - [useAgent](#useagent)
98
+ - [useDatasets](#usedatasets)
99
+ - [useVisualizations](#usevisualizations)
100
+ - [useReports](#usereports)
101
+ - [useModels](#usemodels)
102
+ - [useSites](#usesites)
103
+ - [useDataSources](#usedatasources)
104
+ - [useQuery](#usequery)
105
+ - [useUserManagement](#useusermanagement)
106
+ - [useAuthGuard](#useauthguard)
107
+ - [useFlowstackStatus](#useflowstackstatus)
108
+ - [Additional Hooks (0.5+)](#additional-hooks-05)
109
+ - [Components](#components)
110
+ - [Wallet Module](#wallet-module)
111
+ - [Agent Templates & Factory](#agent-templates--factory)
112
+ - [API Client](#api-client)
113
+ - [Streaming Utilities](#streaming-utilities)
114
+ - [Cache Utilities](#cache-utilities)
115
+ - [API Route Generators](#api-route-generators)
116
+ - [Mock Mode](#mock-mode)
117
+ - [Configuration](#configuration)
118
+ - [Error Handling](#error-handling)
119
+ - [TypeScript](#typescript)
120
+
121
+ ## Installation
122
+
123
+ For generated sites (CDN tarball β€” no registry auth needed):
124
+
125
+ ```json
126
+ {
127
+ "dependencies": {
128
+ "flowstack-sdk": "https://sagecdn.flowstack.fun/sdk/flowstack-sdk-latest.tgz"
129
+ }
130
+ }
131
+ ```
132
+
133
+ For local development:
134
+
135
+ ```bash
136
+ npm install flowstack-sdk
137
+ # or with local copy:
138
+ # "flowstack-sdk": "file:packages/flowstack-sdk"
139
+ ```
140
+
141
+ ## Built Apps β€” Generated Site Pattern
142
+
143
+ **This is the primary pattern for sites generated by the build pipeline.** Built apps use `appScope` for per-app data isolation β€” they do NOT use workspaces.
144
+
145
+ ### Architecture
146
+
147
+ ```
148
+ useCollection β†’ /collections/* β†’ MongoDB (the app reads/writes data β€” tables, forms, cards)
149
+ useAgent β†’ /stream β†’ LLM Agent (chat AND one-shot tasks that write results to MongoDB)
150
+ ```
151
+
152
+ - **`useCollection`** is how the app reads and directly writes data (read, insert, update, delete). Direct MongoDB, no agent.
153
+ - **`useAgent`** handles chat *and* acts as a worker: a one-shot `query()` can have the agent **reason over data you pass in and write a structured result document** to a collection via its `data_access` tools, which the app then reads back with `useCollection`. See [Agent-Driven Data Models β€” Reason β†’ Write β†’ Read](#agent-driven-data-models--reason--write--read). MongoDB is schemaless, so the agent can write any document shape (nested objects/arrays, optional fields) with no migration β€” the prompt defines the shape.
154
+
155
+ ### Minimal Built App
156
+
157
+ ```tsx
158
+ import { FlowstackProvider, useFlowstack, BrokeredLoginButton, useCollection, useAgent } from 'flowstack-sdk';
159
+
160
+ function App() {
161
+ return (
162
+ <FlowstackProvider config={{
163
+ baseUrl: 'https://sage-api.flowstack.fun',
164
+ mode: 'production',
165
+ appScope: '__SITE_ID__', // Hex site ID β€” auto-filled by build pipeline
166
+ // No tenantId needed β€” it's derived from the brokered-login JWT. Only set it
167
+ // if you use usePublicCollection (anonymous data has no token to derive from).
168
+ }}>
169
+ <AuthGate />
170
+ </FlowstackProvider>
171
+ );
172
+ }
173
+
174
+ function AuthGate() {
175
+ const { isAuthenticated, isInitialized } = useFlowstack();
176
+ if (!isInitialized) return <div>Loading...</div>;
177
+ // BrokeredLoginButton opens Casino's auth popup β€” Privy runs there, not here
178
+ return isAuthenticated ? <MainApp /> : (
179
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100dvh' }}>
180
+ <BrokeredLoginButton label="Continue with Flowstack" />
181
+ </div>
182
+ );
183
+ }
184
+
185
+ function MainApp() {
186
+ const { credentials, setCredentials } = useFlowstack();
187
+ // Data β€” direct MongoDB
188
+ const { documents, insert, remove } = useCollection('contacts', {
189
+ sort: { created_at: -1 },
190
+ limit: 50,
191
+ refreshOnAgentComplete: true,
192
+ });
193
+ // AI chat β€” LLM agent (target an agent you registered via casino_create_agent)
194
+ const { query, messages, isStreaming } = useAgent(undefined, {
195
+ targetAgents: ['assistant'], // name you registered with casino_create_agent
196
+ });
197
+
198
+ return (
199
+ <div>
200
+ <header>
201
+ {credentials?.email}
202
+ <button onClick={() => setCredentials(null)}>Sign out</button>
203
+ </header>
204
+ <ContactTable contacts={documents} onDelete={(id) => remove({ _id: id })} />
205
+ <ContactForm onSubmit={(data) => insert({ ...data, created_at: new Date().toISOString() })} />
206
+ <ChatSidebar query={query} messages={messages} isStreaming={isStreaming} />
207
+ </div>
208
+ );
209
+ }
210
+ ```
211
+
212
+ ### Key Rules for Built Apps
213
+
214
+ 1. **`appScope` is required** in FlowstackProvider config. It scopes all data to this app. The build pipeline fills `__SITE_ID__` with the hex site_id.
215
+ - **Don't set `tenantId`.** For authenticated calls the backend reads it from the brokered-login JWT and ignores any client-sent value, so it's derived automatically. The *only* exception is [`usePublicCollection`](#usepubliccollection) (anonymous access has no token) β€” set `tenantId` on the provider there, or the hook errors.
216
+ 2. **No workspace selection.** Built apps do NOT use `useWorkspace` or `selectWorkspace`. The backend creates workspaces automatically.
217
+ 3. **`targetAgents` (plural array)** is required on `useAgent`. Without it, app-scoped users get 403. The name must match an agent you registered via `casino_create_agent`. Example: `targetAgents: ['support_bot']`
218
+ 4. **SHORT collection names only.** Pass `'contacts'`, NOT `'site_abc__contacts'`. The backend auto-prefixes with the site_id.
219
+ 5. **`useCollection` for ALL data** β€” tables, forms, cards, CRUD. Never route data through the agent.
220
+ 6. **`useAgent` for AI only** β€” chat sidebar, analysis, reasoning. The agent reads the same MongoDB collections but is never the data layer.
221
+
222
+ ### Auth Flow (Built Apps)
223
+
224
+ Built apps authenticate via `BrokeredLoginButton` β€” not email/password, not Google sign-in, not the wallet module. The broker opens Casino's auth popup, Casino runs Privy on its registered origin, and Privy creates an embedded wallet for the user. The popup closes and posts a scoped JWT back to the app.
225
+
226
+ ```tsx
227
+ import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';
228
+
229
+ function LoginPage() {
230
+ // No email, no password, no Privy appId needed here
231
+ return (
232
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100dvh', gap: '1rem' }}>
233
+ <h1>Welcome</h1>
234
+ <BrokeredLoginButton label="Continue with Flowstack" />
235
+ </div>
236
+ );
237
+ }
238
+
239
+ // After auth, read identity from useFlowstack() β€” not useAuth()
240
+ function Header() {
241
+ const { credentials, setCredentials } = useFlowstack();
242
+ return (
243
+ <header>
244
+ <span>{credentials?.email}</span>
245
+ <button onClick={() => setCredentials(null)}>Sign out</button>
246
+ </header>
247
+ );
248
+ }
249
+ ```
250
+
251
+ > **Why not email/password?** End-users of built apps don't have Casino email/password accounts β€” they authenticate via Privy (embedded wallet + email/Google) through the broker. `useAuth().login(email, password)` will always return an error for built-app end-users.
252
+
253
+ ### Data Operations (useCollection)
254
+
255
+ ```tsx
256
+ // READ β€” reactive query, auto-refreshes after any mutation
257
+ const { documents, isLoading, total } = useCollection<Contact>('contacts', {
258
+ filter: { status: 'active' },
259
+ sort: { created_at: -1 },
260
+ limit: 50,
261
+ refreshOnAgentComplete: true, // auto-refresh when agent writes via chat
262
+ });
263
+
264
+ // INSERT β€” all useCollection('contacts') instances auto-refresh
265
+ const { insert } = useCollection('contacts');
266
+ await insert({ name: 'Jane', email: 'jane@co.com', created_at: new Date().toISOString() });
267
+
268
+ // BATCH INSERT β€” up to 100 documents
269
+ await insert([{ name: 'A' }, { name: 'B' }, { name: 'C' }]);
270
+
271
+ // UPDATE
272
+ const { update } = useCollection('contacts');
273
+ await update({ _id: docId }, { $set: { status: 'inactive' } });
274
+
275
+ // DELETE
276
+ const { remove } = useCollection('contacts');
277
+ await remove({ _id: docId });
278
+ ```
279
+
280
+ ### AI Chat (useAgent)
281
+
282
+ ```tsx
283
+ import { useAgent } from 'flowstack-sdk';
284
+ import ReactMarkdown from 'react-markdown';
285
+ import { useState, useEffect, useRef } from 'react';
286
+
287
+ // CRITICAL: targetAgents must be a plural ARRAY
288
+ // Use the name you registered via casino_create_agent for this site
289
+ const { query, messages, isStreaming } = useAgent(undefined, {
290
+ targetAgents: ['assistant'],
291
+ });
292
+ const [savedMessages, setSavedMessages] = useState<any[]>([]);
293
+ const messagesEndRef = useRef<HTMLDivElement>(null);
294
+
295
+ // Restore chat history on mount
296
+ useEffect(() => {
297
+ try {
298
+ const stored = sessionStorage.getItem('chat_history');
299
+ if (stored) setSavedMessages(JSON.parse(stored));
300
+ } catch {}
301
+ }, []);
302
+
303
+ // Save after each completed response (cap at 50 messages)
304
+ useEffect(() => {
305
+ if (isStreaming || messages.length === 0) return;
306
+ const all = [...savedMessages, ...messages].slice(-50);
307
+ sessionStorage.setItem('chat_history', JSON.stringify(all));
308
+ }, [messages, isStreaming]);
309
+
310
+ // Auto-scroll to newest message
311
+ useEffect(() => {
312
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
313
+ }, [messages, isStreaming]);
314
+
315
+ // query() is a STREAMING function β€” it does NOT return the response text.
316
+ await query('Analyze my contacts by company size');
317
+
318
+ // Render with height-constrained container
319
+ <div className="chat-container"> {/* max-height: 80vh; flex column */}
320
+ <div className="chat-messages"> {/* flex: 1; overflow-y: auto; min-height: 0 */}
321
+ {[...savedMessages, ...messages].map(m => (
322
+ <div key={m.id} className={`message ${m.role}`}>
323
+ {m.role === 'assistant'
324
+ ? <ReactMarkdown>{m.content}</ReactMarkdown>
325
+ : <p>{m.content}</p>}
326
+ </div>
327
+ ))}
328
+ <div ref={messagesEndRef} />
329
+ </div>
330
+ <div className="chat-input"> {/* flex-shrink: 0 */}
331
+ <input ... />
332
+ <button>Send</button>
333
+ </div>
334
+ </div>
335
+ ```
336
+
337
+ > **Chat UX Requirements (built apps):**
338
+ > - Container: `max-height: 80vh`, flex column
339
+ > - Messages: `overflow-y: auto`, `min-height: 0` (required for flex child scrolling)
340
+ > - Input: `flex-shrink: 0`, pinned at bottom
341
+ > - Auto-scroll to newest message on `[messages, isStreaming]` change
342
+ > - Load/save chat history via `sessionStorage` (last 50 messages)
343
+ > - Wrap assistant messages in `<ReactMarkdown>` (package included in template)
344
+ > - Markdown links must be clickable (`<a>` tags with `target="_blank"`)
345
+
346
+ **Markdown CSS for chat messages** β€” add these styles so rendered markdown is readable and interactive:
347
+ ```css
348
+ /* Links must be clickable and visually distinct */
349
+ .message a, .message.assistant a { color: var(--primary, #2563eb); text-decoration: underline; cursor: pointer; }
350
+ .message a:hover { opacity: 0.8; }
351
+ /* Tables */
352
+ .message table { width: 100%; border-collapse: collapse; margin: 0.5em 0; font-size: 0.9em; }
353
+ .message th, .message td { padding: 0.4rem 0.6rem; border: 1px solid var(--border, #e2e8f0); text-align: left; }
354
+ .message th { background: var(--surface-alt, #f8fafc); font-weight: 600; }
355
+ /* Code */
356
+ .message pre { background: var(--surface-alt, #1e1e1e); color: #e2e8f0; padding: 0.75rem; border-radius: 6px; overflow-x: auto; margin: 0.5em 0; }
357
+ .message code { font-family: 'Fira Code', 'Fira Mono', monospace; font-size: 0.85em; }
358
+ .message p code { background: var(--surface-alt, #f1f5f9); padding: 0.15em 0.3em; border-radius: 3px; }
359
+ /* Lists */
360
+ .message ul, .message ol { margin: 0.25em 0; padding-left: 1.5em; }
361
+ .message li { margin: 0.15em 0; }
362
+ /* Headers */
363
+ .message h1, .message h2, .message h3 { margin: 0.5em 0 0.25em; }
364
+ /* Blockquotes */
365
+ .message blockquote { border-left: 3px solid var(--primary, #2563eb); padding-left: 1em; margin: 0.5em 0; opacity: 0.85; }
366
+ ```
367
+
368
+ **ReactMarkdown link config** β€” make links open in new tab:
369
+ ```tsx
370
+ <ReactMarkdown components={{
371
+ a: ({ href, children }) => <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
372
+ }}>{m.content}</ReactMarkdown>
373
+ ```
374
+
375
+ ### Agent-Driven Data Models β€” Reason β†’ Write β†’ Read
376
+
377
+ **This is the core pattern for using an agent as a worker, not just a chatbox.** It is the proven
378
+ way to get a data model that works on the first try (validated end-to-end by the Caveat app).
379
+
380
+ **The mental model:**
381
+
382
+ 1. The **app reads** whatever context the agent needs (via `useCollection`) and hands it to the
383
+ agent **inside the prompt**.
384
+ 2. The **agent reasons**, then **writes its result as a document** to a MongoDB collection using its
385
+ data tools (`insert_documents` / `update_documents`). The agent is the writer; its job is to
386
+ persist a structured doc, not to "reply."
387
+ 3. The **app reads the result back** via `useCollection` (reactively). **Never** parse the agent's
388
+ chat text or `messages[]` for data β€” read the persisted document.
389
+
390
+ > **MongoDB is schemaless β€” lean on it.** A collection imposes no fixed columns, so the agent can
391
+ > write **any shape**: nested objects, arrays of objects, ragged/optional fields, mixed types,
392
+ > per-row metadata. You do **not** need a migration, a fixed schema, or matching columns up front.
393
+ > Tell the agent the exact JSON shape you want in the prompt, and it will write exactly that. This
394
+ > is why the pattern "one-shots" a working data model: the document *is* the schema, and it can
395
+ > evolve per write.
396
+
397
+ **1. Read context in the app and pass it to the agent (don't make the agent query for it).**
398
+ Reading a shared pool in the app via `useCollection({ layer: 'shared' })` is reliable; hand the
399
+ data to the agent as JSON in the prompt. Strip PII (email/phone) before handing rows to the agent.
400
+
401
+ ```tsx
402
+ // Read the pool in the app β€” this reads the shared layer reliably.
403
+ const pool = useCollection<Member>('members', { layer: 'shared', limit: 200 });
404
+ const requester = pool.documents.find(m => m.member_key === myKey) ?? null;
405
+ const candidates = pool.documents.filter(m => m.member_key !== myKey);
406
+
407
+ // PII-safe projection before the agent ever sees a row.
408
+ const toCandidate = (m: Member) => ({
409
+ member_key: m.member_key, name: m.name, age: m.age, bio: m.bio, traits: m.traits,
410
+ // NO email, NO phone
411
+ });
412
+ ```
413
+
414
+ **2. Build a deterministic prompt that names the EXACT document shape to write.** Keep prompt
415
+ construction in a small builder function so it's testable and the shape is explicit. Tell the agent
416
+ which collection to write and the precise JSON β€” it will follow it verbatim.
417
+
418
+ ```tsx
419
+ function matchmakerPrompt(requester, candidates, nowIso: string) {
420
+ return [
421
+ `TASK β€” find honest matches for member_key "${requester.member_key}".`,
422
+ 'Use the data provided below; do NOT query the database β€” it is all here.',
423
+ '',
424
+ 'REQUESTER:', JSON.stringify(requester, null, 2),
425
+ `CANDIDATES (${candidates.length}):`, JSON.stringify(candidates, null, 2),
426
+ '',
427
+ 'Steps:',
428
+ '1. Apply the requester\'s dealbreakers as HARD filters.',
429
+ '2. WRITE exactly one document to the "matches" collection with insert_documents.',
430
+ ` Use this exact run_at (do NOT call a time tool): "${nowIso}". The document MUST be:`,
431
+ ' { "member_key": "...", "run_at": "...", "status": "fresh",',
432
+ ' "proposals": [{ "member_key", "confidence", "reasons": [], "opener" }],',
433
+ ' "passed": [{ "member_key", "reason" }] }',
434
+ '3. ALWAYS write the doc (even if proposals is empty). Never include email/phone.',
435
+ 'Then reply with one short sentence summarizing the result.',
436
+ ].join('\n');
437
+ }
438
+ ```
439
+
440
+ **3. Run the agent as a one-shot task and read the result back via `useCollection`.** This is NOT a
441
+ chat β€” it's a single `query()` whose side effect is a written document. Use `capabilities:
442
+ ['data_access']` to grant the data tools, and a `sessionKey` so this task's conversation stays
443
+ isolated from any chat surface (see [useAgent](#useagent)). With `refreshOnAgentComplete: true`, the
444
+ result collection re-fetches automatically when the agent's write tool completes β€” no manual
445
+ `refresh()` needed.
446
+
447
+ ```tsx
448
+ const agent = useAgent('custom', { capabilities: ['data_access'], sessionKey: 'matchmaker' });
449
+
450
+ // The result collection β€” reads back what the agent writes.
451
+ const matches = useCollection<MatchDoc>('matches', {
452
+ layer: 'user', // per-user results; no member_key filter needed (partition is scoped)
453
+ sort: { run_at: -1 }, limit: 5,
454
+ refreshOnAgentComplete: true, // auto-refresh when the agent's insert_documents completes
455
+ });
456
+
457
+ async function findMatches() {
458
+ await pool.refresh();
459
+ await agent.query(matchmakerPrompt(toCandidate(requester), candidates.map(toCandidate),
460
+ new Date().toISOString()));
461
+ // matches.documents[0] now holds the agent-written result (auto-refreshed). Render it directly.
462
+ }
463
+
464
+ const latest = matches.documents[0];
465
+ const proposals = latest?.proposals ?? [];
466
+ ```
467
+
468
+ **Why write-to-collection instead of reading `toolCalls[].result`?** As of SDK 0.2.1,
469
+ `toolCalls[].result` does carry the tool's structured return (and `toolCalls[].status` is `'error'`
470
+ when a call fails), so it's fine for ephemeral/inline results. But the **durable** pattern is to
471
+ have the agent persist a document and read it via `useCollection`: the result survives refreshes,
472
+ is reactive across components, and is queryable later. Reach for the persisted collection for
473
+ anything you want to keep.
474
+
475
+ ### Two-layer data model β€” `shared` vs per-user
476
+
477
+ `useCollection` (and the agent's data tools) resolve a collection to one of two namespaces via the
478
+ `layer` option:
479
+
480
+ - **`layer: 'user'`** (per-user) β€” each end-user gets their own **physically isolated** MongoDB
481
+ database (`u_{sha256(user_id)[:16]}`), not just a filtered view. The backend always keys this DB off
482
+ the *requesting* user, so one user literally cannot read or write another user's partition through
483
+ any path. Reads are already scoped β€” do **not** add a `user_id`/key filter. Use for anything truly
484
+ private: results, drafts, private history (e.g. `matches`).
485
+ - **`layer: 'shared'`** β€” one builder database (`b_{...}`, one per app) that **every signed-in user of
486
+ the app reads and (when permitted) writes**. Writes are restricted: the app cannot `insert()`
487
+ directly; a collection marked **`agent_writable`** in the data plan lets the app's own agent append
488
+ (e.g. a registrar writing member profiles), while blocking arbitrary end-user writes.
489
+ - **`layer: 'auto'`** (default) β€” resolves from the collection's configured data model.
490
+
491
+ > **⚠️ Security: `shared` is not access-controlled per user.** Rows written to a shared collection get a
492
+ > `_flowstack.user_id` field, but that is a **filterable field, not an enforced read ACL** β€” any
493
+ > signed-in user can read *every* row in the collection. Treat a shared collection as a public bulletin
494
+ > board at the storage layer. **Never put data one user must not see from another** (private messages,
495
+ > personal info, secrets) in a `shared` collection, even with a `user_id` filter β€” filtering is a UI
496
+ > convenience, not isolation. Only the per-user (`'user'`) layer is private.
497
+
498
+ > **Platform gap: no cross-user delivery (today).** Because the per-user DB is always keyed to the
499
+ > requesting user, **you cannot write into *another* user's private partition.** So patterns that
500
+ > require delivering data *to* a specific recipient privately β€” direct messages, per-user
501
+ > notifications, "send X to user B" β€” are **not achievable** with the current primitives: the only
502
+ > private store (`'user'`) is unreachable cross-user, and the only cross-user store (`'shared'`) is
503
+ > world-readable. The correct design (deliver each item into the recipient's per-user partition) needs
504
+ > a cross-user write primitive the platform does not yet expose.
505
+
506
+ Reads of a shared collection via `useCollection({ layer: 'shared' })` always hit the shared pool.
507
+ Agent-side reads of shared collections are also routed correctly as of P0-132, but the **recommended
508
+ pattern remains "read in the app, pass into the prompt"** (step 1 above) β€” it's the most reliable and
509
+ keeps PII out of the agent.
510
+
511
+ ### File Upload β†’ MongoDB
512
+
513
+ ```tsx
514
+ const { insert } = useCollection('transactions');
515
+
516
+ const handleCSVUpload = async (file: File) => {
517
+ const text = await file.text();
518
+ const rows = Papa.parse(text, { header: true }).data;
519
+ await insert(rows.map(row => ({
520
+ ...row,
521
+ amount: Number(row.amount),
522
+ imported_at: new Date().toISOString(),
523
+ })));
524
+ // All useCollection('transactions') instances auto-refresh
525
+ };
526
+ ```
527
+
528
+ ### Service Connections (useConnections)
529
+
530
+ Every built app should include a Settings page where users connect external services.
531
+ Without connecting, the AI agent cannot access Google Analytics, Drive, Ads, YouTube, Reddit, Strava, or Twitter data.
532
+
533
+ ```tsx
534
+ import { useConnections } from 'flowstack-sdk';
535
+
536
+ const { connections, connect, disconnect, isLoading } = useConnections();
537
+
538
+ // Check status
539
+ connections.google.connected // true | false
540
+ connections.google.analytics // true | false (specific service)
541
+ connections.google.email // "user@gmail.com" if connected
542
+ connections.reddit.connected // true | false
543
+ connections.strava.connected // true | false
544
+ connections.twitter.connected // true | false
545
+
546
+ // Connect β€” opens OAuth popup
547
+ connect('google', ['analytics', 'drive']); // specific Google services
548
+ connect('google', ['all']); // all Google services
549
+ connect('reddit'); // Reddit
550
+ connect('strava'); // Strava
551
+ connect('twitter'); // Twitter/X
552
+
553
+ // Disconnect
554
+ disconnect('google');
555
+ disconnect('reddit');
556
+ ```
557
+
558
+ **Settings page pattern:**
559
+ ```tsx
560
+ function SettingsPage() {
561
+ const { connections, connect, disconnect } = useConnections();
562
+
563
+ const services = [
564
+ { key: 'google', label: 'Google', sublabel: 'Analytics, Ads, Drive, YouTube', services: ['all'] as const },
565
+ { key: 'reddit', label: 'Reddit', sublabel: 'Feed access' },
566
+ { key: 'strava', label: 'Strava', sublabel: 'Activity data' },
567
+ { key: 'twitter', label: 'Twitter / X', sublabel: 'Timeline and bookmarks' },
568
+ ];
569
+
570
+ return (
571
+ <div>
572
+ <h2>Connected Services</h2>
573
+ <p>Connect your accounts so the AI assistant can access your data.</p>
574
+ {services.map(({ key, label, sublabel, services: svc }) => {
575
+ const status = connections[key as keyof typeof connections];
576
+ return (
577
+ <div key={key} className="connection-card">
578
+ <div>
579
+ <h3>{label}</h3>
580
+ <p className="muted">{sublabel}</p>
581
+ </div>
582
+ {status.connected
583
+ ? <button onClick={() => disconnect(key as any)}>Disconnect</button>
584
+ : <button onClick={() => connect(key as any, svc as any)}>Connect</button>}
585
+ </div>
586
+ );
587
+ })}
588
+ </div>
589
+ );
590
+ }
591
+ ```
592
+
593
+ Include this Settings page in every built app's navigation. The AI chat agent can only access
594
+ services the user has connected.
595
+
596
+ ---
597
+
598
+ ## Quick Start (Casino Platform)
599
+
600
+ > **Note:** This section is for the Casino data science platform, not generated apps. If you're building a generated site, see [Built Apps](#built-apps--generated-site-pattern) above.
601
+
602
+ ### 1. Wrap your app with the provider
603
+
604
+ ```tsx
605
+ import { FlowstackProvider } from 'flowstack-sdk';
606
+
607
+ const config = {
608
+ baseUrl: 'https://sage-api.flowstack.fun',
609
+ tenantId: 't_6fe54402be43',
610
+ mode: 'production',
611
+ jwtSecret: 'flowstack-cdn-site',
612
+ passwordSecret: 'flowstack-cdn-site',
613
+ };
614
+
615
+ export default function App({ children }) {
616
+ return (
617
+ <FlowstackProvider config={config}>
618
+ {children}
619
+ </FlowstackProvider>
620
+ );
621
+ }
622
+ ```
623
+
624
+ **Important:** Generated sites always run in production mode. Auth, agent chat, and data access all connect to real infrastructure at `sage-api.flowstack.fun`. Do not use mock mode for deployed sites.
625
+
626
+ ### 2. Add authentication
627
+
628
+ **Built apps must use `BrokeredLoginButton` β€” not `useAuth()`, not `LoginButton` from `flowstack-sdk/wallet`.**
629
+
630
+ `BrokeredLoginButton` opens a Casino popup that handles Privy login on Casino's origin (the only origin registered with Privy). The popup postMessages a scoped JWT back to your app. No Privy app ID, no wagmi/viem peer deps, no `privyConfig` needed.
631
+
632
+ ```tsx
633
+ import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';
634
+
635
+ export default function App() {
636
+ const { isAuthenticated, isInitialized, credentials, setCredentials } = useFlowstack();
637
+
638
+ if (!isInitialized) return <div>Loading…</div>;
639
+
640
+ if (!isAuthenticated) {
641
+ return (
642
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100dvh' }}>
643
+ <BrokeredLoginButton label="Continue with Flowstack" />
644
+ </div>
645
+ );
646
+ }
647
+
648
+ return (
649
+ <div>
650
+ <p>Welcome, {credentials?.email}</p>
651
+ <button onClick={() => setCredentials(null)}>Sign out</button>
652
+ {/* your app */}
653
+ </div>
654
+ );
655
+ }
656
+ ```
657
+
658
+ Or use `AuthGuard` which shows `BrokeredLoginButton` automatically when not authenticated:
659
+
660
+ ```tsx
661
+ import { AuthGuard } from 'flowstack-sdk';
662
+
663
+ export default function App() {
664
+ return (
665
+ <AuthGuard fallback={<LoginScreen />}>
666
+ <Console />
667
+ </AuthGuard>
668
+ );
669
+ }
670
+
671
+ // LoginScreen shows BrokeredLoginButton
672
+ function LoginScreen() {
673
+ return (
674
+ <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}>
675
+ <BrokeredLoginButton />
676
+ </div>
677
+ );
678
+ }
679
+ ```
680
+
681
+ **Guest chat (built apps).** When the app is built with an `appScope` and the site has enabled guest chat
682
+ server-side (`app_config.allowGuestChat`), `AuthGuard` transparently mints a short-lived guest session
683
+ (`POST /auth/guest`) for unauthenticated visitors instead of showing the login gate β€” removing the
684
+ "sign up first" friction. The opt-in is the per-site backend flag: if the site hasn't enabled it,
685
+ `/auth/guest` returns 403 and `AuthGuard` falls back to the normal login UI. This is a no-op without an
686
+ `appScope` (the Casino dashboard has none, so it always requires real login). It's on by default; pass
687
+ `allowGuest={false}` on a specific guard to always require login.
688
+
689
+ ```tsx
690
+ <AuthGuard fallback={<LoginScreen />} allowGuest> {/* default β€” guest if the site opted in */}
691
+ <Console />
692
+ </AuthGuard>
693
+
694
+ <AuthGuard fallback={<LoginScreen />} allowGuest={false}> {/* always require real login */}
695
+ <Billing />
696
+ </AuthGuard>
697
+ ```
698
+
699
+ **Account switching.** After `logout()`, the next `BrokeredLoginButton` click passes `force_login=1`, so
700
+ the broker purges its sticky Privy/Casino session and the user can sign in with a *different* account.
701
+ Without this, brokered re-login silently returns the same identity.
702
+
703
+ **Do not use:**
704
+ - `useAuth()` with `login(email, password)` β€” Casino end-users don't have email/password accounts
705
+ - `LoginButton` from `flowstack-sdk/wallet` β€” requires WalletProvider/Privy which can't run on built-app subdomains
706
+ - `privyConfig.appId` in FlowstackProvider β€” not needed for the broker auth flow
707
+ - Extra peer deps (`@privy-io/react-auth`, `wagmi`, `viem`) β€” only needed for Casino platform features, not built apps
708
+
709
+ ### 3. Chat with AI agent
710
+
711
+ ```tsx
712
+ import { useAgent } from 'flowstack-sdk';
713
+
714
+ function Chat() {
715
+ const { messages, isStreaming, query } = useAgent();
716
+
717
+ return (
718
+ <div>
719
+ {messages.map(msg => (
720
+ <div key={msg.id} className={msg.role}>
721
+ {msg.content}
722
+ {msg.statusLine && <span className="status">{msg.statusLine}</span>}
723
+ </div>
724
+ ))}
725
+ <button onClick={() => query('Analyze my sales data')} disabled={isStreaming}>
726
+ {isStreaming ? 'Thinking...' : 'Ask'}
727
+ </button>
728
+ </div>
729
+ );
730
+ }
731
+ ```
732
+
733
+ ---
734
+
735
+ ## Complete Example App (Casino Platform)
736
+
737
+ > **Note:** This example uses workspace selection which is for the Casino platform only. Generated/built apps should follow the [Built Apps](#built-apps--generated-site-pattern) pattern instead β€” no workspaces, use `appScope`.
738
+
739
+ A complete, working AI chat application in a single file:
740
+
741
+ ```tsx
742
+ 'use client';
743
+
744
+ import { useState } from 'react';
745
+ import {
746
+ FlowstackProvider,
747
+ LoginForm,
748
+ useAuth,
749
+ useWorkspace,
750
+ useAgent,
751
+ } from 'flowstack-sdk';
752
+
753
+ const config = {
754
+ baseUrl: 'https://sage-api.flowstack.fun',
755
+ tenantId: 't_6fe54402be43',
756
+ mode: 'production' as const,
757
+ jwtSecret: 'flowstack-cdn-site',
758
+ passwordSecret: 'flowstack-cdn-site',
759
+ };
760
+
761
+ function ChatApp() {
762
+ const { user, isAuthenticated, login, logout, isLoading: authLoading } = useAuth();
763
+ const { workspaces, selectedWorkspace, selectWorkspace } = useWorkspace();
764
+ const { messages, isStreaming, query, clearMessages } = useAgent();
765
+ const [input, setInput] = useState('');
766
+ const messagesEndRef = useRef<HTMLDivElement>(null);
767
+
768
+ // Auto-scroll to newest message during streaming
769
+ useEffect(() => {
770
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
771
+ }, [messages, isStreaming]);
772
+
773
+ const handleSend = async () => {
774
+ if (!input.trim() || isStreaming) return;
775
+ const message = input;
776
+ setInput('');
777
+ await query(message);
778
+ };
779
+
780
+ if (!isAuthenticated) {
781
+ return (
782
+ <div style={{ padding: 40, maxWidth: 400, margin: '0 auto' }}>
783
+ <h1>AI Chat</h1>
784
+ <LoginForm onSuccess={() => {}} showRegisterLink />
785
+ </div>
786
+ );
787
+ }
788
+
789
+ if (!selectedWorkspace && workspaces.length > 0) {
790
+ return (
791
+ <div style={{ padding: 40 }}>
792
+ <h1>Select Workspace</h1>
793
+ {workspaces.map(ws => (
794
+ <button key={ws.workspaceId} onClick={() => selectWorkspace(ws)} style={{ display: 'block', margin: '8px 0' }}>
795
+ {ws.name}
796
+ </button>
797
+ ))}
798
+ </div>
799
+ );
800
+ }
801
+
802
+ return (
803
+ <div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
804
+ <header style={{ padding: 16, borderBottom: '1px solid #eee', display: 'flex', justifyContent: 'space-between' }}>
805
+ <strong>{selectedWorkspace?.name || 'AI Chat'}</strong>
806
+ <div>
807
+ <button onClick={clearMessages} style={{ marginRight: 8 }}>Clear</button>
808
+ <button onClick={logout}>Logout</button>
809
+ </div>
810
+ </header>
811
+
812
+ <div style={{ flex: 1, overflow: 'auto', padding: 16, minHeight: 0 }}>
813
+ {messages.map(msg => (
814
+ <div key={msg.id} style={{
815
+ marginBottom: 16, padding: 12, borderRadius: 8,
816
+ backgroundColor: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5',
817
+ maxWidth: '80%', marginLeft: msg.role === 'user' ? 'auto' : 0,
818
+ }}>
819
+ <div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
820
+ {msg.role === 'user' ? 'You' : 'Assistant'}
821
+ </div>
822
+ {msg.role === 'assistant'
823
+ ? <ReactMarkdown>{msg.content}</ReactMarkdown>
824
+ : <div style={{ whiteSpace: 'pre-wrap' }}>{msg.content}</div>}
825
+ {msg.statusLine && <div style={{ fontSize: 11, color: '#999', marginTop: 4 }}>{msg.statusLine}</div>}
826
+ {msg.isStreaming && <span style={{ color: '#999' }}>β–‹</span>}
827
+ </div>
828
+ ))}
829
+ <div ref={messagesEndRef} />
830
+ </div>
831
+
832
+ <div style={{ padding: 16, borderTop: '1px solid #eee', display: 'flex', gap: 8 }}>
833
+ <input
834
+ value={input}
835
+ onChange={e => setInput(e.target.value)}
836
+ onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()}
837
+ placeholder="Ask a question..."
838
+ disabled={isStreaming}
839
+ style={{ flex: 1, padding: 12, fontSize: 16, borderRadius: 8, border: '1px solid #ddd' }}
840
+ />
841
+ <button onClick={handleSend} disabled={isStreaming || !input.trim()}>
842
+ {isStreaming ? '...' : 'Send'}
843
+ </button>
844
+ </div>
845
+ </div>
846
+ );
847
+ }
848
+
849
+ export default function App() {
850
+ return (
851
+ <FlowstackProvider config={config}>
852
+ <ChatApp />
853
+ </FlowstackProvider>
854
+ );
855
+ }
856
+ ```
857
+
858
+ ---
859
+
860
+ ## Hooks Reference
861
+
862
+ ### useAuth
863
+
864
+ > **Built apps: do not use this hook.** `useAuth()` is for Casino platform internals. Built apps authenticate via `BrokeredLoginButton` β€” see [Add authentication](#2-add-authentication) above. Calling `login(email, password)` from a built app will always fail because end-users don't have Casino credentials.
865
+
866
+ For built apps, read auth state from `useFlowstack()` instead:
867
+
868
+ ```tsx
869
+ const { isAuthenticated, isInitialized, credentials } = useFlowstack();
870
+ ```
871
+
872
+ `useAuth` (Casino platform only):
873
+
874
+ ```tsx
875
+ const {
876
+ user, // User | null
877
+ credentials, // FlowstackCredentials | null
878
+ isAuthenticated,// boolean
879
+ isLoading, // boolean
880
+ error, // string | null
881
+ login, // (email, password) => Promise<boolean> β€” Casino platform only
882
+ register, // (email, password, name?) => Promise<boolean> β€” Casino platform only
883
+ googleSignIn, // () => Promise<void> β€” Casino platform only
884
+ logout, // () => void
885
+ refreshToken, // () => Promise<boolean>
886
+ } = useAuth();
887
+ ```
888
+
889
+ ### useCollection
890
+
891
+ Direct MongoDB CRUD for built-app data. This is the primary data hook β€” use it for all tables, forms, cards, and data persistence. The agent is NOT involved in data operations.
892
+
893
+ Collection names are auto-prefixed with the site_id by the backend β€” pass SHORT names only (e.g., `'transactions'`, NOT `'site_abc__transactions'`).
894
+
895
+ ```tsx
896
+ const {
897
+ // Read (reactive β€” auto-fetches on mount, re-fetches after mutations)
898
+ documents, // T[]
899
+ count, // number β€” documents returned (≀ limit)
900
+ total, // number β€” total matching (for pagination)
901
+ isLoading, // boolean
902
+ error, // string | null
903
+ refresh, // () => Promise<void> β€” manual re-fetch
904
+
905
+ // Write (auto-refetches all useCollection instances for this collection after success)
906
+ insert, // (doc | doc[]) => Promise<{ inserted_ids: string[] }>
907
+ update, // (filter, update, opts?) => Promise<{ modified_count: number }>
908
+ remove, // (filter) => Promise<{ deleted_count: number }>
909
+ } = useCollection<Transaction>('transactions', {
910
+ filter: { status: 'active' }, // MongoDB query filter
911
+ sort: { date: -1 }, // Sort spec
912
+ limit: 50, // Max documents (default 50, max 500)
913
+ skip: 0, // Pagination offset
914
+ layer: 'auto', // 'user' (per-user) | 'shared' (one pool) | 'auto' (default)
915
+ refreshOnAgentComplete: true, // Auto-refresh after an agent write tool completes (0.2.1+: fires
916
+ // for insert_documents/update_documents/insert_app_data/etc.)
917
+ });
918
+ ```
919
+
920
+ > **`layer`** picks the namespace: `'user'` is a private per-user partition (don't filter by a user
921
+ > key β€” it's already scoped); `'shared'` is one pool all users read (app can't `insert()` directly
922
+ > unless the collection is `agent_writable`); `'auto'` resolves from the collection's data model. See
923
+ > [Two-layer data model](#two-layer-data-model--shared-vs-per-user).
924
+ >
925
+ > **`refreshOnAgentComplete: true`** is the reactive bridge for the Reason→Write→Read pattern — when
926
+ > the agent finishes writing via its data tools, this collection re-fetches automatically. (Before
927
+ > 0.2.1 it only matched legacy tool names and silently no-op'd, so apps called `refresh()` manually;
928
+ > that workaround is no longer needed.)
929
+
930
+ **Data display β€” reactive query:**
931
+ ```tsx
932
+ function TransactionTable() {
933
+ const { documents, isLoading } = useCollection<Transaction>('transactions', {
934
+ sort: { date: -1 }, limit: 50, refreshOnAgentComplete: true,
935
+ });
936
+ if (isLoading) return <div>Loading...</div>;
937
+ return (
938
+ <table>
939
+ <tbody>
940
+ {documents.map(row => (
941
+ <tr key={row._id}><td>{row.date}</td><td>${row.amount}</td></tr>
942
+ ))}
943
+ </tbody>
944
+ </table>
945
+ );
946
+ }
947
+ ```
948
+
949
+ **Form submission β€” insert document:**
950
+ ```tsx
951
+ function AddTransaction() {
952
+ const { insert } = useCollection('transactions');
953
+ const [form, setForm] = useState({ date: '', amount: '' });
954
+
955
+ const handleSubmit = async (e: React.FormEvent) => {
956
+ e.preventDefault();
957
+ await insert({ ...form, amount: Number(form.amount), created_at: new Date().toISOString() });
958
+ setForm({ date: '', amount: '' });
959
+ // All useCollection('transactions') instances auto-refresh
960
+ };
961
+
962
+ return <form onSubmit={handleSubmit}>...</form>;
963
+ }
964
+ ```
965
+
966
+ **Update and delete:**
967
+ ```tsx
968
+ const { update, remove } = useCollection('transactions');
969
+
970
+ // Update
971
+ await update({ _id: docId }, { $set: { category: 'Food' } });
972
+
973
+ // Delete
974
+ await remove({ _id: docId });
975
+ ```
976
+
977
+ **CRITICAL:** The app **reads** all data with `useCollection` (tables, forms, cards, CRUD) β€” never
978
+ parse the agent's chat text or `messages[]` for data, and never use `useToolInvocation`. **Writes**
979
+ happen one of two ways: (a) deterministic user actions β†’ `useCollection().insert/update/remove`
980
+ directly; (b) agent-reasoned results β†’ the agent writes a document via its `data_access` tools and
981
+ the app reads it back with `useCollection` (the [Reason β†’ Write β†’ Read](#agent-driven-data-models--reason--write--read)
982
+ pattern). Either way, `useCollection` is always how the app *reads*.
983
+
984
+ ### usePublicCollection
985
+
986
+ Anonymous public submissions β€” leaderboards, guestbooks, comment threads, voting. **No auth required.** Any visitor can read and insert. The collection must be declared in `app_config.publicCollections`.
987
+
988
+ ```tsx
989
+ import { usePublicCollection } from 'flowstack-sdk';
990
+
991
+ const {
992
+ documents, // T[]
993
+ count, // number
994
+ total, // number
995
+ isLoading, // boolean
996
+ error, // string | null
997
+ insert, // (doc: Partial<T>) => Promise<{ inserted_id: string }>
998
+ refresh, // () => Promise<void>
999
+ } = usePublicCollection<HighScore>('high_scores', {
1000
+ filter: { album: 'yeezus' },
1001
+ sort: { score: -1 },
1002
+ limit: 25,
1003
+ });
1004
+
1005
+ await insert({ album: 'yeezus', name: 'KEON', score: 12500 });
1006
+ ```
1007
+
1008
+ **Key differences from `useCollection`:**
1009
+ - No credentials needed β€” works for anonymous visitors
1010
+ - Insert only (no update/remove β€” data is owned by the app, not the user)
1011
+ - Rate-limited server-side (default 10 writes/min, 100/day per IP)
1012
+ - The collection must be in `app_config.publicCollections` (ask the builder to configure it; the build pipeline sets this up based on `usePublicCollection` usage in the source)
1013
+ - **Requires `tenantId` on `FlowstackProvider`.** Because there's no token for anonymous visitors, the backend can't derive the tenant β€” so this is the one hook that needs `tenantId` set explicitly. If it's missing, `usePublicCollection` raises a clear error instead of silently using a default. (Authenticated hooks don't need it β€” they get the tenant from the JWT.)
1014
+
1015
+ **Builder config required in app_config.json:**
1016
+ ```json
1017
+ {
1018
+ "publicCollections": {
1019
+ "high_scores": {
1020
+ "schema": {
1021
+ "album": { "type": "string", "required": true, "maxLength": 32 },
1022
+ "name": { "type": "string", "required": true, "maxLength": 24 },
1023
+ "score": { "type": "number", "required": true, "min": 0 }
1024
+ },
1025
+ "rateLimit": { "writesPerMinute": 5, "writesPerDay": 50 }
1026
+ }
1027
+ }
1028
+ }
1029
+ ```
1030
+
1031
+ ### useWorkspace
1032
+
1033
+ Workspace management.
1034
+
1035
+ ```tsx
1036
+ const {
1037
+ workspaces, // WorkspaceInfo[]
1038
+ selectedWorkspace, // WorkspaceInfo | null
1039
+ isLoading, // boolean
1040
+ error, // string | null
1041
+ createWorkspace, // (name, description?) => Promise<WorkspaceInfo | null>
1042
+ selectWorkspace, // (workspace) => void
1043
+ refreshWorkspaces, // () => Promise<void>
1044
+ } = useWorkspace();
1045
+ ```
1046
+
1047
+ ### useAgent
1048
+
1049
+ AI agent queries with streaming, tool calls, interrupts, status updates, and **automatic conversation persistence**.
1050
+
1051
+ ```tsx
1052
+ const {
1053
+ messages, // ChatMessage[]
1054
+ isStreaming, // boolean
1055
+ isLoading, // boolean
1056
+ toolCalls, // ToolCall[] β€” active/completed tool executions
1057
+ connectedDataSources, // DataSourceBadgeInfo[] β€” connected data sources
1058
+ error, // string | null
1059
+ query, // (prompt: string) => Promise<void>
1060
+ clearMessages, // () => void β€” wipe in-memory messages (same session)
1061
+ cancelQuery, // () => void
1062
+ interruptAgent, // () => Promise<void> β€” pause agent mid-execution
1063
+ respondToInterrupt, // (response: string) => Promise<void> β€” resume with user input
1064
+ } = useAgent(template?, options?);
1065
+ ```
1066
+
1067
+ **Template (first arg):** `'data-science'` | `'marketing'` | `'support'` | `'custom'`. For a
1068
+ **persona-backed app** (your app's brain is an agent registered via `casino_create_agent`), the
1069
+ template is just a fallback label β€” the registered persona overrides it. Use `'custom'` (or omit) and
1070
+ select the brain with the `persona` option below. The template does **not** pick the agent.
1071
+
1072
+ **Options:**
1073
+
1074
+ | Option | Type | What it does |
1075
+ |---|---|---|
1076
+ | `capabilities` | `string[]` | Pre-load tool categories the agent may use: `'data_access'` (MongoDB read/write tools — required for the Reason→Write→Read pattern), `'external_integration'` (Google/SMS/web/etc.), `'site_operations'`, `'code_execution'`, `'domain_task'`, `'workspace_management'`. Grant only what the task needs. |
1077
+ | `persona` *(0.2.1+)* | `string` | Target a specific registered persona/subagent by name (maps to `target_agents`). Without it the backend auto-selects the **first** registered persona β€” so multi-persona apps **must** set this to reach the others. (`agentName` is an alias.) |
1078
+ | `systemPrompt` *(0.2.1+)* | `string` | Inline system-prompt override for this hook (maps to `system_prompt_override`). Use to tune behavior per-surface without registering a separate persona. |
1079
+ | `sessionKey` *(0.2.1+)* | `string` | Namespaces this hook's conversation. **Pass a stable, distinct value per surface** (e.g. `'interview'`, `'matchmaker'`) so independent `useAgent` instances don't share one conversation. It also persists that surface's history across reloads (same tab). Omit it only for a single-surface app. |
1080
+ | `tools` | `string[]` | Whitelist specific tool names (finer than `capabilities`). |
1081
+ | `targetAgent(s)` | `string` / `string[]` | **Deprecated** (Strands swarm, removed in P0-73). Use `persona` instead. |
1082
+
1083
+ ```tsx
1084
+ // Persona-backed, multi-surface app: each surface gets its own brain + isolated conversation.
1085
+ const interview = useAgent('custom', { capabilities: ['data_access'], persona: 'interviewer', sessionKey: 'interview' });
1086
+ const matchmaker = useAgent('custom', { capabilities: ['data_access'], persona: 'matchmaker', sessionKey: 'matchmaker' });
1087
+ ```
1088
+
1089
+ #### Conversation Memory (Automatic)
1090
+
1091
+ `useAgent` automatically persists a stable **session ID** for each component mount. The session ID is:
1092
+
1093
+ - **Generated on first use** via `crypto.randomUUID()` (with a timestamp fallback for legacy browsers)
1094
+ - **Stable across turns** β€” every call to `query()` passes the same session ID to the backend
1095
+ - **Reused by Strands' `S3SessionManager`** β€” the agent remembers prior messages, tool calls, and context across turns
1096
+ - **Reset on `clearMessages()`** β€” starting a truly new conversation generates a fresh session ID
1097
+
1098
+ This means a chat in a built app correctly handles multi-turn queries:
1099
+
1100
+ ```tsx
1101
+ function FinanceChat() {
1102
+ const { query, messages, isStreaming } = useAgent(undefined, {
1103
+ targetAgents: ['finance_bot'] // agent registered via casino_create_agent
1104
+ });
1105
+
1106
+ // Turn 1: "show me last month's expenses"
1107
+ // Turn 2: "now break it down by category" ← agent remembers "last month's expenses"
1108
+ // Turn 3: "which one is the biggest?" ← agent knows the categories from turn 2
1109
+
1110
+ return (
1111
+ <ChatUI messages={messages} onSend={query} isStreaming={isStreaming} />
1112
+ );
1113
+ }
1114
+ ```
1115
+
1116
+ **Notes:**
1117
+ - **Session ID is tied to the component mount.** Unmounting and remounting `useAgent` (e.g., closing and reopening a modal) starts a new conversation. Persisting sessions across unmounts/page refreshes requires application-level storage of the session ID (see Advanced).
1118
+ - **`clearMessages()` resets the session.** Call it to start a fresh conversation. The next `query()` creates a new backend session.
1119
+ - **Wallet-authenticated users** have their conversations scoped to their wallet address server-side. Same wallet across tabs/browsers can reach the same conversation if the session ID is passed explicitly.
1120
+
1121
+ #### Advanced: Explicit Session Management
1122
+
1123
+ **Simplest path (0.2.1+): pass `sessionKey`.** A stable `sessionKey` per surface gives each
1124
+ `useAgent` its own conversation *and* persists that conversation across reloads (same tab) β€” no
1125
+ localStorage plumbing or remount needed:
1126
+
1127
+ ```tsx
1128
+ const chat = useAgent('custom', { capabilities: ['data_access'], sessionKey: 'support-chat' });
1129
+ // Reopening/reloading this surface resumes the same conversation; a different sessionKey is a
1130
+ // different conversation. Call chat.clearMessages() / startNewSession() to start fresh.
1131
+ ```
1132
+
1133
+ For cross-tab or multi-conversation-per-surface persistence, manage the session ID at the
1134
+ application level:
1135
+
1136
+ ```tsx
1137
+ import { useEffect, useState } from 'react';
1138
+ import { useAgent } from 'flowstack-sdk';
1139
+
1140
+ function PersistentChat() {
1141
+ // Option 1: Resume the most recent session from localStorage
1142
+ const [sessionId] = useState(() => {
1143
+ const existing = localStorage.getItem('my-app:chat:session');
1144
+ if (existing) return existing;
1145
+ const fresh = crypto.randomUUID();
1146
+ localStorage.setItem('my-app:chat:session', fresh);
1147
+ return fresh;
1148
+ });
1149
+
1150
+ // The useAgent hook will create its own sessionIdRef automatically,
1151
+ // but you can also invoke the lower-level `executeQueryWithConfig` with
1152
+ // an explicit sessionId for full control (see API reference).
1153
+ const { query, messages, clearMessages } = useAgent(undefined, {
1154
+ targetAgents: ['finance_bot'] // agent registered via casino_create_agent
1155
+ });
1156
+
1157
+ const handleNewChat = () => {
1158
+ const fresh = crypto.randomUUID();
1159
+ localStorage.setItem('my-app:chat:session', fresh);
1160
+ clearMessages(); // resets the in-hook sessionIdRef as well
1161
+ window.location.reload(); // optional: force remount to pick up new session
1162
+ };
1163
+
1164
+ return (
1165
+ <>
1166
+ <button onClick={handleNewChat}>+ New Chat</button>
1167
+ <ChatUI messages={messages} onSend={query} />
1168
+ </>
1169
+ );
1170
+ }
1171
+ ```
1172
+
1173
+ **Future SDK versions** will expose `useConversations()` and `useConversation(id)` hooks for first-class conversation listing and restoration. Until then, the pattern above is the recommended approach for multi-conversation apps.
1174
+
1175
+ #### When Conversation Memory Breaks
1176
+
1177
+ If your built app appears to "forget" every turn:
1178
+
1179
+ 1. **Check your SDK version.** Apps built with SDK versions before the `sessionIdRef` fix do not pass a session ID at all β€” every turn lands in a new backend session. Rebuild the app via `edit_site` or `build_site` to bundle the latest SDK.
1180
+ 2. **Check that you're not remounting the component** between every query (e.g., conditional rendering based on `isStreaming` can cause remounts).
1181
+ 3. **Check for multiple `useAgent` instances** in the same page. Before 0.2.1 they all *shared* one
1182
+ tenant-wide session, so surfaces bled into each other (a matchmaker reply showing up in an
1183
+ interview). Fix: give each instance a distinct `sessionKey` (0.2.1+) so conversations stay
1184
+ isolated.
1185
+ 4. **Check the browser console** for SDK warnings about `crypto.randomUUID` not being available (very old browsers).
1186
+
1187
+ **ChatMessage fields:**
1188
+
1189
+ | Field | Type | Description |
1190
+ |-------|------|-------------|
1191
+ | `id` | `string` | Unique message ID |
1192
+ | `role` | `'user' \| 'assistant'` | Message sender |
1193
+ | `content` | `string` | Message text |
1194
+ | `isStreaming` | `boolean` | Whether still receiving content |
1195
+ | `statusLine` | `string \| undefined` | Live status text during streaming (tool progress, agent status) |
1196
+ | `toolCalls` | `ToolCall[]` | Tool executions in this message |
1197
+ | `visualizations` | `VisualizationData[]` | Charts/images produced |
1198
+ | `searchResults` | `SearchResultsData[]` | Search results |
1199
+
1200
+ ### useDatasets
1201
+
1202
+ Dataset operations.
1203
+
1204
+ ```tsx
1205
+ const {
1206
+ datasets, // DatasetInfo[] β€” { id, name, rows, columns, schema?, columnNames?, createdAt?, updatedAt? }
1207
+ isLoading, // boolean
1208
+ error, // string | null
1209
+ uploadDataset, // (file, name?) => Promise<DatasetInfo | null>
1210
+ downloadDataset, // (name) => Promise<Blob | null>
1211
+ deleteDataset, // (name) => Promise<boolean>
1212
+ refreshDatasets, // () => Promise<void>
1213
+ } = useDatasets();
1214
+ ```
1215
+
1216
+ > **DatasetInfo fields (source of truth: `src/types/index.ts`):**
1217
+ > - `id: string` β€” stable identifier
1218
+ > - `name: string` β€” user-visible name
1219
+ > - `rows: number` β€” row count (NOT `rowCount`)
1220
+ > - `columns: number` β€” column count (NOT `columnCount`)
1221
+ > - `schema?: Record<string, ColumnSchema>` β€” optional per-column schema
1222
+ > - `columnNames?: string[]` β€” optional ordered column names
1223
+ > - `createdAt?: string`, `updatedAt?: string` β€” ISO8601 timestamps
1224
+
1225
+ ### useVisualizations
1226
+
1227
+ Visualization management.
1228
+
1229
+ ```tsx
1230
+ const {
1231
+ visualizations, // VisualizationData[] β€” { name, type, imageUrl, imageBase64, format, createdAt }
1232
+ isLoading, // boolean
1233
+ error, // string | null
1234
+ refreshVisualizations,// () => Promise<void>
1235
+ } = useVisualizations();
1236
+ ```
1237
+
1238
+ ### useReports
1239
+
1240
+ Report management.
1241
+
1242
+ ```tsx
1243
+ const {
1244
+ reports, // ReportInfo[] β€” { name, content, format, createdAt }
1245
+ isLoading, // boolean
1246
+ error, // string | null
1247
+ refreshReports, // () => Promise<void>
1248
+ } = useReports();
1249
+ ```
1250
+
1251
+ ### useModels
1252
+
1253
+ ML model management.
1254
+
1255
+ ```tsx
1256
+ const {
1257
+ models, // ModelInfo[] β€” { name, framework, metrics, createdAt }
1258
+ isLoading, // boolean
1259
+ error, // string | null
1260
+ refreshModels, // () => Promise<void>
1261
+ } = useModels();
1262
+ ```
1263
+
1264
+ ### useSites
1265
+
1266
+ Published site management β€” list, create, stage files, publish to CDN, delete.
1267
+
1268
+ ```tsx
1269
+ const {
1270
+ sites, // PublishedSiteInfo[]
1271
+ isLoading, // boolean
1272
+ error, // string | null
1273
+ createSite, // (params: CreateSiteParams) => Promise<PublishedSiteInfo | null>
1274
+ addFile, // (siteId, path, content) => Promise<boolean>
1275
+ publishSite, // (siteId) => Promise<PublishedSiteInfo | null>
1276
+ deleteSite, // (siteId) => Promise<boolean>
1277
+ refreshSites, // () => Promise<void>
1278
+ } = useSites();
1279
+ ```
1280
+
1281
+ **PublishedSiteInfo fields:**
1282
+
1283
+ | Field | Type | Description |
1284
+ |-------|------|-------------|
1285
+ | `id` | `string` | Site ID |
1286
+ | `name` | `string` | Display name |
1287
+ | `url` | `string` | Published CDN URL |
1288
+ | `shortUrl` | `string?` | Short URL for sharing |
1289
+ | `siteType` | `'on_demand' \| 'daily' \| 'js_build'` | How the site was created |
1290
+ | `fileCount` | `number` | Number of files |
1291
+ | `totalBytes` | `number?` | Total size |
1292
+ | `createdAt` | `string` | ISO timestamp |
1293
+ | `expiresAt` | `string?` | Expiration (if applicable) |
1294
+
1295
+ **Two publishing workflows:**
1296
+
1297
+ ```tsx
1298
+ // Quick publish β€” pass files inline
1299
+ await createSite({
1300
+ name: 'My Dashboard',
1301
+ siteType: 'on_demand',
1302
+ files: { 'index.html': '<html>...</html>', 'styles.css': 'body { ... }' },
1303
+ });
1304
+
1305
+ // Staged publish β€” add files incrementally, then publish
1306
+ const site = await createSite({ name: 'My App' });
1307
+ await addFile(site.id, 'index.html', '<html>...</html>');
1308
+ await addFile(site.id, 'src/App.tsx', 'export default function App() { ... }');
1309
+ await publishSite(site.id);
1310
+ ```
1311
+
1312
+ ### useDataSources
1313
+
1314
+ External data source connections.
1315
+
1316
+ ```tsx
1317
+ const {
1318
+ dataSources, // DataSource[]
1319
+ isLoading, // boolean
1320
+ error, // string | null
1321
+ createDataSource, // (config) => Promise<DataSource | null>
1322
+ testConnection, // (sourceId) => Promise<boolean>
1323
+ deleteDataSource, // (sourceId) => Promise<boolean>
1324
+ refreshDataSources,// () => Promise<void>
1325
+ } = useDataSources();
1326
+ ```
1327
+
1328
+ Supports: PostgreSQL, MongoDB, MySQL, Snowflake, BigQuery, S3.
1329
+
1330
+ ### useQuery
1331
+
1332
+ Simple one-shot query execution (no streaming).
1333
+
1334
+ ```tsx
1335
+ const {
1336
+ executeQuery, // (prompt: string) => Promise<QueryResult>
1337
+ result, // QueryResult | null
1338
+ isLoading, // boolean
1339
+ error, // string | null
1340
+ } = useQuery();
1341
+ ```
1342
+
1343
+ ### useUserManagement
1344
+
1345
+ Admin user management.
1346
+
1347
+ ```tsx
1348
+ const {
1349
+ users, // ManagedUser[]
1350
+ stats, // UserStats | null β€” { totalUsers, activeUsers, newUsersThisMonth }
1351
+ isLoading, // boolean
1352
+ error, // string | null
1353
+ canManageUsers, // boolean
1354
+ pagination, // { page, limit, totalCount, hasMore }
1355
+ refreshUsers, // (params?) => Promise<void>
1356
+ getUser, // (userId) => Promise<ManagedUser | null>
1357
+ updateUser, // (userId, updates) => Promise<boolean>
1358
+ suspendUser, // (userId, reason?) => Promise<boolean>
1359
+ reactivateUser, // (userId) => Promise<boolean>
1360
+ deleteUser, // (userId) => Promise<boolean>
1361
+ getUserActivity, // (userId, limit?) => Promise<UserActivityLog[]>
1362
+ refreshStats, // () => Promise<void>
1363
+ setPage, // (page) => void
1364
+ setSearch, // (search) => void
1365
+ setRoleFilter, // (role) => void
1366
+ setStatusFilter, // (status) => void
1367
+ } = useUserManagement();
1368
+ ```
1369
+
1370
+ **Role hierarchy:** `owner` > `admin` > `member` > `viewer`
1371
+
1372
+ ### useAuthGuard
1373
+
1374
+ Programmatic route protection.
1375
+
1376
+ ```tsx
1377
+ const {
1378
+ isAllowed, // boolean
1379
+ isLoading, // boolean
1380
+ shouldRedirect, // boolean
1381
+ redirectTo, // string | undefined
1382
+ } = useAuthGuard({
1383
+ requireAuth: true,
1384
+ requireWorkspace: true,
1385
+ redirectTo: '/login',
1386
+ });
1387
+ ```
1388
+
1389
+ ### useFlowstackStatus
1390
+
1391
+ Backend connection monitoring.
1392
+
1393
+ ```tsx
1394
+ const {
1395
+ status, // 'connected' | 'disconnected' | 'checking'
1396
+ isConnected, // boolean
1397
+ latency, // number | null (ms)
1398
+ error, // string | null
1399
+ checkConnection, // () => Promise<void>
1400
+ } = useFlowstackStatus({
1401
+ pollInterval: 30000,
1402
+ autoPoll: true,
1403
+ });
1404
+ ```
1405
+
1406
+ ### Additional Hooks (0.5+)
1407
+
1408
+ The SDK ships many hooks beyond the classic fifteen above. These are fully exported from `flowstack-sdk` and share the same `FlowstackProvider` context. Treat the type signatures in `packages/flowstack-sdk/src/hooks/<name>.ts` as the source of truth β€” the list below is a one-liner reference and will be auto-generated from source in a future release.
1409
+
1410
+ <!-- AUTO-GENERATED:START:hooks-extended -->
1411
+ - **`useAgent()`** β€” useAgent Hook The main hook for interacting with AI agents.
1412
+ - **`useAgents()`** β€” useAgents Hook Fetches the catalog of available agents from the backend.
1413
+ - **`useAuth()`** β€” useAuth Hook Provides authentication functionality including login, register, and Google OAuth.
1414
+ - **`useAuthGuard()`** β€” useAuthGuard Hook Provides auth guard logic without rendering components.
1415
+ - **`useAutomations()`** β€” CRUD for agent cron automations (P0-85).
1416
+ - **`useCollection()`** β€” Direct MongoDB access for built-app components.
1417
+ - **`useCollectionExplorer()`** β€” Browse, query, export, and delete a specific MongoDB collection.
1418
+ - **`useConnections()`** β€” Manage external service connections (Google, Reddit, Strava, Twitter, GitHub).
1419
+ - **`useConversations()`** β€” fetches the user's past Casino builder conversations from GET /library/conversations.
1420
+ - **`useDataOverview()`** β€” Unified summary of all user-owned data.
1421
+ - **`useDataSources()`** β€” useDataSources Hook Provides data source management for connecting to external databases.
1422
+ - **`useDatasets()`** β€” useDatasets Hook Provides dataset management functionality including upload, download, and deletion.
1423
+ - **`useFlowstackStatus()`** β€” useFlowstackStatus Hook Monitors connection status and health of the Flowstack backend.
1424
+ - **`useIntegrations()`** β€” CRUD for HTTP API integrations (P0-79).
1425
+ - **`useIntentAgent()`** β€” useIntentAgent Hook Creates and manages agents dynamically based on user intent.
1426
+ - **`useModels()`** β€” useModels Hook Provides ML model management functionality.
1427
+ - **`useOllamaDetection()`** β€” Detect a local Ollama instance and list available models.
1428
+ - **`useProviderCredentials()`** β€” Manage LLM provider credentials (BYOK + Ollama).
1429
+ - **`usePublicCollection()`** β€” anonymous public submissions for built apps.
1430
+ - **`useQuery()`** β€” useQuery Hook A lower-level hook for executing queries without managing chat history.
1431
+ - **`useReports()`** β€” useReports Hook Provides report management functionality.
1432
+ - **`useSiteVersions()`** β€” See source for usage.
1433
+ - **`useSites()`** β€” useSites Hook Provides published site management β€” list, create, stage files, publish to CDN, delete.
1434
+ - **`useToolInvocation()`** β€” Direct tool invocation hook.
1435
+ - **`useUserCollections()`** β€” List all MongoDB collections the user owns.
1436
+ - **`useUserManagement()`** β€” useUserManagement Hook Provides user management functionality for developers to track and manage users who sign up through their apps.
1437
+ - **`useVisualizations()`** β€” useVisualizations Hook Provides access to workspace visualizations.
1438
+ - **`useWorkspace()`** β€” useWorkspace Hook Provides workspace management functionality.
1439
+ <!-- AUTO-GENERATED:END:hooks-extended -->
1440
+
1441
+ ---
1442
+
1443
+ ### useIntegrations
1444
+
1445
+ Register any HTTPS REST API as a named integration that the agent can call as a tool. Credentials are encrypted at rest; raw secrets are never returned after creation.
1446
+
1447
+ ```tsx
1448
+ import { useIntegrations } from 'flowstack-sdk';
1449
+
1450
+ const { integrations, create, update, remove, isLoading } = useIntegrations();
1451
+
1452
+ // Register Shopify
1453
+ await create({
1454
+ name: 'Shopify',
1455
+ description: 'Shopify Admin API for order and product management',
1456
+ base_url: 'https://my-store.myshopify.com/admin/api/2024-01',
1457
+ auth_type: 'bearer', // 'bearer' | 'api_key_header' | 'api_key_query' | 'basic' | 'none'
1458
+ auth_config: { token: 'shpat_xxx' },
1459
+ endpoints: [
1460
+ { name: 'list_orders', method: 'GET', path: '/orders.json' },
1461
+ { name: 'get_order', method: 'GET', path: '/orders/{id}.json' },
1462
+ { name: 'cancel_order',method: 'POST', path: '/orders/{id}/cancel.json' },
1463
+ ],
1464
+ });
1465
+
1466
+ // Update credentials
1467
+ await update(id, { auth_config: { token: 'new_token' } });
1468
+
1469
+ // Remove
1470
+ await remove(id);
1471
+ ```
1472
+
1473
+ Return value:
1474
+
1475
+ ```tsx
1476
+ {
1477
+ integrations: Integration[]; // [{integration_id, name, base_url, auth_type, endpoint_count, ...}]
1478
+ isLoading: boolean;
1479
+ error: string | null;
1480
+ create: (input) => Promise<Integration | null>;
1481
+ update: (id, input) => Promise<boolean>;
1482
+ remove: (id) => Promise<boolean>;
1483
+ get: (id) => Promise<Integration | null>; // includes full endpoint list
1484
+ refresh: () => Promise<void>;
1485
+ }
1486
+ ```
1487
+
1488
+ ---
1489
+
1490
+ ### useAutomations
1491
+
1492
+ Schedule agent prompts on a cron schedule via AWS EventBridge. Results can be delivered silently (stored only), by email, webhook, or as a downloadable file.
1493
+
1494
+ Schedule format: **5-field Unix cron** `"minute hour dom month dow"`
1495
+
1496
+ ```tsx
1497
+ import { useAutomations } from 'flowstack-sdk';
1498
+
1499
+ const { automations, create, pause, resume, runNow, getRuns } = useAutomations();
1500
+
1501
+ // Daily sales digest β€” weekdays at 9 AM Eastern
1502
+ await create({
1503
+ name: 'Daily sales digest',
1504
+ prompt: "Pull yesterday's orders from Shopify, summarize revenue by product and region, flag any anomalies.",
1505
+ schedule: '0 9 * * 1-5',
1506
+ timezone: 'America/New_York',
1507
+ target_agents: ['shopify_analyst'], // routes to a specific agent persona; omit for default
1508
+ output_config: {
1509
+ type: 'email',
1510
+ to: 'team@company.com',
1511
+ subject_template: 'Sales digest β€” {date}',
1512
+ },
1513
+ });
1514
+
1515
+ // Run once right now (ignores schedule)
1516
+ await runNow(automationId);
1517
+
1518
+ // Pause / resume
1519
+ await pause(automationId);
1520
+ await resume(automationId);
1521
+
1522
+ // Get last 10 run results
1523
+ const runs = await getRuns(automationId, 10);
1524
+ // [{run_id, status, started_at, duration_ms, credits_used, output_summary, output_url}]
1525
+ ```
1526
+
1527
+ Output config options:
1528
+
1529
+ | `type` | Delivers via | Extra fields |
1530
+ |---|---|---|
1531
+ | `"silent"` | Stored only (default) | β€” |
1532
+ | `"email"` | Email | `to`, `subject_template`, `format` |
1533
+ | `"webhook"` | HTTP POST | `url`, `headers`, `format` |
1534
+ | `"file"` | Downloadable URL in `output_url` | `format` (`"csv"` \| `"json"` \| `"pdf"`) |
1535
+
1536
+ Return value:
1537
+
1538
+ ```tsx
1539
+ {
1540
+ automations: Automation[]; // [{automation_id, name, schedule, status, last_run_at, run_count, ...}]
1541
+ isLoading: boolean;
1542
+ error: string | null;
1543
+ create: (input) => Promise<Automation | null>;
1544
+ update: (id, input) => Promise<boolean>;
1545
+ remove: (id) => Promise<boolean>;
1546
+ pause: (id) => Promise<boolean>;
1547
+ resume: (id) => Promise<boolean>;
1548
+ runNow: (id) => Promise<{ invoked: boolean } | null>;
1549
+ getRuns: (id, limit?) => Promise<AutomationRun[]>;
1550
+ refresh: () => Promise<void>;
1551
+ }
1552
+ ```
1553
+
1554
+ ---
1555
+
1556
+ ## Components
1557
+
1558
+ ### Pre-built Page Components
1559
+
1560
+ ```tsx
1561
+ import { AuthPage, DashboardLayout, ChatPage } from 'flowstack-sdk';
1562
+
1563
+ // Complete auth page with login/register tabs
1564
+ <AuthPage defaultTab="login" onSuccess={() => router.push('/dashboard')} showGoogle />
1565
+
1566
+ // Dashboard layout with sidebar
1567
+ <DashboardLayout sidebar={<Sidebar />} header={<Header />}>
1568
+ <Content />
1569
+ </DashboardLayout>
1570
+
1571
+ // Full chat interface
1572
+ <ChatPage title="AI Assistant" placeholder="Ask anything..." welcomeMessage={<Welcome />} />
1573
+ ```
1574
+
1575
+ ### Form Components
1576
+
1577
+ ```tsx
1578
+ import { LoginForm, RegisterForm, AuthGuard } from 'flowstack-sdk';
1579
+
1580
+ <LoginForm onSuccess={handleSuccess} onError={handleError} showRegisterLink />
1581
+ <RegisterForm onSuccess={handleSuccess} showLoginLink />
1582
+
1583
+ // Route protection
1584
+ <AuthGuard fallback={<LoginPage />} requireWorkspace>
1585
+ <ProtectedContent />
1586
+ </AuthGuard>
1587
+ ```
1588
+
1589
+ ### Workspace & Data Components
1590
+
1591
+ ```tsx
1592
+ import { WorkspaceSelector, CreateWorkspaceModal, DatasetUploader, ChatInterface, MessageList } from 'flowstack-sdk';
1593
+
1594
+ <WorkspaceSelector onSelect={handleSelect} />
1595
+ <CreateWorkspaceModal isOpen={open} onCreated={handleCreated} />
1596
+ <DatasetUploader onUpload={handleUpload} accept=".csv,.xlsx,.parquet" />
1597
+ <ChatInterface template="data-science" height={500} placeholder="Ask about your data..." />
1598
+ <MessageList messages={messages} isStreaming={isStreaming} />
1599
+ ```
1600
+
1601
+ ### MarkdownRenderer
1602
+
1603
+ Drop-in component for rendering agent responses with full GFM support (tables, code blocks, inline formatting). Handles mid-stream table repair automatically β€” no Tailwind or external CSS required.
1604
+
1605
+ ```tsx
1606
+ import { useAgent, MarkdownRenderer } from 'flowstack-sdk';
1607
+
1608
+ function Chat() {
1609
+ const { messages, isStreaming } = useAgent('data-science');
1610
+ return (
1611
+ <div>
1612
+ {messages.map(msg =>
1613
+ msg.role === 'assistant' ? (
1614
+ <MarkdownRenderer
1615
+ key={msg.id}
1616
+ content={msg.content}
1617
+ isStreaming={isStreaming && msg === messages.at(-1)}
1618
+ />
1619
+ ) : (
1620
+ <p key={msg.id}>{msg.content}</p>
1621
+ )
1622
+ )}
1623
+ </div>
1624
+ );
1625
+ }
1626
+ ```
1627
+
1628
+ | Prop | Type | Description |
1629
+ |------|------|-------------|
1630
+ | `content` | `string` | Raw markdown from `message.content` |
1631
+ | `isStreaming` | `boolean?` | Pass `true` while streaming to apply mid-stream table repair |
1632
+ | `className` | `string?` | Custom class for the wrapper `<div>` |
1633
+
1634
+ ---
1635
+
1636
+ ## Wallet Module
1637
+
1638
+ > **Built apps: do not use this module for authentication.** The wallet module requires peer dependencies (`@privy-io/react-auth`, `wagmi`, `viem`, `@tanstack/react-query`) and needs Privy running on the page's origin β€” which is only possible on `casino.flowstack.fun`, not on `*.casino.flowstack.fun` built-app subdomains. **Use `BrokeredLoginButton` for auth in built apps.**
1639
+ >
1640
+ > Use this module only when your app needs to read/display INFER token balances, accept token payments, or access wallet addresses directly.
1641
+
1642
+ Separate entry point at `flowstack-sdk/wallet` β€” import only when you need INFER token payments. Peer dependencies required:
1643
+
1644
+ ```json
1645
+ {
1646
+ "dependencies": {
1647
+ "@privy-io/react-auth": ">=2.0.0",
1648
+ "wagmi": ">=2.0.0",
1649
+ "viem": ">=2.0.0",
1650
+ "@tanstack/react-query": ">=5.0.0"
1651
+ }
1652
+ }
1653
+ ```
1654
+
1655
+ ### Hooks
1656
+
1657
+ ```tsx
1658
+ import { useWalletAuth, useInferBalance, useAgentBalance, useDeposit, useBuyInfer } from 'flowstack-sdk/wallet';
1659
+ ```
1660
+
1661
+ #### useWalletAuth
1662
+
1663
+ ```tsx
1664
+ const {
1665
+ isConnected, // boolean
1666
+ isLoading, // boolean
1667
+ address, // string | null β€” checksummed wallet address
1668
+ isEmbeddedWallet, // boolean β€” Privy embedded vs MetaMask
1669
+ authMethod, // 'privy' | 'siwe' | null
1670
+ error, // string | null
1671
+ login, // (method?: 'privy' | 'wallet') => Promise<void>
1672
+ logout, // () => void
1673
+ } = useWalletAuth();
1674
+ ```
1675
+
1676
+ #### useInferBalance
1677
+
1678
+ ```tsx
1679
+ const {
1680
+ data, // InferBalance | null β€” { balance, available, queryCredits, balanceWei, heldWei, availableWei }
1681
+ isLoading, // boolean
1682
+ error, // string | null
1683
+ refetch, // () => Promise<void>
1684
+ } = useInferBalance();
1685
+ ```
1686
+
1687
+ #### useDeposit
1688
+
1689
+ ```tsx
1690
+ const {
1691
+ deposit, // (amount: number) => Promise<string | null> β€” returns tx hash
1692
+ isDepositing, // boolean
1693
+ txHash, // string | null
1694
+ error, // string | null
1695
+ } = useDeposit();
1696
+ ```
1697
+
1698
+ #### useBuyInfer
1699
+
1700
+ ```tsx
1701
+ const {
1702
+ buy, // (amount?: number) => void β€” opens fiat on-ramp widget
1703
+ isBuying, // boolean
1704
+ status, // 'idle' | 'pending' | 'completed' | 'failed'
1705
+ error, // string | null
1706
+ } = useBuyInfer();
1707
+ ```
1708
+
1709
+ #### useAgentBalance
1710
+
1711
+ Polls `GET /billing/agent/balance` for the connected wallet's AGENT token balance. For built apps that need to check whether the end-user has enough AGENT to run an operation.
1712
+
1713
+ ```tsx
1714
+ const {
1715
+ data, // AgentBalance | null β€” { balance, available, buildCredits, balanceWei, ... }
1716
+ isLoading, // boolean
1717
+ error, // string | null
1718
+ refetch, // () => Promise<void>
1719
+ } = useAgentBalance();
1720
+
1721
+ // data.available β€” spendable AGENT (balance minus active holds)
1722
+ // data.balance β€” total AGENT in wallet
1723
+ ```
1724
+
1725
+ ### Components
1726
+
1727
+ ```tsx
1728
+ import {
1729
+ LoginButton, InferBalanceBadge, AgentBalanceBadge,
1730
+ NeedsAgent, PaymentRequired, BuyInferModal
1731
+ } from 'flowstack-sdk/wallet';
1732
+
1733
+ // Privy/SIWE login β€” Casino PLATFORM only, NOT for built apps
1734
+ // Built apps use BrokeredLoginButton from 'flowstack-sdk' instead
1735
+ <LoginButton />
1736
+
1737
+ // Display wallet balances
1738
+ <InferBalanceBadge />
1739
+ <AgentBalanceBadge />
1740
+
1741
+ // Gate a feature behind a minimum AGENT balance.
1742
+ // If insufficient β†’ shows "Get AGENT β†’" CTA deep-linking to OIF /buy.
1743
+ // If sufficient β†’ renders children and calls onProceed when clicked.
1744
+ <NeedsAgent
1745
+ amountNeeded={2}
1746
+ onProceed={generateAlbumArt}
1747
+ returnUrl={window.location.href}
1748
+ >
1749
+ <p>Generate a new design β€” costs 2 AGENT</p>
1750
+ </NeedsAgent>
1751
+
1752
+ // Gate content behind payment
1753
+ <PaymentRequired>
1754
+ <PaidContent />
1755
+ </PaymentRequired>
1756
+
1757
+ // Buy INFER tokens modal
1758
+ <BuyInferModal />
1759
+ ```
1760
+
1761
+ **`<NeedsAgent>` deep-link flow:**
1762
+ 1. Built app renders `<NeedsAgent amountNeeded={5} returnUrl={window.location.href}>`
1763
+ 2. User has 0 AGENT β†’ "Get AGENT β†’" button appears
1764
+ 3. Click opens `https://openinferencefoundation.org/buy?returnTo=<encoded-url>&need=5`
1765
+ 4. User buys AGENT with a credit card on OIF (no wallet setup required β€” Privy creates it)
1766
+ 5. OIF redirects back to `returnUrl` β€” user now has AGENT and can proceed
1767
+
1768
+ ---
1769
+
1770
+ ## Direct Agent Access
1771
+
1772
+ Discover and target specific agents directly, bypassing the default strategic planner routing.
1773
+
1774
+ ### useAgents (Discovery)
1775
+
1776
+ ```tsx
1777
+ const { agents, isLoading, refreshAgents } = useAgents();
1778
+
1779
+ // agents: AgentInfo[] β€” each has name, description, tools, triggerPhrases, useFor
1780
+ ```
1781
+
1782
+ ### Targeting Custom Agents (Built Apps)
1783
+
1784
+ For built apps, `targetAgents` refers to agents **you create** via `casino_create_agent`. There are no pre-built ecosystem agents β€” you define the persona and capabilities, the platform enforces them.
1785
+
1786
+ ```tsx
1787
+ // 1. Create your agent via MCP first (once, at setup time):
1788
+ // casino_create_agent(site_id, "support_bot", "You are a helpful support assistant...", capabilities=["data_access"])
1789
+ // OR for per-tool control: casino_create_agent(site_id, "support_bot", "...", tools=["query_mongodb", "count_documents"])
1790
+
1791
+ // 2. Target it from your built app:
1792
+ const { query, messages, isStreaming } = useAgent(undefined, {
1793
+ targetAgents: ['support_bot'], // must match the name you registered
1794
+ capabilities: ['data_access'], // advisory β€” agent definition overrides this
1795
+ });
1796
+ ```
1797
+
1798
+ **CRITICAL:** `targetAgents` is required for built-app users (`appScope` set). Without it, app-scoped users get 403. The name must exactly match a `casino_create_agent` registration for your site.
1799
+
1800
+ ### Available Tool Categories
1801
+
1802
+ Declare these in `capabilities` when registering an agent or calling `useAgent`:
1803
+
1804
+ | Category | What it provides |
1805
+ |----------|-----------------|
1806
+ | `data_access` | query_mongodb, insert_documents, update_documents, aggregate_mongodb, list_collections, query_data_source, and more |
1807
+ | `external_integration` | web_search, web_search_deep, fetch_url, google_analytics, google_ads, google_drive, RSS, Reddit |
1808
+ | `code_execution` | python_exec, daytona_run_code |
1809
+ | `workspace_management` | save_document, list_documents, search_documents, save_report, save_visualization |
1810
+ | `site_operations` | build_project, write_project_file, get_site_files, publish_version |
1811
+ | `agent_management` | spawn_subagent, list_subagents |
1812
+
1813
+ Most built-app agents only need `data_access`. Use `casino_list_tools` (MCP) to see the full tool list for each category.
1814
+
1815
+ ### Tool-level Access Control (P0-117)
1816
+
1817
+ In addition to `capabilities` (category grants), you can pass an explicit `tools` list to `casino_create_agent` for per-tool access control:
1818
+
1819
+ | Param | Granularity | Example |
1820
+ |-------|-------------|---------|
1821
+ | `capabilities` | Category β€” all tools in that group | `["data_access"]` β†’ all 14 data tools |
1822
+ | `tools` | Individual tool names | `["query_mongodb", "count_documents"]` β†’ only those 2 |
1823
+
1824
+ **Priority:** `tools` (specific) overrides `capabilities` (category). If only `capabilities` is set, P0-116 behavior is unchanged.
1825
+
1826
+ Use `casino_list_tools()` first to see all valid tool names and categories, then pass exact names:
1827
+
1828
+ ```
1829
+ // Step 1 β€” discover available tool names:
1830
+ casino_list_tools()
1831
+ // β†’ returns { "data": ["query_mongodb", "insert_documents", ...], "capabilities": [...], ... }
1832
+
1833
+ // Step 2 β€” create a read-only support bot (no write access):
1834
+ casino_create_agent(
1835
+ site_id="<your_site_id>",
1836
+ name="support_bot",
1837
+ system_prompt="You are a helpful support assistant...",
1838
+ tools=["query_mongodb", "count_documents", "describe_collection"]
1839
+ )
1840
+ // β†’ capabilities auto-derived as ["data_access"], tools enforced at runtime
1841
+ ```
1842
+
1843
+ **Always-on tool exemption:** `web_search`, `create_diagram`, `list_sites`, `get_site_files`, and the 6 category meta-tools are never filtered β€” only category-loaded tools are subject to the allowlist.
1844
+
1845
+ ---
1846
+
1847
+ ## Agent Catalog
1848
+
1849
+ There are no pre-built ecosystem agents. Agents are per-app β€” you create them with `casino_create_agent` via the Casino MCP, then target them by name.
1850
+
1851
+ **Creating an agent (MCP) β€” category grant:**
1852
+ ```
1853
+ casino_create_agent(
1854
+ site_id="<your_site_id>",
1855
+ name="support_bot",
1856
+ system_prompt="You are a helpful support assistant...",
1857
+ capabilities=["data_access"] // scopes what tools this agent can use
1858
+ )
1859
+ ```
1860
+
1861
+ **Creating an agent (MCP) β€” per-tool grant:**
1862
+ ```
1863
+ casino_create_agent(
1864
+ site_id="<your_site_id>",
1865
+ name="readonly_bot",
1866
+ system_prompt="You are a read-only support assistant...",
1867
+ tools=["query_mongodb", "count_documents"] // only these 2 tools, no writes
1868
+ )
1869
+ ```
1870
+
1871
+ **Targeting it from your app:**
1872
+ ```tsx
1873
+ const { query, messages } = useAgent(undefined, {
1874
+ targetAgents: ['support_bot'],
1875
+ capabilities: ['data_access'],
1876
+ });
1877
+ ```
1878
+
1879
+ The backend reads the agent definition from your app's config, injects the system prompt, and enforces the declared capabilities β€” the client cannot escalate to broader tool access.
1880
+
1881
+ ---
1882
+
1883
+ ## Agent Templates & Factory
1884
+
1885
+ ### Pre-configured Templates
1886
+
1887
+ ```tsx
1888
+ import { dataScienceTemplate, marketingTemplate, supportTemplate, createCustomTemplate } from 'flowstack-sdk';
1889
+
1890
+ // Use with ChatInterface
1891
+ <ChatInterface template="data-science" />
1892
+ <ChatInterface template="marketing" />
1893
+ <ChatInterface template="support" />
1894
+
1895
+ // Custom template
1896
+ const custom = createCustomTemplate({
1897
+ streaming: true,
1898
+ networkMode: 'PUBLIC',
1899
+ systemPrompt: 'You are a helpful assistant for ...',
1900
+ });
1901
+ ```
1902
+
1903
+ ### Agent Factory (Dynamic Routing)
1904
+
1905
+ ```tsx
1906
+ import { AgentFactory, IntentAnalyzer, AgentRegistry, DEFAULT_PATTERNS } from 'flowstack-sdk';
1907
+
1908
+ // Register agents
1909
+ const registry = new AgentRegistry();
1910
+ registry.register({
1911
+ name: 'data-analyst',
1912
+ description: 'Analyzes datasets and creates visualizations',
1913
+ template: dataScienceTemplate,
1914
+ patterns: [/analyz/i, /chart/i, /data/i],
1915
+ });
1916
+
1917
+ // Analyze user intent and route to best agent
1918
+ const analyzer = new IntentAnalyzer({ registry });
1919
+ const intent = analyzer.analyze('Show me a chart of Q4 revenue');
1920
+ // β†’ { category: 'data-analysis', confidence: 0.92, entities: ['Q4 revenue'] }
1921
+ ```
1922
+
1923
+ ---
1924
+
1925
+ ## API Client
1926
+
1927
+ Direct API access β€” use these when you need lower-level control than hooks provide.
1928
+
1929
+ ```tsx
1930
+ import {
1931
+ // Auth
1932
+ login, register,
1933
+ // Workspaces
1934
+ listWorkspaces, createWorkspace, getWorkspace,
1935
+ // Datasets
1936
+ listDatasets, getDataset, getDatasetPreview, deleteDataset,
1937
+ // Visualizations & Reports
1938
+ listVisualizations, listReports,
1939
+ // Models & Scripts
1940
+ listModels, getModel, listScripts,
1941
+ // Data Sources
1942
+ listDataSources, createDataSource, testDataSource, deleteDataSource,
1943
+ // Sites
1944
+ listSites, getSite, createSite, addSiteFile, publishStagedSite, deleteSite,
1945
+ // User Management
1946
+ listUsers, getUser, updateUser, deleteUser, suspendUser, reactivateUser,
1947
+ getUserActivity, getUserStats, checkAdminPermissions,
1948
+ // Conversations
1949
+ getConversationHistory,
1950
+ // Query
1951
+ executeQuery, executeQueryWithConfig,
1952
+ // File Upload
1953
+ uploadFile,
1954
+ // Low-level
1955
+ flowstackFetch,
1956
+ } from 'flowstack-sdk';
1957
+ ```
1958
+
1959
+ All functions take `(credentials, ...params, config?)` and return `Promise<ApiResponse<T>>`.
1960
+
1961
+ ---
1962
+
1963
+ ## Streaming Utilities
1964
+
1965
+ Parse Server-Sent Events from agent streams.
1966
+
1967
+ ```tsx
1968
+ import { parseSSELine, parseSSEStream, processSSEStream } from 'flowstack-sdk';
1969
+
1970
+ // Parse a single SSE line
1971
+ const event = parseSSELine('data: {"type":"content","content":"Hello"}');
1972
+
1973
+ // Parse a full SSE stream (async generator)
1974
+ for await (const event of parseSSEStream(response)) {
1975
+ switch (event.type) {
1976
+ case 'content': console.log(event.content); break;
1977
+ case 'tool_start': console.log('Tool:', event.tool); break;
1978
+ case 'tool_result': console.log('Result:', event.result); break;
1979
+ case 'done': console.log('Complete'); break;
1980
+ }
1981
+ }
1982
+
1983
+ // High-level: process stream with callbacks
1984
+ await processSSEStream(response, {
1985
+ onContent: (text) => appendMessage(text),
1986
+ onToolStart: (tool) => showToolIndicator(tool),
1987
+ onDone: () => finalize(),
1988
+ });
1989
+ ```
1990
+
1991
+ **StreamEvent types:** `content`, `delta`, `text`, `tool_start`, `tool_result`, `tool_error`, `progress`, `status`, `interrupt`, `done`, `error`
1992
+
1993
+ ---
1994
+
1995
+ ## Cache Utilities
1996
+
1997
+ Per-resource client-side caching with TTL.
1998
+
1999
+ ```tsx
2000
+ import {
2001
+ // Generic
2002
+ getCached, setCached, deleteCached,
2003
+ CACHE_TTL,
2004
+ // Resource-specific
2005
+ getCachedWorkspaces, setCachedWorkspaces, invalidateWorkspacesCache,
2006
+ getCachedDatasets, setCachedDatasets, invalidateDatasetsCache,
2007
+ getCachedVisualizations, setCachedVisualizations, invalidateVisualizationsCache,
2008
+ getCachedReports, setCachedReports, invalidateReportsCache,
2009
+ getCachedSites, setCachedSites, invalidateSitesCache,
2010
+ // Bulk invalidation
2011
+ invalidateWorkspaceArtifacts, // Invalidate all artifacts for a workspace
2012
+ invalidateAllUserCache, // Clear everything for current user
2013
+ } from 'flowstack-sdk';
2014
+ ```
2015
+
2016
+ ---
2017
+
2018
+ ## API Route Generators
2019
+
2020
+ Generate Next.js API routes for auth endpoints.
2021
+
2022
+ ```tsx
2023
+ // app/api/auth/login/route.ts
2024
+ import { createLoginRoute } from 'flowstack-sdk';
2025
+
2026
+ export const POST = createLoginRoute({
2027
+ jwtSecret: process.env.JWT_SECRET!,
2028
+ passwordSecret: process.env.PASSWORD_SECRET!,
2029
+ });
2030
+
2031
+ // app/api/auth/register/route.ts
2032
+ import { createRegisterRoute } from 'flowstack-sdk';
2033
+
2034
+ export const POST = createRegisterRoute({
2035
+ jwtSecret: process.env.JWT_SECRET!,
2036
+ passwordSecret: process.env.PASSWORD_SECRET!,
2037
+ });
2038
+ ```
2039
+
2040
+ ---
2041
+
2042
+ ## Mock Mode
2043
+
2044
+ Develop and test without a backend. All hooks work offline with realistic mock data.
2045
+
2046
+ ```tsx
2047
+ <FlowstackProvider config={{ ...config, mode: 'mock' }}>
2048
+ <App />
2049
+ </FlowstackProvider>
2050
+ ```
2051
+
2052
+ | Hook | Behavior |
2053
+ |------|----------|
2054
+ | `useAuth` | Login/register succeed with any credentials |
2055
+ | `useWorkspace` | Returns 3 sample workspaces |
2056
+ | `useDatasets` | Returns sample datasets, upload/delete simulated |
2057
+ | `useAgent` | Returns contextual responses with simulated streaming |
2058
+ | `useUserManagement` | Returns 5 sample users with stats |
2059
+ | `useDataSources` | Returns sample MongoDB/PostgreSQL sources |
2060
+ | `useSites` | Returns empty sites array |
2061
+
2062
+ ```tsx
2063
+ import {
2064
+ mockCredentials, mockUser, mockWorkspaces, mockDatasets,
2065
+ mockVisualizations, mockDataSources, mockManagedUsers,
2066
+ mockUserStats, mockUserActivity, mockChatHistory,
2067
+ generateMockId, mockDelay,
2068
+ } from 'flowstack-sdk';
2069
+ ```
2070
+
2071
+ ---
2072
+
2073
+ ## Configuration
2074
+
2075
+ ```tsx
2076
+ const config = {
2077
+ // Required
2078
+ jwtSecret: process.env.JWT_SECRET!,
2079
+ passwordSecret: process.env.PASSWORD_SECRET!,
2080
+
2081
+ // API
2082
+ baseUrl: 'https://sage-api.flowstack.fun',
2083
+ tenantId: 't_6fe54402be43',
2084
+ mode: 'production', // 'production' | 'development' | 'mock'
2085
+ storage: 'local', // 'local' | 'session'
2086
+
2087
+ // Google OAuth (optional)
2088
+ auth: {
2089
+ googleClientId: process.env.GOOGLE_CLIENT_ID,
2090
+ },
2091
+
2092
+ // Wallet / INFER token (optional)
2093
+ privyConfig: { appId: 'your-privy-app-id' },
2094
+ chain: 'arbitrum-sepolia', // 'arbitrum-sepolia' | 'arbitrum'
2095
+ onRampConfig: { apiKey: 'moonpay-api-key', environment: 'sandbox' },
2096
+ };
2097
+ ```
2098
+
2099
+ | Option | Type | Required | Description |
2100
+ |--------|------|----------|-------------|
2101
+ | `jwtSecret` | `string` | Yes | JWT secret for token verification |
2102
+ | `passwordSecret` | `string` | Yes | Secret for password hashing |
2103
+ | `baseUrl` | `string` | No | API base URL (default: `https://sage-api.flowstack.fun`) |
2104
+ | `tenantId` | `string` | No | Tenant ID for user isolation |
2105
+ | `mode` | `string` | No | `'production'` \| `'development'` \| `'mock'` |
2106
+ | `storage` | `string` | No | `'local'` \| `'session'` for credentials |
2107
+ | `auth.googleClientId` | `string` | No | Google OAuth client ID |
2108
+ | `privyConfig.appId` | `string` | No | Privy app ID for embedded wallets |
2109
+ | `chain` | `string` | No | Blockchain for INFER token |
2110
+ | `onRampConfig` | `object` | No | Fiat on-ramp (MoonPay/Transak) config |
2111
+
2112
+ ### Configuration Validation
2113
+
2114
+ ```tsx
2115
+ import { validateConfig, validateConfigOrThrow, isDevelopmentConfig, getConfigSummary } from 'flowstack-sdk';
2116
+
2117
+ const result = validateConfig(config);
2118
+ if (!result.valid) console.error('Errors:', result.errors);
2119
+ result.warnings.forEach(w => console.warn(w));
2120
+
2121
+ // Or throw on invalid
2122
+ validateConfigOrThrow(config);
2123
+ ```
2124
+
2125
+ ---
2126
+
2127
+ ## Error Handling
2128
+
2129
+ Structured errors with codes and recovery suggestions.
2130
+
2131
+ ```tsx
2132
+ import { FlowstackError, isFlowstackError, ErrorCodes } from 'flowstack-sdk';
2133
+
2134
+ try {
2135
+ await someOperation();
2136
+ } catch (error) {
2137
+ if (isFlowstackError(error)) {
2138
+ console.log(error.code); // 'AUTHENTICATION_FAILED'
2139
+ console.log(error.message); // Technical message
2140
+ console.log(error.userMessage); // User-friendly message
2141
+ console.log(error.recoveryAction); // Suggested action
2142
+
2143
+ switch (error.code) {
2144
+ case ErrorCodes.AUTHENTICATION_FAILED: redirectToLogin(); break;
2145
+ case ErrorCodes.RATE_LIMITED: showRateLimitWarning(); break;
2146
+ case ErrorCodes.NETWORK_ERROR: showOfflineMessage(); break;
2147
+ }
2148
+ }
2149
+ }
2150
+ ```
2151
+
2152
+ | Code | Description |
2153
+ |------|-------------|
2154
+ | `NETWORK_ERROR` | Unable to connect to server |
2155
+ | `AUTHENTICATION_FAILED` | Login credentials invalid |
2156
+ | `ACCOUNT_NOT_ACTIVE` | Account needs verification |
2157
+ | `WORKSPACE_NOT_FOUND` | Workspace doesn't exist |
2158
+ | `DATASET_UPLOAD_FAILED` | File upload failed |
2159
+ | `QUERY_FAILED` | AI query failed |
2160
+ | `RATE_LIMITED` | Too many requests |
2161
+ | `CONFIG_INVALID` | Invalid SDK configuration |
2162
+ | `SERVER_ERROR` | Backend server error |
2163
+
2164
+ ---
2165
+
2166
+ ## TypeScript
2167
+
2168
+ Full TypeScript support. Key type exports:
2169
+
2170
+ ```tsx
2171
+ import type {
2172
+ // Config
2173
+ FlowstackConfig, AuthConfig, RedisConfig, DatabaseConfig,
2174
+ // Auth
2175
+ User, FlowstackCredentials, UserRole, UserStatus, ManagedUser,
2176
+ LoginRequest, LoginResponse, RegisterRequest, RegisterResponse,
2177
+ // Workspace
2178
+ WorkspaceInfo, CreateWorkspaceRequest, SessionState,
2179
+ // Data
2180
+ DatasetInfo, DatasetPreview, ColumnSchema, DatasetRow,
2181
+ VisualizationData, ReportInfo, ModelInfo, ScriptInfo,
2182
+ DataSource, DataSourceType, DataSourceConfig, ConnectionTestResult,
2183
+ // Sites
2184
+ PublishedSiteInfo, CreateSiteParams, UseSitesReturn,
2185
+ // Chat & Streaming
2186
+ ChatMessage, ToolCall, StreamEvent, StreamEventType, InterruptInfo,
2187
+ DataSourceBadgeInfo, SearchResult, SearchResultsData,
2188
+ // Agent
2189
+ AgentTemplate, AgentConfig, QueryOptions,
2190
+ UseAgentReturn, UseAgentOptions,
2191
+ // Factory
2192
+ DynamicAgentConfig, IntentCategory, IntentEntity, IntentAnalysis,
2193
+ IntentPattern, RegisteredAgent, UseIntentAgentReturn,
2194
+ // Hook Returns
2195
+ UseAuthReturn, UseWorkspaceReturn, UseDatasetsReturn,
2196
+ UseVisualizationsReturn, UseReportsReturn, UseModelsReturn,
2197
+ UseDataSourcesReturn, UseQueryReturn, UseUserManagementReturn,
2198
+ // API
2199
+ ApiResponse, ListResponse, FlowstackClientConfig, RequestOptions,
2200
+ // Context
2201
+ FlowstackContextValue,
2202
+ // User Management
2203
+ UserActivityLog, UpdateUserRequest, UserStats, UserListParams,
2204
+ // Errors
2205
+ FlowstackError, ErrorCode, FlowstackErrorOptions,
2206
+ // Config Validation
2207
+ ValidationResult,
2208
+ } from 'flowstack-sdk';
2209
+
2210
+ // Wallet types (separate entry point)
2211
+ import type {
2212
+ WalletAuthState, UseWalletAuthReturn,
2213
+ InferBalance, UseInferBalanceReturn,
2214
+ UseDepositReturn, UseBuyInferReturn,
2215
+ WalletProviderProps,
2216
+ } from 'flowstack-sdk/wallet';
2217
+ ```
2218
+
2219
+ ---
2220
+
2221
+ ## MCP Tool Reference (for AI agents building Casino apps)
2222
+
2223
+ Use these tools in the Casino MCP server to build, stage, and manage apps.
2224
+
2225
+ ### Build Loop
2226
+
2227
+ | Tool | Description |
2228
+ |---|---|
2229
+ | `casino_create_site(name, description)` | Allocate site_id + bootstrap scaffold files |
2230
+ | `casino_stage_files(site_id, files)` | Stage multiple files in one call (dict of path→content) |
2231
+ | `casino_stage_file(site_id, path, content)` | Stage a single file |
2232
+ | `casino_list_staged(site_id)` | List staged files β€” survives failed builds |
2233
+ | `casino_clear_staged(site_id)` | Explicit staging reset (success clears automatically) |
2234
+ | `casino_build_staged(site_id, message)` | Build + publish; returns url, version, build_summary |
2235
+ | `casino_get_app_source(site_id)` | Fetch live source for editing |
2236
+
2237
+ #### Fast build loop (one-shot)
2238
+
2239
+ ```
2240
+ casino_create_site("My App")
2241
+ β†’ bootstrap.files (6 scaffold files, pre-wired with tenant/site config)
2242
+ β†’ site_id, url
2243
+
2244
+ casino_stage_files(site_id, {
2245
+ ...bootstrap.files, // package.json, tsconfig, vite.config.ts, index.html, src/main.tsx
2246
+ "src/App.tsx": "...", // your product component
2247
+ })
2248
+
2249
+ casino_build_staged(site_id)
2250
+ β†’ { url, version, build_summary: "sandbox: 3s | install+build: 35s | total: 38s" }
2251
+ ```
2252
+
2253
+ If the build fails β€” staged files are **preserved**. Fix the error and call `casino_build_staged` again without re-staging.
2254
+
2255
+ ### Agent Personas
2256
+
2257
+ | Tool | Description |
2258
+ |---|---|
2259
+ | `casino_create_agent(site_id, name, system_prompt, capabilities)` | Register new agent persona |
2260
+ | `casino_update_agent(site_id, name, system_prompt=None, ...)` | Patch existing agent β€” only provided fields updated |
2261
+ | `casino_list_agents(site_id)` | List registered personas |
2262
+
2263
+ `capabilities` values: `data_access`, `external_integration`, `site_operations`, `code_execution`, `domain_task`, `workspace_management`, `agent_management`
2264
+
2265
+ ### App Inspection
2266
+
2267
+ | Tool | Description |
2268
+ |---|---|
2269
+ | `casino_list_apps()` | List all your Casino apps |
2270
+ | `casino_list_collections(site_id)` | List MongoDB collections for an app |
2271
+ | `casino_list_tools()` | List all tool names valid for per-tool agent access control |
2272
+ | `casino_get_sdk_docs(topic)` | SDK documentation (full or filtered by topic) |
2273
+
2274
+ ---
2275
+
2276
+ ## License
2277
+
2278
+ MIT