# Flowstack SDK — Full Reference (SDK v0.3.3) This file contains the complete API reference, generated from the canonical doc registry. Each section documents one hook, component, utility, or endpoint. Note: the `flowstack-sdk` package and the REST API are versioned independently — the version above is the SDK; the REST & SSE API section below carries its own version. --- # Built Apps The generated-site pattern: provider, auth flow, and data operations for apps built on Flowstack. ## FlowstackProvider *provider* — The root provider that wraps a built app and supplies backend config (baseUrl, tenantId, mode, appScope) to all Flowstack hooks. ```ts import { FlowstackProvider } from 'flowstack-sdk' ``` Wrap your entire app in `FlowstackProvider` (Quick Start step 1). It supplies the backend connection config consumed by every Flowstack hook. For **built apps** the critical field is `appScope` — it scopes all data to the app and the build pipeline fills `__SITE_ID__` with the hex `site_id`. Built apps do NOT use workspaces; the backend creates them automatically. Generated sites always run in `mode: 'production'` — auth, agent chat, and data all connect to real infrastructure at `sage-api.flowstack.fun`. Do not use mock mode for deployed sites. No Privy app ID, `privyConfig.appId`, or wallet peer deps are needed for the broker auth flow. ```ts {children} ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `config` | `FlowstackConfig` | required | Backend configuration object (see fields below). | | `config.baseUrl` | `string` | required | API base URL, e.g. `'https://sage-api.flowstack.fun'`. | | `config.tenantId` | `string` | — | Optional. For authenticated calls the backend derives the tenant from the brokered-login JWT and ignores any client-sent value, so built apps should **not** set it. Required **only** for `usePublicCollection` (anonymous access has no token to derive from); if missing there, that hook raises a clear error rather than using a default. | | `config.mode` | `'production' | 'development' | 'mock'` | required | Connection mode. Generated/built sites must use `'production'` (real infrastructure at `sage-api.flowstack.fun`). `'mock'` and `'development'` are for local SDK development only — never deploy a built app with them. | | `config.appScope` | `string` | required | Built apps only — hex site ID that scopes all data to this app. The build pipeline auto-fills `__SITE_ID__`. | | `config.jwtSecret` | `string` | — | Casino platform config, e.g. `'flowstack-cdn-site'`. | | `config.passwordSecret` | `string` | — | Casino platform config, e.g. `'flowstack-cdn-site'`. | | `children` | `ReactNode` | required | The app tree that consumes Flowstack hooks. | ### Examples **Minimal built-app wrap** ```tsx import { FlowstackProvider, useFlowstack, BrokeredLoginButton } from 'flowstack-sdk'; function App() { return ( ); } ``` **Casino platform wrap** ```tsx import { FlowstackProvider } from 'flowstack-sdk'; const config = { baseUrl: 'https://sage-api.flowstack.fun', tenantId: 't_6fe54402be43', mode: 'production', jwtSecret: 'flowstack-cdn-site', passwordSecret: 'flowstack-cdn-site', }; export default function App({ children }) { return ( {children} ); } ``` > ⚠️ **Warning:** **`appScope` is required for built apps.** It scopes all data to this app; the build pipeline fills `__SITE_ID__` with the hex site_id. > ℹ️ **Note:** Generated sites always run in production mode and connect to real infrastructure at `sage-api.flowstack.fun`. Do not use mock mode for deployed sites. > ℹ️ **Note:** No `privyConfig.appId` or wallet peer deps (`@privy-io/react-auth`, `wagmi`, `viem`) are needed for the broker auth flow. > ℹ️ **Note:** **`tenantId` is derived from the JWT** — don't set it for built apps. The backend reads the tenant from the brokered-login token and ignores any client-sent `X-Tenant-ID`. The only place it must be set explicitly is `usePublicCollection` (anonymous, no token). **See also:** [useFlowstack](https://flowstack.fun/docs/useFlowstack) · [BrokeredLoginButton](https://flowstack.fun/docs/BrokeredLoginButton) · [AuthGuard](https://flowstack.fun/docs/AuthGuard) · [useCollection](https://flowstack.fun/docs/useCollection) · [useAgent](https://flowstack.fun/docs/useAgent) *Source: README.md#built-apps--generated-site-pattern* ## useFlowstack *hook* — The hook built apps use to read auth state — isAuthenticated, isInitialized, and credentials — instead of useAuth(). ```ts import { useFlowstack } from 'flowstack-sdk' ``` For built apps, read auth state from `useFlowstack()` instead of `useAuth()`. It exposes whether the provider has initialized, whether a session is active, and the active credentials (e.g. `credentials?.email`). It also exposes `setCredentials` — call `setCredentials(null)` to sign out. Built apps authenticate via `BrokeredLoginButton`; `useFlowstack()` is how they observe the resulting auth state and identity. ```ts const { isAuthenticated, isInitialized, credentials, setCredentials } = useFlowstack() ``` ### Returns | Field | Type | Description | |---|---|---| | `isAuthenticated` | `boolean` | Whether a session is active. | | `isInitialized` | `boolean` | Whether the provider has finished initializing — gate on this before rendering auth-dependent UI. | | `credentials` | `FlowstackCredentials | null` | Active session credentials, e.g. `credentials?.email`. | | `setCredentials` | `(creds: FlowstackCredentials | null) => void` | Set or clear credentials; call `setCredentials(null)` to sign out. | ### Examples **Gate UI on auth state** ```tsx function AuthGate() { const { isAuthenticated, isInitialized } = useFlowstack(); if (!isInitialized) return
Loading...
; // BrokeredLoginButton opens Casino's auth popup — Privy runs there, not here return isAuthenticated ? : (
); } ``` **Read identity and sign out** ```tsx function Header() { const { credentials, setCredentials } = useFlowstack(); return (
{credentials?.email}
); } ``` > ℹ️ **Note:** Built apps read auth state from `useFlowstack()`, not `useAuth()`. After auth, read identity from `credentials` here. **See also:** [BrokeredLoginButton](https://flowstack.fun/docs/BrokeredLoginButton) · [AuthGuard](https://flowstack.fun/docs/AuthGuard) · [useAuth](https://flowstack.fun/docs/useAuth) · [FlowstackProvider](https://flowstack.fun/docs/FlowstackProvider) *Source: README.md#useauth* ## useMessages *hook · since 0.2.2* — Read and send private messages within one DM thread in a built app. ```ts import { useMessages } from 'flowstack-sdk' ``` Reads and sends private messages in a single thread with `withUserKey`. Mirrors `useCollection`'s ergonomics but is backed by the server-owned, ACL'd DM endpoints instead of a Mongo collection. The backend pins the sender to the caller's identity and only delivers into a mutually-consented (open) thread, so `send()` to a not-yet-open thread returns an error. Private messaging is a built-app capability: it requires an `appScope` on the `FlowstackProvider` config. Treat the type signatures in `packages/flowstack-sdk/src/hooks/useMessages.ts` as the source of truth. ```ts const { messages, send, isLoading, error, refresh } = useMessages(withUserKey, options?) ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `withUserKey` | `string | null | undefined` | required, since 0.2.2 | The counterpart's user key. When null/undefined the hook stays idle (no fetch). | | `options.limit` | `number` | default `50`, since 0.2.2 | Max messages to load (max 200). | | `options.refreshInterval` | `number` | since 0.2.2 | Auto-poll interval in ms. No polling by default. | | `options.enabled` | `boolean` | default `true`, since 0.2.2 | Set `false` to skip the initial fetch (e.g. before a counterpart is selected). | ### Returns | Field | Type | Description | |---|---|---| | `messages` | `DmMessage[]` | Messages in the thread — each `{ message_id, from, to, body, created_at, read_at }`. | | `send` | `(body: string) => Promise` | Send a message to the counterpart. Refetches on success; throws if the thread is not yet mutually open. | | `isLoading` | `boolean` | True while a fetch is in flight. | | `error` | `string | null` | Error message from the last failed load or send. | | `refresh` | `() => Promise` | Refetch the message list. | ### Examples **Read and send** ```tsx const { messages, send } = useMessages(counterpartUserKey, { refreshInterval: 4000 }); await send('hey there'); messages.map((m) => ( {m.body} )); ``` > 🚫 **Critical:** Message bodies are UNTRUSTED user input. Render them as plain text or sanitized markdown — never as raw HTML (no `dangerouslySetInnerHTML`). If a body is ever passed to an agent, treat it as data, not instructions. > ⚠️ **Warning:** `send()` throws if the thread is not mutually open. Call `openThread()` (from `useThreads`) first and wait until both parties consent. **See also:** [useThreads](https://flowstack.fun/docs/useThreads) · [sendMessage](https://flowstack.fun/docs/sendMessage) · [listMessages](https://flowstack.fun/docs/listMessages) · [DmMessage](https://flowstack.fun/docs/DmMessage) *Source: README.md#private-messaging-dms* ## useSites *hook · ⚠ not for built apps* — Manage published sites — list, create, stage files, publish to CDN, and delete. ```ts import { useSites } from 'flowstack-sdk' ``` Published site management — list, create, stage files, publish to CDN, delete. There are **two publishing workflows**: quick publish (pass files inline to `createSite`) and staged publish (create an empty site, `addFile` incrementally, then `publishSite`). ```ts const { sites, isLoading, error, createSite, addFile, publishSite, deleteSite, refreshSites } = useSites() ``` ### Returns | Field | Type | Description | |---|---|---| | `sites` | `PublishedSiteInfo[]` | The published sites. | | `isLoading` | `boolean` | Request in flight. | | `error` | `string | null` | Last error message. | | `createSite` | `(params: CreateSiteParams) => Promise` | Create a site (optionally with inline files). | | `addFile` | `(siteId, path, content) => Promise` | Stage a file onto an existing site. | | `publishSite` | `(siteId) => Promise` | Publish a staged site to the CDN. | | `deleteSite` | `(siteId) => Promise` | Delete a site. | | `refreshSites` | `() => Promise` | Re-fetch the site list. | ### Examples **Hook surface** ```tsx const { sites, // PublishedSiteInfo[] isLoading, // boolean error, // string | null createSite, // (params: CreateSiteParams) => Promise addFile, // (siteId, path, content) => Promise publishSite, // (siteId) => Promise deleteSite, // (siteId) => Promise refreshSites, // () => Promise } = useSites(); ``` **Two publishing workflows** ```tsx // Quick publish — pass files inline await createSite({ name: 'My Dashboard', siteType: 'on_demand', files: { 'index.html': '...', 'styles.css': 'body { ... }' }, }); // Staged publish — add files incrementally, then publish const site = await createSite({ name: 'My App' }); await addFile(site.id, 'index.html', '...'); await addFile(site.id, 'src/App.tsx', 'export default function App() { ... }'); await publishSite(site.id); ``` > ℹ️ **Note:** **PublishedSiteInfo fields:** `id` (string) site ID; `name` (string) display name; `url` (string) published CDN URL; `shortUrl` (string?) short URL for sharing; `siteType` (`'on_demand' | 'daily' | 'js_build'`) how the site was created; `fileCount` (number) number of files; `totalBytes` (number?) total size; `createdAt` (string) ISO timestamp; `expiresAt` (string?) expiration if applicable. *Source: README.md#usesites* ## useThreads *hook · since 0.2.2* — List a built-app user's private message threads, with unread counts and consent control. ```ts import { useThreads } from 'flowstack-sdk' ``` Lists the authenticated user's private DM threads from the server-owned, ACL'd DM store — the backend only ever returns threads the caller participates in. Each thread carries the counterpart's user key, the last message, and an unread count. Private messaging is a built-app capability: it requires an `appScope` on the `FlowstackProvider` config. In a Casino personal session the backend returns 403 and this hook surfaces an error. Treat the type signatures in `packages/flowstack-sdk/src/hooks/useThreads.ts` as the source of truth. ```ts const { threads, isLoading, error, refresh, openThread } = useThreads(options?) ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `options.refreshInterval` | `number` | since 0.2.2 | Auto-poll interval in ms. No polling by default. | | `options.enabled` | `boolean` | default `true`, since 0.2.2 | Set `false` to skip the initial fetch. | ### Returns | Field | Type | Description | |---|---|---| | `threads` | `DmThread[]` | The caller's threads — each `{ pair_key, with_user_key, status, last_message, unread_count, updated_at }`. | | `isLoading` | `boolean` | True while a fetch is in flight. | | `error` | `string | null` | Error message, e.g. a 403 when used outside a built-app (no `appScope`). | | `refresh` | `() => Promise` | Refetch the thread list. | | `openThread` | `(withUserKey: string) => Promise<'pending' | 'open' | null>` | Record the caller's consent to open a thread. The thread becomes sendable only once BOTH parties consent. Idempotent per caller; refetches on success. | ### Examples **List threads with polling** ```tsx const { threads, isLoading, openThread } = useThreads({ refreshInterval: 5000 }); threads.map((t) => ( )); // Consent to open a new conversation const status = await openThread(otherUserKey); // 'pending' | 'open' | null ``` > ⚠️ **Warning:** Requires a built-app `appScope`. Outside a built app the backend returns 403 and `error` is set. **See also:** [useMessages](https://flowstack.fun/docs/useMessages) · [listThreads](https://flowstack.fun/docs/listThreads) · [openThread](https://flowstack.fun/docs/openThread) · [DmThread](https://flowstack.fun/docs/DmThread) *Source: README.md#private-messaging-dms* ## activateDomain *utility · since 0.2.8 · ⚠ not for built apps* — Client function: activate a domain once its ACM cert is issued (CloudFront goes live ~5–15 min after). ```ts import { activateDomain } from 'flowstack-sdk' ``` Non-React client function. Call after `getDomainStatus` reports `status: "issued"`. `POST /api/v1/apps/{siteId}/domains/{domain}/activate`. ```ts activateDomain(credentials, siteId, domain, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `domain` | `string` | required | The custom domain, e.g. `app.example.com`. | | `config` | `FlowstackClientConfig` | since 0.2.8 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `ActivateDomainResult`: `{ domain, site_id, status: "live", url, message }`. | ### Examples **Activate** ```ts const res = await activateDomain(credentials, siteId, 'app.example.com', { appScope }); if (res.ok) console.log('live at', res.data.url); ``` > ℹ️ **Note:** The cert must be `issued` first (see `getDomainStatus`). After activation CloudFront propagation takes ~5–15 minutes before the domain serves. > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [getDomainStatus](https://flowstack.fun/docs/getDomainStatus) · [connectDomain](https://flowstack.fun/docs/connectDomain) *Source: src/api/client.ts (Custom Domains, P0-158)* ## clearStagedFiles *utility · since 0.2.7 · ⚠ not for built apps* — Client function: clear all staged files for a site (the .staging marker is preserved). ```ts import { clearStagedFiles } from 'flowstack-sdk' ``` Non-React client function. `DELETE /api/v1/sites/{siteId}/staged`. ```ts clearStagedFiles(credentials, siteId, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `config` | `FlowstackClientConfig` | since 0.2.7 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | How many staged files were deleted. | ### Examples **Clear staged files** ```ts await clearStagedFiles(credentials, siteId, { appScope }); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [stageFiles](https://flowstack.fun/docs/stageFiles) · [listStagedFiles](https://flowstack.fun/docs/listStagedFiles) *Source: src/api/client.ts (Site Ops, P0-156)* ## connectDomain *utility · since 0.2.8 · ⚠ not for built apps* — Client function: start connecting a custom domain — requests an ACM cert and returns the DNS records to add. ```ts import { connectDomain } from 'flowstack-sdk' ``` Non-React client function. Kicks off custom-domain setup: requests the ACM certificate (DNS validation) and returns the validation + routing records to add at the registrar. Then poll `getDomainStatus`, and call `activateDomain` once the cert is issued. `POST /api/v1/apps/{siteId}/domains` with `{ domain }`. ```ts connectDomain(credentials, siteId, domain, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `domain` | `string` | required | The custom domain, e.g. `app.example.com`. | | `config` | `FlowstackClientConfig` | since 0.2.8 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `ConnectDomainResult`: `{ domain, site_id, status, cert_arn, validation_records, routing_record, message }`. Add `validation_records` (and `routing_record`) at your DNS provider. | ### Examples **Connect a domain** ```ts const res = await connectDomain(credentials, siteId, 'app.example.com', { appScope }); if (res.ok) { for (const r of res.data.validation_records) console.log(`${r.type} ${r.name} -> ${r.value}`); console.log('routing:', res.data.routing_record); } ``` > ℹ️ **Note:** Flow: `connectDomain` → add the returned DNS records → poll `getDomainStatus` until `status: "issued"` → `activateDomain`. CloudFront goes live ~5–15 min after activation. > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [getDomainStatus](https://flowstack.fun/docs/getDomainStatus) · [activateDomain](https://flowstack.fun/docs/activateDomain) · [listDomains](https://flowstack.fun/docs/listDomains) · [disconnectDomain](https://flowstack.fun/docs/disconnectDomain) · [CustomDomainRecord](https://flowstack.fun/docs/CustomDomainRecord) *Source: src/api/client.ts (Custom Domains, P0-158)* ## disconnectDomain *utility · since 0.2.8 · ⚠ not for built apps* — Client function: disconnect a custom domain from a site. ```ts import { disconnectDomain } from 'flowstack-sdk' ``` Non-React client function. Removes the domain mapping. `DELETE /api/v1/apps/{siteId}/domains/{domain}`. ```ts disconnectDomain(credentials, siteId, domain, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `domain` | `string` | required | The custom domain, e.g. `app.example.com`. | | `config` | `FlowstackClientConfig` | since 0.2.8 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | Confirmation of removal. | ### Examples **Disconnect** ```ts await disconnectDomain(credentials, siteId, 'app.example.com', { appScope }); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [connectDomain](https://flowstack.fun/docs/connectDomain) · [listDomains](https://flowstack.fun/docs/listDomains) *Source: src/api/client.ts (Custom Domains, P0-158)* ## dmPairKey *utility · since 0.2.2* — Pure helper: deterministic, order-independent thread key for a pair of user keys. ```ts import { dmPairKey } from 'flowstack-sdk' ``` Computes the deterministic `pair_key` for two user keys by sorting them and joining with `::`. Mirrors the backend's thread-key derivation, so `dmPairKey(a, b) === dmPairKey(b, a)`. Pure — no credentials, no network. Useful for locally matching a thread to a counterpart. ```ts dmPairKey(a: string, b: string): string ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `a` | `string` | required, since 0.2.2 | One user key. | | `b` | `string` | required, since 0.2.2 | The other user key. | ### Returns | Field | Type | Description | |---|---|---| | — | `string` | The sorted, `::`-joined pair key (order-independent). | ### Examples **Derive a thread key** ```ts dmPairKey('userA', 'userB'); // 'userA::userB' dmPairKey('userB', 'userA'); // 'userA::userB' (same) ``` **See also:** [openThread](https://flowstack.fun/docs/openThread) · [useThreads](https://flowstack.fun/docs/useThreads) · [DmThread](https://flowstack.fun/docs/DmThread) *Source: README.md#private-messaging-dms* ## getAppSource *utility · since 0.2.7 · ⚠ not for built apps* — Client function: fetch a version's source files as a { path: content } map (live version by default). ```ts import { getAppSource } from 'flowstack-sdk' ``` Non-React client function. Returns the source tree for a site version. Omit `version` for the live version. `GET /api/v1/sites/{siteId}/source?version={n}`. ```ts getAppSource(credentials, siteId, version?, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `version` | `number` | — | Specific version to fetch. Defaults to the live version. | | `config` | `FlowstackClientConfig` | since 0.2.7 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `AppSource`: `{ site_id, version, files: Record }` (version is null for legacy unversioned sites). | ### Examples **Read the live source** ```ts const res = await getAppSource(credentials, siteId, undefined, { appScope }); if (res.ok) for (const [p, content] of Object.entries(res.data.files)) console.log(p, content.length); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [stageFiles](https://flowstack.fun/docs/stageFiles) · [useSiteVersions](https://flowstack.fun/docs/useSiteVersions) *Source: src/api/client.ts (Site Ops, P0-156)* ## getDataPlan *utility · since 0.2.7 · ⚠ not for built apps* — Client function: get the saved mongoCollections schema (data plan) for a site. ```ts import { getDataPlan } from 'flowstack-sdk' ``` Non-React client function. `GET /api/v1/sites/{siteId}/data-plan`. ```ts getDataPlan(credentials, siteId, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `config` | `FlowstackClientConfig` | since 0.2.7 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | The stored `DataPlan` (its `mongoCollections` schema). | ### Examples **Read the data plan** ```ts const res = await getDataPlan(credentials, siteId, { appScope }); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [setDataPlan](https://flowstack.fun/docs/setDataPlan) *Source: src/api/client.ts (Site Ops, P0-156)* ## getDomainStatus *utility · since 0.2.8 · ⚠ not for built apps* — Client function: poll a custom domain's validation/activation status. ```ts import { getDomainStatus } from 'flowstack-sdk' ``` Non-React client function. Poll this after `connectDomain` until `status` is `issued`, then call `activateDomain`. `GET /api/v1/apps/{siteId}/domains/{domain}/status`. ```ts getDomainStatus(credentials, siteId, domain, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `domain` | `string` | required | The custom domain, e.g. `app.example.com`. | | `config` | `FlowstackClientConfig` | since 0.2.8 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `DomainStatusResult`: `{ domain, site_id, status, cert_arn?, validation_records, routing_record, connected_at?, last_check? }`. `status` is one of `validating | issued | activating | live | failed`. | ### Examples **Poll until issued** ```ts const res = await getDomainStatus(credentials, siteId, 'app.example.com', { appScope }); if (res.ok && res.data.status === 'issued') { await activateDomain(credentials, siteId, 'app.example.com', { appScope }); } ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [connectDomain](https://flowstack.fun/docs/connectDomain) · [activateDomain](https://flowstack.fun/docs/activateDomain) · [CustomDomainRecord](https://flowstack.fun/docs/CustomDomainRecord) *Source: src/api/client.ts (Custom Domains, P0-158)* ## listDomains *utility · since 0.2.8 · ⚠ not for built apps* — Client function: list the custom domains connected to a site and their status. ```ts import { listDomains } from 'flowstack-sdk' ``` Non-React client function. `GET /api/v1/apps/{siteId}/domains`. ```ts listDomains(credentials, siteId, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `config` | `FlowstackClientConfig` | since 0.2.8 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | All connected domains as `CustomDomainRecord[]`. | ### Examples **List domains** ```ts const res = await listDomains(credentials, siteId, { appScope }); if (res.ok) res.data.domains.forEach(d => console.log(d.domain, d.status)); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [connectDomain](https://flowstack.fun/docs/connectDomain) · [getDomainStatus](https://flowstack.fun/docs/getDomainStatus) · [CustomDomainRecord](https://flowstack.fun/docs/CustomDomainRecord) *Source: src/api/client.ts (Custom Domains, P0-158)* ## listMessages *utility · since 0.2.2* — Client function: list messages in the caller's thread with a counterpart. ACL'd server-side. ```ts import { listMessages } from 'flowstack-sdk' ``` Non-React client function backing `useMessages`. Lists messages in the caller's thread with `withUserKey`. Reads are ACL'd to thread participants. `config` must carry an `appScope`. ```ts listMessages(credentials, withUserKey, options?, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required, since 0.2.2 | The authenticated caller's credentials. | | `withUserKey` | `string` | required, since 0.2.2 | The counterpart's user key. | | `options.limit` | `number` | default `50`, since 0.2.2 | Max messages to load (max 200). | | `options.before` | `string` | since 0.2.2 | Cursor: return messages created before this id/timestamp (pagination). | | `config` | `FlowstackClientConfig` | required, since 0.2.2 | Client config. Must include `appScope`. | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `{ ok, data: { messages, count }, error }`. | ### Examples **List messages** ```ts const res = await listMessages(credentials, counterpartKey, { limit: 50 }, { appScope }); if (res.ok) console.log(res.data.messages); ``` > 🚫 **Critical:** `body` on each `DmMessage` is UNTRUSTED user input. Render as text / sanitized markdown, never raw HTML. **See also:** [useMessages](https://flowstack.fun/docs/useMessages) · [sendMessage](https://flowstack.fun/docs/sendMessage) · [DmMessage](https://flowstack.fun/docs/DmMessage) *Source: README.md#private-messaging-dms* ## listStagedFiles *utility · since 0.2.7 · ⚠ not for built apps* — Client function: list the files currently staged for a site. ```ts import { listStagedFiles } from 'flowstack-sdk' ``` Non-React client function. `GET /api/v1/sites/{siteId}/staged`. ```ts listStagedFiles(credentials, siteId, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `config` | `FlowstackClientConfig` | since 0.2.7 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `StagedFilesList`: `{ site_id, staged_files: { path, size_bytes, staged_at }[], count }`. | ### Examples **List staged files** ```ts const res = await listStagedFiles(credentials, siteId, { appScope }); if (res.ok) console.log(res.data.count, 'staged'); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [stageFiles](https://flowstack.fun/docs/stageFiles) · [clearStagedFiles](https://flowstack.fun/docs/clearStagedFiles) *Source: src/api/client.ts (Site Ops, P0-156)* ## listThreads *utility · since 0.2.2* — Client function: list the caller's DM threads (counterpart key, last message, unread count). ```ts import { listThreads } from 'flowstack-sdk' ``` Non-React client function backing `useThreads`. Lists the caller's threads from the server-owned, ACL'd DM store. `config` must carry an `appScope` (built-app context) or the call throws before any request. ```ts listThreads(credentials, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required, since 0.2.2 | The authenticated caller's credentials. | | `config` | `FlowstackClientConfig` | required, since 0.2.2 | Client config. Must include `appScope`; `baseUrl`/`tenantId` as needed. | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `{ ok, data: { threads, count }, error }`. ACL'd server-side to the caller's threads. | ### Examples **List threads** ```ts const res = await listThreads(credentials, { appScope }); if (res.ok) console.log(res.data.threads); ``` > ⚠️ **Warning:** Throws `Private messaging requires an app scope` if `config.appScope` is missing. **See also:** [useThreads](https://flowstack.fun/docs/useThreads) · [listMessages](https://flowstack.fun/docs/listMessages) · [DmThread](https://flowstack.fun/docs/DmThread) *Source: README.md#private-messaging-dms* ## markMessageRead *utility · since 0.2.2* — Client function: mark a received DM as read (recipient only). ```ts import { markMessageRead } from 'flowstack-sdk' ``` Non-React client function. Marks the message `messageId` as read. Only the recipient may mark a message read; the backend rejects others. Sets `read_at` on the message and decrements the thread's `unread_count`. `config` must carry an `appScope`. ```ts markMessageRead(credentials, messageId, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required, since 0.2.2 | The recipient's credentials. | | `messageId` | `string` | required, since 0.2.2 | The `message_id` of the received message to mark read. | | `config` | `FlowstackClientConfig` | required, since 0.2.2 | Client config. Must include `appScope`. | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `{ ok, data: { message_id, read }, error }`. | ### Examples **Mark read** ```ts await markMessageRead(credentials, msg.message_id, { appScope }); ``` > ℹ️ **Note:** Recipient-only. The sender cannot mark their own outgoing message read. **See also:** [useMessages](https://flowstack.fun/docs/useMessages) · [listMessages](https://flowstack.fun/docs/listMessages) · [DmMessage](https://flowstack.fun/docs/DmMessage) *Source: README.md#private-messaging-dms* ## openThread *utility · since 0.2.2* — Client function: record the caller's consent to open a DM thread with another user. ```ts import { openThread } from 'flowstack-sdk' ``` Non-React client function backing `useThreads().openThread`. Records the caller's consent to open a thread with `withUserKey`. The thread becomes `open` (sendable) only once BOTH parties have consented; until then it is `pending`. Idempotent per caller. `config` must carry an `appScope`, and the caller's credentials must include a `userId`. ```ts openThread(credentials, withUserKey, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required, since 0.2.2 | The authenticated caller's credentials (must include `userId`). | | `withUserKey` | `string` | required, since 0.2.2 | The counterpart's user key. | | `config` | `FlowstackClientConfig` | required, since 0.2.2 | Client config. Must include `appScope`. | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `{ ok, data: { pair_key, status: 'pending' | 'open' }, error }`. | ### Examples **Open a thread** ```ts const res = await openThread(credentials, otherUserKey, { appScope }); if (res.ok) console.log(res.data.status); // 'pending' until the other party also consents ``` > ℹ️ **Note:** The thread key is derived as `dmPairKey(me, withUserKey)` — deterministic and order-independent. **See also:** [useThreads](https://flowstack.fun/docs/useThreads) · [sendMessage](https://flowstack.fun/docs/sendMessage) · [dmPairKey](https://flowstack.fun/docs/dmPairKey) *Source: README.md#private-messaging-dms* ## purchaseDomain *utility · since 0.3.1 · ⚠ not for built apps* — Client function: buy a domain through Casino — returns a Stripe Checkout URL; registration + site auto-connect fire from the payment webhook (P0-163). ```ts import { purchaseDomain } from 'flowstack-sdk' ``` Non-React client function. Starts a domain purchase through Casino's registrar: `POST /api/v1/registrar/purchase` with a `PurchaseDomainRequest` returns a Stripe Checkout `session_url` — redirect the builder there to pay. Route 53 registration and (if `site_id` was set) automatic `connectDomain`-style wiring happen asynchronously from the payment webhook, NOT in this call. Use `searchDomains` first to confirm availability and price. ```ts purchaseDomain(credentials, request, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The builder's credentials (owner-scoped JWT). | | `request` | `PurchaseDomainRequest` | required | Domain (name without TLD), TLD, `RegistrantContact`, and options (`auto_renew`, `whois_privacy`, `site_id` for auto-connect). | | `config` | `FlowstackClientConfig` | — | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | The Stripe Checkout URL to redirect the builder to, plus the quoted first-year price. | ### Examples **Buy a domain and auto-connect it to a site** ```ts const res = await purchaseDomain(credentials, { domain: 'thezengarden', tld: 'com', registrant: { first_name: 'Ada', last_name: 'Lovelace', email: 'ada@example.com', phone: '+12125551234', address_line1: '1 Main St', city: 'New York', state: 'NY', zip_code: '10001', country_code: 'US', }, auto_renew: true, whois_privacy: true, site_id: siteId, }); if (res.data?.session_url) window.location.href = res.data.session_url; ``` > ⚠️ **Warning:** The domain is NOT owned when this call returns — payment happens in the browser at `session_url`, and Route 53 registration + site auto-connect fire from the payment webhook afterwards. Poll `listDomains` / `getDomainStatus` to observe completion; do not treat a 200 here as a completed purchase. > ⚠️ **Warning:** Owner/management operation — call it with the **builder's** credentials from a dashboard/builder context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. > ℹ️ **Note:** Registrant contact fields are registrar-validated: `phone` must be E.164 (`+12125551234`), `country_code` a 2-char ISO code, `state` a 2-char code for US addresses. Invalid contact data fails the registration after payment — get these right up front. **See also:** [searchDomains](https://flowstack.fun/docs/searchDomains) · [PurchaseDomainRequest](https://flowstack.fun/docs/PurchaseDomainRequest) · [RegistrantContact](https://flowstack.fun/docs/RegistrantContact) · [listDomains](https://flowstack.fun/docs/listDomains) · [getDomainStatus](https://flowstack.fun/docs/getDomainStatus) *Source: src/api/client.ts (Domain Registrar, P0-163)* ## renameSite *utility · since 0.2.7 · ⚠ not for built apps* — Client function: rename a site (display name in the manifest + dashboard metadata). ```ts import { renameSite } from 'flowstack-sdk' ``` Non-React client function. Updates a built app's display name. `PATCH /api/v1/sites/{siteId}` with `{ name }`. ```ts renameSite(credentials, siteId, name, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `name` | `string` | required | The new display name. | | `config` | `FlowstackClientConfig` | since 0.2.7 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `{ ok, data: { site_id, name }, error }`. | ### Examples **Rename a site** ```ts const res = await renameSite(credentials, siteId, 'My CRM', { appScope }); if (!res.ok) console.error(res.error); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [getAppSource](https://flowstack.fun/docs/getAppSource) · [useSiteVersions](https://flowstack.fun/docs/useSiteVersions) *Source: src/api/client.ts (Site Ops, P0-156)* ## searchDomains *utility · since 0.3.1 · ⚠ not for built apps* — Client function: check domain availability + Casino retail pricing across TLDs (P0-163). ```ts import { searchDomains } from 'flowstack-sdk' ``` Non-React client function. Checks availability and Casino retail pricing (markup included) for a name across one or more TLDs. `GET /api/v1/registrar/check?name={name}&tlds={tlds}`. This is the search half of the registrar buy plane — pair it with `purchaseDomain` to complete a purchase, or with `connectDomain` if the user already owns the domain elsewhere. ```ts searchDomains(credentials, name, tlds?, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The builder's credentials (owner-scoped JWT). | | `name` | `string` | required | The name to check, without TLD — e.g. `thezengarden`. | | `tlds` | `string` | — | Comma-separated TLDs to check, e.g. `com,dev,app`. Omit for the default set. | | `config` | `FlowstackClientConfig` | — | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | One `DomainSearchResult` per TLD checked. `degraded: true` means every lookup failed (registrar outage). | ### Examples **Check a name across TLDs** ```ts const res = await searchDomains(credentials, 'thezengarden', 'com,dev,app'); if (res.data?.degraded) { showError('Domain search is temporarily unavailable — try again shortly.'); } else { for (const r of res.data.results) { if (r.available) console.log(`${r.domain} — $${r.price_usd}/yr`); } } ``` > ⚠️ **Warning:** `degraded: true` means every lookup failed (registrar outage) — show an error state, NOT "taken". Per-TLD, `available: null` means that single lookup failed; only `available: false` means the domain is actually taken. > ⚠️ **Warning:** Owner/management operation — call it with the **builder's** credentials from a dashboard/builder context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [purchaseDomain](https://flowstack.fun/docs/purchaseDomain) · [DomainSearchResult](https://flowstack.fun/docs/DomainSearchResult) · [connectDomain](https://flowstack.fun/docs/connectDomain) · [listDomains](https://flowstack.fun/docs/listDomains) *Source: src/api/client.ts (Domain Registrar, P0-163)* ## sendMessage *utility · since 0.2.2* — Client function: send a private message. 403 unless the thread is mutually open. ```ts import { sendMessage } from 'flowstack-sdk' ``` Non-React client function backing `useMessages().send`. Sends `body` to `toUserKey`. The backend pins `from` to the caller's JWT identity and returns 403 unless the thread is mutually consented (open). `config` must carry an `appScope`. ```ts sendMessage(credentials, toUserKey, body, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required, since 0.2.2 | The authenticated sender's credentials. | | `toUserKey` | `string` | required, since 0.2.2 | The recipient's user key. | | `body` | `string` | required, since 0.2.2 | Message text. Stored and returned verbatim as untrusted input. | | `config` | `FlowstackClientConfig` | required, since 0.2.2 | Client config. Must include `appScope`. | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | `{ ok, data: { message_id, created_at }, error }`. `ok` is false (403) if the thread is not mutually open. | ### Examples **Send a message** ```ts const res = await sendMessage(credentials, toUserKey, 'hey there', { appScope }); if (!res.ok) console.error(res.error); // e.g. thread not open ``` > ⚠️ **Warning:** Returns 403 unless BOTH parties have consented via `openThread`. There is no way to message a user who has not opened a thread with you. **See also:** [useMessages](https://flowstack.fun/docs/useMessages) · [openThread](https://flowstack.fun/docs/openThread) · [listMessages](https://flowstack.fun/docs/listMessages) *Source: README.md#private-messaging-dms* ## setDataPlan *utility · since 0.2.7 · ⚠ not for built apps* — Client function: merge collection schemas into the site's data plan (PUT-with-merge). ```ts import { setDataPlan } from 'flowstack-sdk' ``` Non-React client function. Merges collection schemas: new names override, other existing names are preserved. `PUT /api/v1/sites/{siteId}/data-plan` with `{ collections }`. ```ts setDataPlan(credentials, siteId, collections, config?): Promise }>> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `collections` | `Record` | required | Collection-name → schema map to merge into the plan. | | `config` | `FlowstackClientConfig` | since 0.2.7 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise }>>` | The saved collection names and the resulting merged `mongoCollections`. | ### Examples **Merge a collection schema** ```ts await setDataPlan(credentials, siteId, { tasks: { fields: { title: 'string', done: 'boolean' } }, }, { appScope }); ``` > ℹ️ **Note:** Merge semantics: a name present in `collections` overrides that collection; names you omit are left untouched. It never wipes the whole plan. > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [getDataPlan](https://flowstack.fun/docs/getDataPlan) *Source: src/api/client.ts (Site Ops, P0-156)* ## stageFiles *utility · since 0.2.7 · ⚠ not for built apps* — Client function: stage multiple files in one call (batch form of addSiteFile). ```ts import { stageFiles } from 'flowstack-sdk' ``` Non-React client function. Stages a `{ path: content }` map for a site ahead of a build/publish. `POST /api/v1/sites/{siteId}/staged` with `{ files }`. ```ts stageFiles(credentials, siteId, files, config?): Promise> ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `credentials` | `FlowstackCredentials` | required | The app owner's credentials (owner-scoped JWT). | | `siteId` | `string` | required | The site/app id to operate on. | | `files` | `Record` | required | Map of file path → file content to stage. | | `config` | `FlowstackClientConfig` | since 0.2.7 | Client config (baseUrl, appScope, tenantId as needed). | ### Returns | Field | Type | Description | |---|---|---| | — | `Promise>` | Count, paths, and total bytes of what was staged. | ### Examples **Stage two files** ```ts const res = await stageFiles(credentials, siteId, { 'index.html': '

hi

', 'app.js': 'console.log(1)', }, { appScope }); ``` > ⚠️ **Warning:** Owner/management operation — call it with the **app owner's** credentials from a builder/dashboard/server context, not from inside the generated app's end-user runtime. `builtAppSafe` is `false`. **See also:** [listStagedFiles](https://flowstack.fun/docs/listStagedFiles) · [clearStagedFiles](https://flowstack.fun/docs/clearStagedFiles) · [getAppSource](https://flowstack.fun/docs/getAppSource) *Source: src/api/client.ts (Site Ops, P0-156)* ## CustomDomainRecord *type · since 0.2.8 · ⚠ not for built apps* — Type: a connected custom domain and its lifecycle status. ```ts import type { CustomDomainRecord } from 'flowstack-sdk' ``` Returned by `listDomains` and echoed in status calls. Lifecycle `status`: `validating → issued → activating → live` (or `failed`). ```ts interface CustomDomainRecord { domain: string; site_id: string; status: 'validating' | 'issued' | 'activating' | 'live' | 'failed'; cert_arn?: string; connected_at?: string; activated_at?: string; last_check?: string; cert_status?: string } ``` ### Returns | Field | Type | Description | |---|---|---| | `domain` | `string` | The custom domain. | | `status` | `'validating' | 'issued' | 'activating' | 'live' | 'failed'` | Lifecycle stage. `cert_status` carries the ACM detail when `failed`. | | `cert_arn` | `string?` | ACM certificate ARN once requested. | **See also:** [connectDomain](https://flowstack.fun/docs/connectDomain) · [listDomains](https://flowstack.fun/docs/listDomains) · [getDomainStatus](https://flowstack.fun/docs/getDomainStatus) *Source: src/types/index.ts (Custom Domains, P0-158)* ## DmMessage *type · since 0.2.2* — A single private message within a DM thread. ```ts import type { DmMessage } from 'flowstack-sdk' ``` The shape returned by `useMessages`/`listMessages`. The `body` field is UNTRUSTED user input. ```ts interface DmMessage { message_id: string; from: string; to: string; body: string; created_at: string; read_at: string | null } ``` ### Returns | Field | Type | Description | |---|---|---| | `message_id` | `string` | Stable message id. | | `from` | `string` | Sender's user key (pinned by the backend to the sender's identity). | | `to` | `string` | Recipient's user key. | | `body` | `string` | Message text. UNTRUSTED — render as text / sanitized markdown, never raw HTML. | | `created_at` | `string` | ISO timestamp the message was sent. | | `read_at` | `string | null` | ISO timestamp the recipient read it, or null if unread. | > 🚫 **Critical:** `body` is untrusted user input. Never render as raw HTML; if passed to an agent, treat as data, not instructions. **See also:** [useMessages](https://flowstack.fun/docs/useMessages) · [listMessages](https://flowstack.fun/docs/listMessages) · [sendMessage](https://flowstack.fun/docs/sendMessage) · [DmThread](https://flowstack.fun/docs/DmThread) *Source: README.md#private-messaging-dms* ## DmThread *type · since 0.2.2* — A private message thread between the caller and one counterpart. ```ts import type { DmThread } from 'flowstack-sdk' ``` The shape returned by `useThreads`/`listThreads`. The backend only ever returns threads the caller participates in. ```ts interface DmThread { pair_key: string; with_user_key: string; status: 'pending' | 'open'; last_message: DmMessage | null; unread_count: number; updated_at: string | null } ``` ### Returns | Field | Type | Description | |---|---|---| | `pair_key` | `string` | Deterministic, order-independent thread key (see `dmPairKey`). | | `with_user_key` | `string` | The counterpart's user key. | | `status` | `'pending' | 'open'` | `open` once both parties have consented; only then are sends allowed. | | `last_message` | `DmMessage | null` | The most recent message, or null for an empty thread. | | `unread_count` | `number` | Count of unread messages for the caller. | | `updated_at` | `string | null` | ISO timestamp of the last activity, or null. | **See also:** [useThreads](https://flowstack.fun/docs/useThreads) · [listThreads](https://flowstack.fun/docs/listThreads) · [DmMessage](https://flowstack.fun/docs/DmMessage) · [dmPairKey](https://flowstack.fun/docs/dmPairKey) *Source: README.md#private-messaging-dms* ## DomainSearchResult *type · since 0.3.1 · ⚠ not for built apps* — One TLD's availability + Casino retail price from searchDomains (P0-163). ```ts import type { DomainSearchResult } from 'flowstack-sdk' ``` A single row of a `searchDomains` response: whether `{name}.{tld}` is available and what Casino charges per year. `available` is tri-state — `null` means the lookup for this TLD failed (transient), which is NOT the same as taken. ```ts interface DomainSearchResult ``` ### Returns | Field | Type | Description | |---|---|---| | `domain` | `string` | The full domain checked, e.g. `thezengarden.com`. | | `tld` | `string` | The TLD for this row, e.g. `com`. | | `available` | `boolean | null` | `true` = purchasable, `false` = taken, `null` = the lookup for this TLD failed — retry or show an unknown state, never "taken". | | `price_usd` | `number | null` | Casino retail price per year (markup included); `null` when unknown. | | `aws_cost_usd` | `number | null?` | Underlying Route 53 cost per year, when surfaced. | > ⚠️ **Warning:** Treat `available: null` as "unknown", not "taken" — rendering failed lookups as taken silently loses sales during registrar blips. **See also:** [searchDomains](https://flowstack.fun/docs/searchDomains) · [purchaseDomain](https://flowstack.fun/docs/purchaseDomain) · [PurchaseDomainRequest](https://flowstack.fun/docs/PurchaseDomainRequest) *Source: src/types/index.ts (Domain Registrar, P0-163)* ## PurchaseDomainRequest *type · since 0.3.1 · ⚠ not for built apps* — Request body for purchaseDomain: name + TLD + registrant contact + auto-renew/privacy/auto-connect options (P0-163). ```ts import type { PurchaseDomainRequest } from 'flowstack-sdk' ``` What `purchaseDomain` sends to `POST /api/v1/registrar/purchase`. Setting `site_id` makes the purchased domain auto-connect to that site after the payment webhook completes registration — the buy-and-wire path; omit it to just own the domain. ```ts interface PurchaseDomainRequest ``` ### Returns | Field | Type | Description | |---|---|---| | `domain` | `string` | Name WITHOUT the TLD, e.g. `thezengarden` (not `thezengarden.com`). | | `tld` | `string` | The TLD to register, e.g. `com`. | | `registrant` | `RegistrantContact` | WHOIS contact — registrar-validated formats (E.164 phone, ISO country code). | | `auto_renew` | `boolean?` | Renew automatically each year. | | `whois_privacy` | `boolean?` | Enable WHOIS privacy protection. | | `site_id` | `string?` | If set, the domain auto-connects to this site after payment — no separate `connectDomain` call needed. | > ℹ️ **Note:** `domain` is the bare name — passing `thezengarden.com` as `domain` with `tld: "com"` would attempt `thezengarden.com.com`. **See also:** [purchaseDomain](https://flowstack.fun/docs/purchaseDomain) · [RegistrantContact](https://flowstack.fun/docs/RegistrantContact) · [DomainSearchResult](https://flowstack.fun/docs/DomainSearchResult) · [searchDomains](https://flowstack.fun/docs/searchDomains) *Source: src/types/index.ts (Domain Registrar, P0-163)* ## RegistrantContact *type · since 0.3.1 · ⚠ not for built apps* — WHOIS registrant contact for a domain purchase — registrar-validated formats (P0-163). ```ts import type { RegistrantContact } from 'flowstack-sdk' ``` The registrant block inside `PurchaseDomainRequest`. These fields go to the registrar (Route 53) as the domain's WHOIS contact, so formats are strictly validated — bad data fails the registration step after payment. ```ts interface RegistrantContact ``` ### Returns | Field | Type | Description | |---|---|---| | `first_name` | `string` | Registrant first name. | | `last_name` | `string` | Registrant last name. | | `email` | `string` | Contact email — receives registrar verification mail; must be real. | | `phone` | `string` | E.164 format, e.g. `+12125551234`. | | `address_line1` | `string` | Street address. | | `city` | `string` | City. | | `state` | `string` | 2-char code for US addresses (e.g. `NY`); full name otherwise. | | `zip_code` | `string` | Postal code. | | `country_code` | `string` | 2-char ISO 3166 code, e.g. `US`. | > ⚠️ **Warning:** Validation failures here surface AFTER payment (registration fires from the webhook) — validate `phone` (E.164) and `country_code` (ISO 3166 alpha-2) client-side before calling `purchaseDomain`. **See also:** [PurchaseDomainRequest](https://flowstack.fun/docs/PurchaseDomainRequest) · [purchaseDomain](https://flowstack.fun/docs/purchaseDomain) · [searchDomains](https://flowstack.fun/docs/searchDomains) *Source: src/types/index.ts (Domain Registrar, P0-163)* --- # Authentication Login, sessions, brokered login, and route guards. ## useAuth *hook · since 0.1.0 · ⚠ not for built apps* — Read and mutate Casino-platform auth state (login, register, Google sign-in, logout, token refresh). ```ts import { useAuth } from 'flowstack-sdk' ``` `useAuth()` is for Casino platform internals. Built apps must NOT use it — they authenticate via `BrokeredLoginButton` and read auth state from `useFlowstack()` instead. Calling `login(email, password)` from a built app always fails because end-users don't have Casino credentials. ```ts const { user, credentials, isAuthenticated, isLoading, error, login, register, googleSignIn, logout, refreshToken } = useAuth() ``` ### Returns | Field | Type | Description | |---|---|---| | `user` | `User | null` | The authenticated user, or null. | | `credentials` | `FlowstackCredentials | null` | Active session credentials. | | `isAuthenticated` | `boolean` | Whether a session is active. | | `isLoading` | `boolean` | Auth request in flight. | | `error` | `string | null` | Last auth error message. | | `login` | `(email: string, password: string) => Promise` | Casino platform only — password login. | | `register` | `(email: string, password: string, name?: string) => Promise` | Casino platform only — create an account. | | `googleSignIn` | `() => Promise` | Casino platform only — Google OAuth. | | `logout` | `() => void` | Clear the session. | | `refreshToken` | `() => Promise` | Refresh the access token. | ### Examples **Built apps — read auth state instead** ```tsx const { isAuthenticated, isInitialized, credentials } = useFlowstack(); ``` > ⚠️ **Warning:** **Built apps: do not use this hook.** Read auth state from `useFlowstack()` (`isAuthenticated`, `isInitialized`, `credentials`) and authenticate via `BrokeredLoginButton`. **See also:** [BrokeredLoginButton](https://flowstack.fun/docs/BrokeredLoginButton) · [useFlowstackStatus](https://flowstack.fun/docs/useFlowstackStatus) · [AuthGuard](https://flowstack.fun/docs/AuthGuard) *Source: README.md#useauth* ## useAuthGuard *hook* — Programmatic route protection that reports whether access is allowed and where to redirect. ```ts import { useAuthGuard } from 'flowstack-sdk' ``` Programmatic route protection. Pass requirements (`requireAuth`, `requireWorkspace`, `redirectTo`) and read back whether the current user is allowed and, if not, where to send them. ```ts const { isAllowed, isLoading, shouldRedirect, redirectTo } = useAuthGuard(options) ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `options.requireAuth` | `boolean` | — | Require an authenticated session. | | `options.requireWorkspace` | `boolean` | — | Require a selected workspace. | | `options.redirectTo` | `string` | — | Path to redirect to when access is denied, e.g. `'/login'`. | ### Returns | Field | Type | Description | |---|---|---| | `isAllowed` | `boolean` | Whether access is permitted. | | `isLoading` | `boolean` | Guard check in flight. | | `shouldRedirect` | `boolean` | Whether the caller should redirect. | | `redirectTo` | `string | undefined` | Where to redirect when denied. | ### Examples **Protect a route** ```tsx const { isAllowed, // boolean isLoading, // boolean shouldRedirect, // boolean redirectTo, // string | undefined } = useAuthGuard({ requireAuth: true, requireWorkspace: true, redirectTo: '/login', }); ``` **See also:** [useAuth](https://flowstack.fun/docs/useAuth) · [AuthGuard](https://flowstack.fun/docs/AuthGuard) *Source: README.md#useauthguard* ## AuthGuard *component* — Wrapper that renders children only when authenticated — showing a login fallback, or transparently issuing a guest session for opted-in built apps. ```ts import { AuthGuard } from 'flowstack-sdk' ``` `AuthGuard` gates its children behind authentication. When the user is not signed in it renders the `fallback` — typically a login screen containing `BrokeredLoginButton`, which it can show automatically. **Guest chat (built apps).** When the app has an `appScope` and the site enabled guest chat server-side (`app_config.allowGuestChat`), an unauthenticated visitor is transparently issued a short-lived guest session (`POST /auth/guest`) instead of seeing the login gate — removing the "sign up first" friction. Control is the per-site backend flag: if the site hasn't opted in, `/auth/guest` returns 403 and `AuthGuard` falls back to the normal login UI. It's a no-op unless the app is built with an `appScope` (the Casino dashboard has none, so it always requires real login). Opt out per-guard with `allowGuest={false}`. **Never use `redirectTo`.** Use `fallback={}` instead — `redirectTo="/"` kicks users out before they can sign in. ```ts }>{children} ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `fallback` | `React.ReactNode` | — | Rendered when the user is not authenticated — use a login screen with `BrokeredLoginButton`. | | `requireWorkspace` | `boolean` | — | Require an active workspace, not just authentication. | | `allowGuest` | `boolean` | default `true` | Opt-in guest chat for built apps. When the app has an `appScope` and the site enabled `allowGuestChat`, mints a guest session via `POST /auth/guest` instead of showing the login gate; falls back to login on 403. No-op without an `appScope`. Set `false` to always require login. | | `children` | `React.ReactNode` | required | Protected content rendered only when authenticated. | ### Examples **Guard with a login fallback** ```tsx import { AuthGuard } from 'flowstack-sdk'; export default function App() { return ( }> ); } // LoginScreen shows BrokeredLoginButton function LoginScreen() { return (
); } ``` > 🚫 **Critical:** **Never use `redirectTo`.** Use `fallback={}` instead — `redirectTo="/"` kicks users out before they can sign in. > ℹ️ **Note:** Guest chat (`allowGuest`, default true) is gated entirely by the site's server-side `allowGuestChat` flag and only applies to apps built with an `appScope`. It calls `POST /auth/guest`; a 403 means the site hasn't opted in and the login UI is shown instead. **See also:** [BrokeredLoginButton](https://flowstack.fun/docs/BrokeredLoginButton) · [useFlowstack](https://flowstack.fun/docs/useFlowstack) · [useAuthGuard](https://flowstack.fun/docs/useAuthGuard) · [useAuth](https://flowstack.fun/docs/useAuth) *Source: README.md#authguard--never-use-redirectto* ## BrokeredLoginButton *component* — The only authentication method for built apps — a button that opens Casino's auth popup where Privy runs and posts a scoped JWT back to the app. ```ts import { BrokeredLoginButton } from 'flowstack-sdk' ``` Built apps authenticate **only** via `BrokeredLoginButton` — not email/password, not Google sign-in, not the wallet module. The broker opens a Casino popup that handles Privy login on Casino's origin (the only origin registered with Privy). Privy creates an embedded wallet for the user, the popup closes, and it postMessages a scoped JWT back to your app. No Privy app ID, no `privyConfig`, no wagmi/viem peer deps are needed here. After auth, read identity from `useFlowstack()` (`credentials`) — not `useAuth()`. **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, so `useAuth().login(email, password)` always returns an error for built-app end-users. ```ts ``` ### Parameters | Param | Type | Notes | Description | |---|---|---|---| | `label` | `string` | — | Button text, e.g. `"Continue with Flowstack"`. | ### Examples **Login page** ```tsx import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk'; function LoginPage() { // No email, no password, no Privy appId needed here return (

Welcome

); } ``` > ⚠️ **Warning:** Built apps must use `BrokeredLoginButton` — **not** `useAuth().login()`, **not** `LoginButton` from `flowstack-sdk/wallet`, and **not** `privyConfig.appId`. End-users don't have Casino email/password accounts. > ℹ️ **Note:** After auth, read identity from `useFlowstack()` (`credentials`) — not `useAuth()`. > ℹ️ **Note:** **Account switching:** after `logout()`, the next `BrokeredLoginButton` click passes `force_login=1`, so the broker purges its sticky Privy/Casino session and the user can sign in with a *different* account. Without this, brokered re-login silently returns the same identity. **See also:** [useFlowstack](https://flowstack.fun/docs/useFlowstack) · [AuthGuard](https://flowstack.fun/docs/AuthGuard) · [useAuth](https://flowstack.fun/docs/useAuth) · [FlowstackProvider](https://flowstack.fun/docs/FlowstackProvider) *Source: README.md#2-add-authentication* --- # Data MongoDB-backed collections, public submissions, datasets, and queries. ## Agent-Driven Data Models — Reason → Write → Read *guide · since 0.2.1* — The core pattern for using an agent as a worker: the app reads context and passes it in the prompt, the agent reasons and writes a structured document, and the app reads the result back via useCollection. This is the core pattern for using an agent as a worker, not just a chatbox — the proven way to get a data model that works on the first try (validated end-to-end by the Caveat app). **The mental model:** 1. The **app reads** whatever context the agent needs (via `useCollection`) and hands it to the agent **inside the prompt**. 2. The **agent reasons**, then **writes its result as a document** to a MongoDB collection using its data tools (`insert_documents` / `update_documents`). The agent is the writer; its job is to persist a structured doc, not to "reply." 3. The **app reads the result back** via `useCollection` (reactively). **Never** parse the agent's chat text or `messages[]` for data — read the persisted document. **MongoDB is schemaless — lean on it.** A collection imposes no fixed columns, so the agent can write **any shape**: nested objects, arrays of objects, ragged/optional fields, mixed types, per-row metadata. You do **not** need a migration, a fixed schema, or matching columns up front. Tell the agent the exact JSON shape you want in the prompt and it will write exactly that — the document *is* the schema, and it can evolve per write. **Three steps in practice:** 1. **Read context in the app and pass it to the agent** (don't make the agent query for it). Reading a shared pool via `useCollection({ layer: 'shared' })` is reliable; hand the data to the agent as JSON in the prompt. Strip PII (email/phone) before handing rows to the agent. 2. **Build a deterministic prompt that names the EXACT document shape to write.** Keep prompt construction in a small builder function so it's testable and the shape is explicit. Tell the agent which collection to write and the precise JSON — it will follow it verbatim. 3. **Run the agent as a one-shot task and read the result back via `useCollection`.** This is NOT a chat — it's a single `query()` whose side effect is a written document. Use `capabilities: ['data_access']` to grant the data tools, and a `sessionKey` so this task's conversation stays isolated. With `refreshOnAgentComplete: true`, the result collection re-fetches automatically when the agent's write tool completes — no manual `refresh()` needed. **Why write-to-collection instead of reading `toolCalls[].result`?** As of SDK 0.2.1, `toolCalls[].result` does carry the tool's structured return (and `toolCalls[].status` is `'error'` when a call fails), so it's fine for ephemeral/inline results. But the **durable** pattern is to have the agent persist a document and read it via `useCollection`: the result survives refreshes, is reactive across components, and is queryable later. ### Examples **1. Read context in the app, PII-safe projection** ```tsx // Read the shared pool the agent will reason over. const pool = useCollection('records', { layer: 'shared', limit: 200 }); // Read the current user's OWN row with a dedicated 1-doc query — do NOT find() it // inside the pool. The pool can settle a beat later, so `pool.documents.find(...)` // reads as "missing" on first paint even when the row exists (a real, hard-won bug). // A filtered limit:1 query resolves it independently of the pool load. const mine = useCollection('records', { layer: 'shared', filter: { owner_key: myKey }, limit: 1, refreshOnAgentComplete: true, }); const self = mine.documents[0] ?? null; const others = pool.documents.filter(r => r.owner_key !== myKey); // PII-safe projection before the agent ever sees a row. const toSafe = (r: Record) => ({ owner_key: r.owner_key, title: r.title, tags: r.tags, // NO email, NO phone }); ``` **2. Deterministic prompt naming the exact document shape** ```tsx function buildPrompt(self, others, nowIso: string) { return [ `TASK — produce a result for owner_key "${self.owner_key}".`, 'Use the data provided below; do NOT query the database — it is all here.', '', 'SUBJECT:', JSON.stringify(self, null, 2), `CONTEXT (${others.length}):`, JSON.stringify(others, null, 2), '', 'Steps:', '1. Reason over the inputs.', '2. WRITE exactly one document to the "results" collection with insert_documents.', ` Use this exact run_at (do NOT call a time tool): "${nowIso}". The document MUST be:`, ' { "owner_key": "...", "run_at": "...", "status": "fresh",', ' "items": [{ "ref", "score", "reasons": [] }] }', '3. ALWAYS write the doc (even if items is empty). Never include email/phone.', 'Then reply with one short sentence summarizing the result.', ].join('\n'); } ``` **3. One-shot query, read result back reactively** ```tsx const agent = useAgent('custom', { capabilities: ['data_access'], sessionKey: 'worker' }); // The result collection — reads back what the agent writes. const results = useCollection('results', { layer: 'user', // per-user results; no owner_key filter needed (partition is scoped) sort: { run_at: -1 }, limit: 5, refreshOnAgentComplete: true, // auto-refresh when the agent's insert_documents completes }); async function run() { await Promise.all([pool.refresh(), mine.refresh()]); if (!self) return; // the 1-doc query may settle after first paint — gate on it await agent.query(buildPrompt(toSafe(self), others.map(toSafe), new Date().toISOString())); // results.documents[0] now holds the agent-written result (auto-refreshed). Render it directly. } const latest = results.documents[0]; const items = latest?.items ?? []; ``` **4. Upsert from the agent — ensure the row exists before you update it** ```tsx // When an agent ENRICHES a record over a conversation, update_documents alone // silently no-ops if the row doesn't exist yet — the user 'finishes' the chat but // has no saved record (a real, hard-won bug). Tell the agent to UPSERT: insert a // starter row first, then update it as it learns. function enrichPrompt(ownerKey: string) { return [ `ENRICH MODE. owner_key: "${ownerKey}".`, 'FIRST, query the "profiles" collection for this owner_key.', 'If there is NO doc, INSERT one now (insert_documents) as a starter row:', ` { "owner_key": "${ownerKey}", "fields": {}, "created_at": }`, 'THEN, as you learn facts, UPDATE that doc (update_documents, filter by', 'owner_key) and set updated_at. Keep nuanced observations as append-only notes.', ].join('\n'); } // The app reflects the row passively — it never 'completes' on a chat marker. // With refreshOnAgentComplete, the indicator flips once the row lands. const mineP = useCollection('profiles', { layer: 'shared', filter: { owner_key: myKey }, limit: 1, refreshOnAgentComplete: true, }); const ready = !!mineP.documents[0]; ``` > 🚫 **Critical:** **Never** parse the agent's chat text or `messages[]` for data — read the persisted document via `useCollection`. > ℹ️ **Note:** MongoDB is schemaless: the agent can write any document shape (nested objects, arrays, optional/mixed fields) with no migration. The prompt defines the shape. > ℹ️ **Note:** Use `capabilities: ['data_access']` to grant the agent its data tools, a `sessionKey` to isolate the task's conversation, and `refreshOnAgentComplete: true` on the result collection for automatic reactive read-back. > 🚫 **Critical:** Read the **current user's own row** with a dedicated `useCollection({ filter: { owner_key }, limit: 1 })` query — not `pool.documents.find(...)`. The shared pool can settle after first paint, so finding 'self' inside it reads as absent on the first render even when the row exists. > 🚫 **Critical:** When an agent **enriches** a record over a conversation, instruct it to **upsert**: query for the row, `insert_documents` a starter row if missing, then `update_documents` as it learns. A bare `update_documents` no-ops on a missing row — the user finishes the chat with no saved record. **See also:** [useAgent](https://flowstack.fun/docs/useAgent) · [useCollection](https://flowstack.fun/docs/useCollection) · [two-layer-data-model](https://flowstack.fun/docs/two-layer-data-model) *Source: README.md#agent-driven-data-models--reason--write--read* ## Two-layer data model — shared vs per-user *guide* — How useCollection and the agent's data tools resolve a collection to one of two namespaces — a physically isolated per-user database or a single world-readable shared pool — via the layer option. `useCollection` (and the agent's data tools) resolve a collection to one of two namespaces via the `layer` option: - **`layer: 'user'`** (per-user) — each end-user gets their own **physically isolated** MongoDB database (`u_{sha256(user_id)[:16]}`), not just a filtered view. The backend always keys this DB off the *requesting* user, so one user literally cannot read or write another user's partition through any path. Reads are already scoped — do **not** add a `user_id`/key filter. Use for anything truly private: results, drafts, private history (e.g. `matches`). - **`layer: 'shared'`** — one builder database (`b_{...}`, one per app) that **every signed-in user reads** (e.g. a directory, catalog, leaderboard). Writes are restricted: the app cannot `insert()` directly; a collection marked **`agent_writable`** in the data plan lets the app's own agent append (e.g. a registrar writing member profiles), while blocking arbitrary end-user writes. - **`layer: 'auto'`** (default) — resolves from the collection's configured data model. **Security — `shared` is not access-controlled per user.** Rows in a shared collection carry a `_flowstack.user_id` field, but that is a *filterable field, not an enforced read ACL* — any signed-in user can read every row. Treat a shared collection as a public bulletin board at the storage layer; never store data one user must not see from another there. Only the `'user'` layer is private. **Platform gap — no cross-user delivery (today).** Because the per-user DB is always keyed to the requester, you cannot write into *another* user's private partition. Patterns that deliver data privately *to* a specific recipient — direct messages, per-user notifications — are not achievable: the only private store (`'user'`) is unreachable cross-user, and the only cross-user store (`'shared'`) is world-readable. The correct design (deliver into the recipient's per-user partition) needs a cross-user write primitive the platform does not yet expose. Reads of a shared collection via `useCollection({ layer: 'shared' })` always hit the shared pool. Agent-side reads of shared collections are also routed correctly as of P0-132, but the **recommended pattern remains "read in the app, pass into the prompt"** — it's the most reliable and keeps PII out of the agent. **File Upload → MongoDB:** parse a file in the app and `insert()` the rows directly via `useCollection`; all `useCollection` instances for that collection auto-refresh. ### Examples **File upload to MongoDB via useCollection** ```tsx const { insert } = useCollection('transactions'); const handleCSVUpload = async (file: File) => { const text = await file.text(); const rows = Papa.parse(text, { header: true }).data; await insert(rows.map(row => ({ ...row, amount: Number(row.amount), imported_at: new Date().toISOString(), }))); // All useCollection('transactions') instances auto-refresh }; ``` > ℹ️ **Note:** `layer: 'user'` reads are already scoped to the current user — do **not** add a `user_id`/key filter. The partition is a separate physical MongoDB database (`u_{hash}`), always keyed to the requesting user. > 🚫 **Critical:** **`shared` is world-readable.** The `_flowstack.user_id` field on shared rows is a filter, not an access-control boundary — any signed-in user can read every row. Never store private data (messages, PII, secrets) in a `shared` collection, even filtered by user. Only `layer: 'user'` is private. > ⚠️ **Warning:** **No cross-user delivery primitive yet.** You cannot write into another user's per-user partition, so private per-recipient delivery (DMs, targeted notifications) isn't achievable with current layers — the private store is unreachable cross-user and the shared store is world-readable. > ⚠️ **Warning:** The app cannot `insert()` directly into a `shared` collection. Only an agent acting on a collection marked `agent_writable` in the data plan may append to the shared pool; arbitrary end-user writes stay blocked. > ℹ️ **Note:** Recommended pattern remains "read in the app, pass into the prompt" — most reliable and keeps PII out of the agent. **See also:** [useCollection](https://flowstack.fun/docs/useCollection) · [reason-write-read](https://flowstack.fun/docs/reason-write-read) · [useAgent](https://flowstack.fun/docs/useAgent) *Source: README.md#two-layer-data-model--shared-vs-per-user* ## form-write-direct *recipe · since 0.2.8* — Save form submissions and simple CRUD deterministically — no LLM, no tokens, no streaming **Plane:** data_pattern **Steps:** 1. `useCollection` — For plain saves: call insert/update/remove directly — deterministic, instant, free. 2. `useToolInvocation` — Only when you need a SPECIFIC agent tool deterministically (e.g. a registered agent's mongodb_query) — direct tool call, still no LLM. ### Examples **End-to-end example** ```tsx import { useState } from 'react'; import { useCollection } from '@flowstack/sdk'; export function ContactForm() { const [email, setEmail] = useState(''); const [msg, setMsg] = useState(''); const { insert } = useCollection('submissions', { layer: 'shared' }); return (
{ e.preventDefault(); await insert({ email, msg, submitted_at: new Date().toISOString() }); setEmail(''); setMsg(''); }} > setEmail(e.target.value)} placeholder="Email" />