npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

flowstack-sdk

v0.6.0

Published

Complete Backend-as-a-Service SDK for building AI-powered apps with zero backend code

Downloads

3,327

Readme

Flowstack SDK

npm version npm downloads/week npm total downloads license

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.

npm install flowstack-sdk

Documentation

  • 📖 Docs site: https://flowstack.fun/docs — browsable reference for every hook, component, and utility.
  • 🔌 REST & SSE API: https://flowstack.fun/openapi.json (OpenAPI 3.1) · human view: https://flowstack.fun/docs/rest-api
  • 🤖 For LLMs & coding agents: https://flowstack.fun/llms.txt (index) · https://flowstack.fun/llms-full.txt (full corpus)
  • 🧠 Machine-readable: semantic graph · search index · registry JSON

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.


MCP Build Agent — Required Pre-Flight Checklist

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.

File upload rules

  • upload_source requires ALL files in one call. The sandbox is fresh every build — no files persist from previous runs.
  • 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.
  • casino_get_app_source returns {} (empty) after most builds — do NOT rely on it to retrieve previous source. Regenerate from scratch.
  • casino_stage_file (singular) is removed. Use casino_upload_source with all files in a single dict.

Buffer polyfill — FIRST lines of src/main.tsx

import { Buffer } from 'buffer';
if (typeof (globalThis as any).Buffer === 'undefined') { (globalThis as any).Buffer = Buffer; }
// all other imports below

Without this: "Can't find variable: Buffer" crash at runtime. Add "buffer": "^6.0.3" to package.json dependencies.

vite.config.ts — always include

export default defineConfig({ base: '/', plugins: [react()], define: { global: 'globalThis' } });

package.json — always include mermaid and buffer

"mermaid": "^11.4.1", "buffer": "^6.0.3"

Missing mermaid → Rollup dynamic-import error. Missing buffer → Buffer polyfill fails.

Authentication — BrokeredLoginButton only

import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';
// Never use useAuth().login(), LoginButton from flowstack-sdk/wallet, or privyConfig.appId

Styles — inline or upload, never import without uploading

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.

AuthGuard — never use redirectTo

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.

CRITICAL — upload_source creates a DRAFT, not a live update

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:

1. casino_upload_source(site_id, files)          → check status="success", note version number
2. casino_promote_version(site_id, version)       → NOW the site goes live

If you skip step 2, the build succeeded but nobody sees it. There is no automatic promotion.

Simpler alternative: Use casino_build_staged which builds AND goes live in one call (no promote needed):

1. casino_upload_source(...)   ← build + validate as draft
   OR
   casino_build_staged(...)    ← build + go live immediately (preferred for updates)

If build fails, read build_log:

  • "No package.json" → add it to the files dict
  • "Could not resolve './X'" → add the missing file
  • "Cannot find variable: Buffer" → Buffer polyfill missing or not the first lines

Table of Contents

Installation

For generated sites (CDN tarball — no registry auth needed):

{
  "dependencies": {
    "flowstack-sdk": "https://sagecdn.flowstack.fun/sdk/flowstack-sdk-latest.tgz"
  }
}

For local development:

npm install flowstack-sdk
# or with local copy:
# "flowstack-sdk": "file:packages/flowstack-sdk"

Built Apps — Generated Site Pattern

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.

Architecture

useCollection  →  /collections/*  →  MongoDB     (the app reads/writes data — tables, forms, cards)
useAgent       →  /stream          →  LLM Agent   (chat AND one-shot tasks that write results to MongoDB)
  • useCollection is how the app reads and directly writes data (read, insert, update, delete). Direct MongoDB, no agent.
  • 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. MongoDB is schemaless, so the agent can write any document shape (nested objects/arrays, optional fields) with no migration — the prompt defines the shape.

Minimal Built App

import { FlowstackProvider, useFlowstack, BrokeredLoginButton, useCollection, useAgent } from 'flowstack-sdk';

function App() {
  return (
    <FlowstackProvider config={{
      baseUrl: 'https://sage-api.flowstack.fun',
      mode: 'production',
      appScope: '__SITE_ID__',  // Hex site ID — auto-filled by build pipeline
      // No tenantId needed — it's derived from the brokered-login JWT. Only set it
      // if you use usePublicCollection (anonymous data has no token to derive from).
    }}>
      <AuthGate />
    </FlowstackProvider>
  );
}

function AuthGate() {
  const { isAuthenticated, isInitialized } = useFlowstack();
  if (!isInitialized) return <div>Loading...</div>;
  // BrokeredLoginButton opens Casino's auth popup — Privy runs there, not here
  return isAuthenticated ? <MainApp /> : (
    <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100dvh' }}>
      <BrokeredLoginButton label="Continue with Flowstack" />
    </div>
  );
}

function MainApp() {
  const { credentials, setCredentials } = useFlowstack();
  // Data — direct MongoDB
  const { documents, insert, remove } = useCollection('contacts', {
    sort: { created_at: -1 },
    limit: 50,
    refreshOnAgentComplete: true,
  });
  // AI chat — LLM agent (target an agent you registered via casino_create_agent)
  const { query, messages, isStreaming } = useAgent(undefined, {
    targetAgents: ['assistant'],   // name you registered with casino_create_agent
  });

  return (
    <div>
      <header>
        {credentials?.email}
        <button onClick={() => setCredentials(null)}>Sign out</button>
      </header>
      <ContactTable contacts={documents} onDelete={(id) => remove({ _id: id })} />
      <ContactForm onSubmit={(data) => insert({ ...data, created_at: new Date().toISOString() })} />
      <ChatSidebar query={query} messages={messages} isStreaming={isStreaming} />
    </div>
  );
}

Key Rules for Built Apps

  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.
    • 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 (anonymous access has no token) — set tenantId on the provider there, or the hook errors.
  2. No workspace selection. Built apps do NOT use useWorkspace or selectWorkspace. The backend creates workspaces automatically.
  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']
  4. SHORT collection names only. Pass 'contacts', NOT 'site_abc__contacts'. The backend auto-prefixes with the site_id.
  5. useCollection for ALL data — tables, forms, cards, CRUD. Never route data through the agent.
  6. useAgent for AI only — chat sidebar, analysis, reasoning. The agent reads the same MongoDB collections but is never the data layer.

Auth Flow (Built Apps)

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.

import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';

function LoginPage() {
  // No email, no password, no Privy appId needed here
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100dvh', gap: '1rem' }}>
      <h1>Welcome</h1>
      <BrokeredLoginButton label="Continue with Flowstack" />
    </div>
  );
}

// After auth, read identity from useFlowstack() — not useAuth()
function Header() {
  const { credentials, setCredentials } = useFlowstack();
  return (
    <header>
      <span>{credentials?.email}</span>
      <button onClick={() => setCredentials(null)}>Sign out</button>
    </header>
  );
}

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.

Data Operations (useCollection)

// READ — reactive query, auto-refreshes after any mutation
const { documents, isLoading, total } = useCollection<Contact>('contacts', {
  filter: { status: 'active' },
  sort: { created_at: -1 },
  limit: 50,
  refreshOnAgentComplete: true,  // auto-refresh when agent writes via chat
});

// INSERT — all useCollection('contacts') instances auto-refresh
const { insert } = useCollection('contacts');
await insert({ name: 'Jane', email: '[email protected]', created_at: new Date().toISOString() });

// BATCH INSERT — up to 100 documents
await insert([{ name: 'A' }, { name: 'B' }, { name: 'C' }]);

// UPDATE
const { update } = useCollection('contacts');
await update({ _id: docId }, { $set: { status: 'inactive' } });

// DELETE
const { remove } = useCollection('contacts');
await remove({ _id: docId });

AI Chat (useAgent)

import { useAgent } from 'flowstack-sdk';
import ReactMarkdown from 'react-markdown';
import { useState, useEffect, useRef } from 'react';

// CRITICAL: targetAgents must be a plural ARRAY
// Use the name you registered via casino_create_agent for this site
const { query, messages, isStreaming } = useAgent(undefined, {
  targetAgents: ['assistant'],
});
const [savedMessages, setSavedMessages] = useState<any[]>([]);
const messagesEndRef = useRef<HTMLDivElement>(null);

// Restore chat history on mount
useEffect(() => {
  try {
    const stored = sessionStorage.getItem('chat_history');
    if (stored) setSavedMessages(JSON.parse(stored));
  } catch {}
}, []);

// Save after each completed response (cap at 50 messages)
useEffect(() => {
  if (isStreaming || messages.length === 0) return;
  const all = [...savedMessages, ...messages].slice(-50);
  sessionStorage.setItem('chat_history', JSON.stringify(all));
}, [messages, isStreaming]);

// Auto-scroll to newest message
useEffect(() => {
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isStreaming]);

// query() is a STREAMING function — it does NOT return the response text.
await query('Analyze my contacts by company size');

// Render with height-constrained container
<div className="chat-container">       {/* max-height: 80vh; flex column */}
  <div className="chat-messages">       {/* flex: 1; overflow-y: auto; min-height: 0 */}
    {[...savedMessages, ...messages].map(m => (
      <div key={m.id} className={`message ${m.role}`}>
        {m.role === 'assistant'
          ? <ReactMarkdown>{m.content}</ReactMarkdown>
          : <p>{m.content}</p>}
      </div>
    ))}
    <div ref={messagesEndRef} />
  </div>
  <div className="chat-input">          {/* flex-shrink: 0 */}
    <input ... />
    <button>Send</button>
  </div>
</div>

Chat UX Requirements (built apps):

  • Container: max-height: 80vh, flex column
  • Messages: overflow-y: auto, min-height: 0 (required for flex child scrolling)
  • Input: flex-shrink: 0, pinned at bottom
  • Auto-scroll to newest message on [messages, isStreaming] change
  • Load/save chat history via sessionStorage (last 50 messages)
  • Wrap assistant messages in <ReactMarkdown> (package included in template)
  • Markdown links must be clickable (<a> tags with target="_blank")

Markdown CSS for chat messages — add these styles so rendered markdown is readable and interactive:

/* Links must be clickable and visually distinct */
.message a, .message.assistant a { color: var(--primary, #2563eb); text-decoration: underline; cursor: pointer; }
.message a:hover { opacity: 0.8; }
/* Tables */
.message table { width: 100%; border-collapse: collapse; margin: 0.5em 0; font-size: 0.9em; }
.message th, .message td { padding: 0.4rem 0.6rem; border: 1px solid var(--border, #e2e8f0); text-align: left; }
.message th { background: var(--surface-alt, #f8fafc); font-weight: 600; }
/* Code */
.message pre { background: var(--surface-alt, #1e1e1e); color: #e2e8f0; padding: 0.75rem; border-radius: 6px; overflow-x: auto; margin: 0.5em 0; }
.message code { font-family: 'Fira Code', 'Fira Mono', monospace; font-size: 0.85em; }
.message p code { background: var(--surface-alt, #f1f5f9); padding: 0.15em 0.3em; border-radius: 3px; }
/* Lists */
.message ul, .message ol { margin: 0.25em 0; padding-left: 1.5em; }
.message li { margin: 0.15em 0; }
/* Headers */
.message h1, .message h2, .message h3 { margin: 0.5em 0 0.25em; }
/* Blockquotes */
.message blockquote { border-left: 3px solid var(--primary, #2563eb); padding-left: 1em; margin: 0.5em 0; opacity: 0.85; }

ReactMarkdown link config — make links open in new tab:

<ReactMarkdown components={{
  a: ({ href, children }) => <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
}}>{m.content}</ReactMarkdown>

Agent-Driven Data Models — Reason → Write → Read

This is the core pattern for using an agent as a worker, not just a chatbox. It is 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. This is why the pattern "one-shots" a working data model: the document is the schema, and it can evolve per write.

1. Read context in the app and pass it to the agent (don't make the agent query for it). Reading a shared pool in the app 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.

// Read the pool in the app — this reads the shared layer reliably.
const pool = useCollection<Member>('members', { layer: 'shared', limit: 200 });
const requester = pool.documents.find(m => m.member_key === myKey) ?? null;
const candidates = pool.documents.filter(m => m.member_key !== myKey);

// PII-safe projection before the agent ever sees a row.
const toCandidate = (m: Member) => ({
  member_key: m.member_key, name: m.name, age: m.age, bio: m.bio, traits: m.traits,
  // NO email, NO phone
});

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.

function matchmakerPrompt(requester, candidates, nowIso: string) {
  return [
    `TASK — find honest matches for member_key "${requester.member_key}".`,
    'Use the data provided below; do NOT query the database — it is all here.',
    '',
    'REQUESTER:', JSON.stringify(requester, null, 2),
    `CANDIDATES (${candidates.length}):`, JSON.stringify(candidates, null, 2),
    '',
    'Steps:',
    '1. Apply the requester\'s dealbreakers as HARD filters.',
    '2. WRITE exactly one document to the "matches" collection with insert_documents.',
    `   Use this exact run_at (do NOT call a time tool): "${nowIso}". The document MUST be:`,
    '   { "member_key": "...", "run_at": "...", "status": "fresh",',
    '     "proposals": [{ "member_key", "confidence", "reasons": [], "opener" }],',
    '     "passed":    [{ "member_key", "reason" }] }',
    '3. ALWAYS write the doc (even if proposals is empty). Never include email/phone.',
    'Then reply with one short sentence summarizing the result.',
  ].join('\n');
}

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 from any chat surface (see useAgent). With refreshOnAgentComplete: true, the result collection re-fetches automatically when the agent's write tool completes — no manual refresh() needed.

const agent = useAgent('custom', { capabilities: ['data_access'], sessionKey: 'matchmaker' });

// The result collection — reads back what the agent writes.
const matches = useCollection<MatchDoc>('matches', {
  layer: 'user',                 // per-user results; no member_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 findMatches() {
  await pool.refresh();
  await agent.query(matchmakerPrompt(toCandidate(requester), candidates.map(toCandidate),
    new Date().toISOString()));
  // matches.documents[0] now holds the agent-written result (auto-refreshed). Render it directly.
}

const latest = matches.documents[0];
const proposals = latest?.proposals ?? [];

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. Reach for the persisted collection for anything you want to keep.

Two-layer data model — shared vs per-user

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 of the app reads and (when permitted) writes. 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 written to a shared collection get a _flowstack.user_id field, but that is a filterable field, not an enforced read ACL — any signed-in user can read every row in the collection. Treat a shared collection as a public bulletin board at the storage layer. Never put data one user must not see from another (private messages, personal info, secrets) in a shared collection, even with a user_id filter — filtering is a UI convenience, not isolation. Only the per-user ('user') layer is private.

Platform gap: no cross-user delivery (today). Because the per-user DB is always keyed to the requesting user, you cannot write into another user's private partition. So patterns that require delivering data to a specific recipient privately — direct messages, per-user notifications, "send X to user B" — are not achievable with the current primitives: the only private store ('user') is unreachable cross-user, and the only cross-user store ('shared') is world-readable. The correct design (deliver each item 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" (step 1 above) — it's the most reliable and keeps PII out of the agent.

File Upload → MongoDB

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
};

Service Connections (useConnections)

Every built app should include a Settings page where users connect external services. Without connecting, the AI agent cannot access Google Analytics, Drive, Ads, YouTube, Reddit, Strava, or Twitter data.

import { useConnections } from 'flowstack-sdk';

const { connections, connect, disconnect, isLoading } = useConnections();

// Check status
connections.google.connected     // true | false
connections.google.analytics     // true | false (specific service)
connections.google.email         // "[email protected]" if connected
connections.reddit.connected     // true | false
connections.strava.connected     // true | false
connections.twitter.connected    // true | false

// Connect — opens OAuth popup
connect('google', ['analytics', 'drive']);  // specific Google services
connect('google', ['all']);                  // all Google services
connect('reddit');                           // Reddit
connect('strava');                           // Strava
connect('twitter');                          // Twitter/X

// Disconnect
disconnect('google');
disconnect('reddit');

Settings page pattern:

function SettingsPage() {
  const { connections, connect, disconnect } = useConnections();

  const services = [
    { key: 'google', label: 'Google', sublabel: 'Analytics, Ads, Drive, YouTube', services: ['all'] as const },
    { key: 'reddit', label: 'Reddit', sublabel: 'Feed access' },
    { key: 'strava', label: 'Strava', sublabel: 'Activity data' },
    { key: 'twitter', label: 'Twitter / X', sublabel: 'Timeline and bookmarks' },
  ];

  return (
    <div>
      <h2>Connected Services</h2>
      <p>Connect your accounts so the AI assistant can access your data.</p>
      {services.map(({ key, label, sublabel, services: svc }) => {
        const status = connections[key as keyof typeof connections];
        return (
          <div key={key} className="connection-card">
            <div>
              <h3>{label}</h3>
              <p className="muted">{sublabel}</p>
            </div>
            {status.connected
              ? <button onClick={() => disconnect(key as any)}>Disconnect</button>
              : <button onClick={() => connect(key as any, svc as any)}>Connect</button>}
          </div>
        );
      })}
    </div>
  );
}

Include this Settings page in every built app's navigation. The AI chat agent can only access services the user has connected.


Quick Start (Casino Platform)

Note: This section is for the Casino data science platform, not generated apps. If you're building a generated site, see Built Apps above.

1. Wrap your app with the provider

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 (
    <FlowstackProvider config={config}>
      {children}
    </FlowstackProvider>
  );
}

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.

2. Add authentication

Built apps must use BrokeredLoginButton — not useAuth(), not LoginButton from flowstack-sdk/wallet.

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.

import { useFlowstack, BrokeredLoginButton } from 'flowstack-sdk';

export default function App() {
  const { isAuthenticated, isInitialized, credentials, setCredentials } = useFlowstack();

  if (!isInitialized) return <div>Loading…</div>;

  if (!isAuthenticated) {
    return (
      <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100dvh' }}>
        <BrokeredLoginButton label="Continue with Flowstack" />
      </div>
    );
  }

  return (
    <div>
      <p>Welcome, {credentials?.email}</p>
      <button onClick={() => setCredentials(null)}>Sign out</button>
      {/* your app */}
    </div>
  );
}

Or use AuthGuard which shows BrokeredLoginButton automatically when not authenticated:

import { AuthGuard } from 'flowstack-sdk';

export default function App() {
  return (
    <AuthGuard fallback={<LoginScreen />}>
      <Console />
    </AuthGuard>
  );
}

// LoginScreen shows BrokeredLoginButton
function LoginScreen() {
  return (
    <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}>
      <BrokeredLoginButton />
    </div>
  );
}

Guest chat (built apps). When the app is built with an appScope and the site has enabled guest chat server-side (app_config.allowGuestChat), AuthGuard transparently mints a short-lived guest session (POST /auth/guest) for unauthenticated visitors instead of showing the login gate — removing the "sign up first" friction. The opt-in is the per-site backend flag: if the site hasn't enabled it, /auth/guest returns 403 and AuthGuard falls back to the normal login UI. This is a no-op without an appScope (the Casino dashboard has none, so it always requires real login). It's on by default; pass allowGuest={false} on a specific guard to always require login.

<AuthGuard fallback={<LoginScreen />} allowGuest>     {/* default — guest if the site opted in */}
  <Console />
</AuthGuard>

<AuthGuard fallback={<LoginScreen />} allowGuest={false}>  {/* always require real login */}
  <Billing />
</AuthGuard>

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.

Do not use:

  • useAuth() with login(email, password) — Casino end-users don't have email/password accounts
  • LoginButton from flowstack-sdk/wallet — requires WalletProvider/Privy which can't run on built-app subdomains
  • privyConfig.appId in FlowstackProvider — not needed for the broker auth flow
  • Extra peer deps (@privy-io/react-auth, wagmi, viem) — only needed for Casino platform features, not built apps

3. Chat with AI agent

import { useAgent } from 'flowstack-sdk';

function Chat() {
  const { messages, isStreaming, query } = useAgent();

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id} className={msg.role}>
          {msg.content}
          {msg.statusLine && <span className="status">{msg.statusLine}</span>}
        </div>
      ))}
      <button onClick={() => query('Analyze my sales data')} disabled={isStreaming}>
        {isStreaming ? 'Thinking...' : 'Ask'}
      </button>
    </div>
  );
}

Complete Example App (Casino Platform)

Note: This example uses workspace selection which is for the Casino platform only. Generated/built apps should follow the Built Apps pattern instead — no workspaces, use appScope.

A complete, working AI chat application in a single file:

'use client';

import { useState } from 'react';
import {
  FlowstackProvider,
  LoginForm,
  useAuth,
  useWorkspace,
  useAgent,
} from 'flowstack-sdk';

const config = {
  baseUrl: 'https://sage-api.flowstack.fun',
  tenantId: 't_6fe54402be43',
  mode: 'production' as const,
  jwtSecret: 'flowstack-cdn-site',
  passwordSecret: 'flowstack-cdn-site',
};

function ChatApp() {
  const { user, isAuthenticated, login, logout, isLoading: authLoading } = useAuth();
  const { workspaces, selectedWorkspace, selectWorkspace } = useWorkspace();
  const { messages, isStreaming, query, clearMessages } = useAgent();
  const [input, setInput] = useState('');
  const messagesEndRef = useRef<HTMLDivElement>(null);

  // Auto-scroll to newest message during streaming
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages, isStreaming]);

  const handleSend = async () => {
    if (!input.trim() || isStreaming) return;
    const message = input;
    setInput('');
    await query(message);
  };

  if (!isAuthenticated) {
    return (
      <div style={{ padding: 40, maxWidth: 400, margin: '0 auto' }}>
        <h1>AI Chat</h1>
        <LoginForm onSuccess={() => {}} showRegisterLink />
      </div>
    );
  }

  if (!selectedWorkspace && workspaces.length > 0) {
    return (
      <div style={{ padding: 40 }}>
        <h1>Select Workspace</h1>
        {workspaces.map(ws => (
          <button key={ws.workspaceId} onClick={() => selectWorkspace(ws)} style={{ display: 'block', margin: '8px 0' }}>
            {ws.name}
          </button>
        ))}
      </div>
    );
  }

  return (
    <div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
      <header style={{ padding: 16, borderBottom: '1px solid #eee', display: 'flex', justifyContent: 'space-between' }}>
        <strong>{selectedWorkspace?.name || 'AI Chat'}</strong>
        <div>
          <button onClick={clearMessages} style={{ marginRight: 8 }}>Clear</button>
          <button onClick={logout}>Logout</button>
        </div>
      </header>

      <div style={{ flex: 1, overflow: 'auto', padding: 16, minHeight: 0 }}>
        {messages.map(msg => (
          <div key={msg.id} style={{
            marginBottom: 16, padding: 12, borderRadius: 8,
            backgroundColor: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5',
            maxWidth: '80%', marginLeft: msg.role === 'user' ? 'auto' : 0,
          }}>
            <div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
              {msg.role === 'user' ? 'You' : 'Assistant'}
            </div>
            {msg.role === 'assistant'
              ? <ReactMarkdown>{msg.content}</ReactMarkdown>
              : <div style={{ whiteSpace: 'pre-wrap' }}>{msg.content}</div>}
            {msg.statusLine && <div style={{ fontSize: 11, color: '#999', marginTop: 4 }}>{msg.statusLine}</div>}
            {msg.isStreaming && <span style={{ color: '#999' }}>▋</span>}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      <div style={{ padding: 16, borderTop: '1px solid #eee', display: 'flex', gap: 8 }}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()}
          placeholder="Ask a question..."
          disabled={isStreaming}
          style={{ flex: 1, padding: 12, fontSize: 16, borderRadius: 8, border: '1px solid #ddd' }}
        />
        <button onClick={handleSend} disabled={isStreaming || !input.trim()}>
          {isStreaming ? '...' : 'Send'}
        </button>
      </div>
    </div>
  );
}

export default function App() {
  return (
    <FlowstackProvider config={config}>
      <ChatApp />
    </FlowstackProvider>
  );
}

Hooks Reference

useAuth

Built apps: do not use this hook. useAuth() is for Casino platform internals. Built apps authenticate via BrokeredLoginButton — see Add authentication above. Calling login(email, password) from a built app will always fail because end-users don't have Casino credentials.

For built apps, read auth state from useFlowstack() instead:

const { isAuthenticated, isInitialized, credentials } = useFlowstack();

useAuth (Casino platform only):

const {
  user,           // User | null
  credentials,    // FlowstackCredentials | null
  isAuthenticated,// boolean
  isLoading,      // boolean
  error,          // string | null
  login,          // (email, password) => Promise<boolean>  — Casino platform only
  register,       // (email, password, name?) => Promise<boolean>  — Casino platform only
  googleSignIn,   // () => Promise<void>  — Casino platform only
  logout,         // () => void
  refreshToken,   // () => Promise<boolean>
} = useAuth();

useCollection

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.

Collection names are auto-prefixed with the site_id by the backend — pass SHORT names only (e.g., 'transactions', NOT 'site_abc__transactions').

const {
  // Read (reactive — auto-fetches on mount, re-fetches after mutations)
  documents,       // T[]
  count,           // number — documents returned (≤ limit)
  total,           // number — total matching (for pagination)
  isLoading,       // boolean
  error,           // string | null
  refresh,         // () => Promise<void> — manual re-fetch

  // Write (auto-refetches all useCollection instances for this collection after success)
  insert,          // (doc | doc[]) => Promise<{ inserted_ids: string[] }>
  update,          // (filter, update, opts?) => Promise<{ modified_count: number }>
  remove,          // (filter) => Promise<{ deleted_count: number }>
} = useCollection<Transaction>('transactions', {
  filter: { status: 'active' },   // MongoDB query filter
  sort: { date: -1 },             // Sort spec
  limit: 50,                      // Max documents (default 50, max 500)
  skip: 0,                        // Pagination offset
  layer: 'auto',                  // 'user' (per-user) | 'shared' (one pool) | 'auto' (default)
  refreshOnAgentComplete: true,   // Auto-refresh after an agent write tool completes (0.2.1+: fires
                                  // for insert_documents/update_documents/insert_app_data/etc.)
});

layer picks the namespace: 'user' is a private per-user partition (don't filter by a user key — it's already scoped); 'shared' is one pool all users read (app can't insert() directly unless the collection is agent_writable); 'auto' resolves from the collection's data model. See Two-layer data model.

refreshOnAgentComplete: true is the reactive bridge for the Reason→Write→Read pattern — when the agent finishes writing via its data tools, this collection re-fetches automatically. (Before 0.2.1 it only matched legacy tool names and silently no-op'd, so apps called refresh() manually; that workaround is no longer needed.)

Data display — reactive query:

function TransactionTable() {
  const { documents, isLoading } = useCollection<Transaction>('transactions', {
    sort: { date: -1 }, limit: 50, refreshOnAgentComplete: true,
  });
  if (isLoading) return <div>Loading...</div>;
  return (
    <table>
      <tbody>
        {documents.map(row => (
          <tr key={row._id}><td>{row.date}</td><td>${row.amount}</td></tr>
        ))}
      </tbody>
    </table>
  );
}

Form submission — insert document:

function AddTransaction() {
  const { insert } = useCollection('transactions');
  const [form, setForm] = useState({ date: '', amount: '' });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await insert({ ...form, amount: Number(form.amount), created_at: new Date().toISOString() });
    setForm({ date: '', amount: '' });
    // All useCollection('transactions') instances auto-refresh
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

Update and delete:

const { update, remove } = useCollection('transactions');

// Update
await update({ _id: docId }, { $set: { category: 'Food' } });

// Delete
await remove({ _id: docId });

CRITICAL: The app reads all data with useCollection (tables, forms, cards, CRUD) — never parse the agent's chat text or messages[] for data, and never use useToolInvocation. Writes happen one of two ways: (a) deterministic user actions → useCollection().insert/update/remove directly; (b) agent-reasoned results → the agent writes a document via its data_access tools and the app reads it back with useCollection (the Reason → Write → Read pattern). Either way, useCollection is always how the app reads.

usePublicCollection

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.

import { usePublicCollection } from 'flowstack-sdk';

const {
  documents,   // T[]
  count,       // number
  total,       // number
  isLoading,   // boolean
  error,       // string | null
  insert,      // (doc: Partial<T>) => Promise<{ inserted_id: string }>
  refresh,     // () => Promise<void>
} = usePublicCollection<HighScore>('high_scores', {
  filter: { album: 'yeezus' },
  sort: { score: -1 },
  limit: 25,
});

await insert({ album: 'yeezus', name: 'KEON', score: 12500 });

Key differences from useCollection:

  • No credentials needed — works for anonymous visitors
  • Insert only (no update/remove — data is owned by the app, not the user)
  • Rate-limited server-side (default 10 writes/min, 100/day per IP)
  • 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)
  • 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.)

Builder config required in app_config.json:

{
  "publicCollections": {
    "high_scores": {
      "schema": {
        "album": { "type": "string", "required": true, "maxLength": 32 },
        "name":  { "type": "string", "required": true, "maxLength": 24 },
        "score": { "type": "number", "required": true, "min": 0 }
      },
      "rateLimit": { "writesPerMinute": 5, "writesPerDay": 50 }
    }
  }
}

useWorkspace

Workspace management.

const {
  workspaces,        // WorkspaceInfo[]
  selectedWorkspace, // WorkspaceInfo | null
  isLoading,         // boolean
  error,             // string | null
  createWorkspace,   // (name, description?) => Promise<WorkspaceInfo | null>
  selectWorkspace,   // (workspace) => void
  refreshWorkspaces, // () => Promise<void>
} = useWorkspace();

useAgent

AI agent queries with streaming, tool calls, interrupts, status updates, and automatic conversation persistence.

const {
  messages,               // ChatMessage[]
  isStreaming,             // boolean
  isLoading,              // boolean
  toolCalls,              // ToolCall[] — active/completed tool executions
  connectedDataSources,   // DataSourceBadgeInfo[] — connected data sources
  error,                  // string | null
  query,                  // (prompt: string) => Promise<void>
  clearMessages,          // () => void — wipe in-memory messages (same session)
  cancelQuery,            // () => void
  interruptAgent,         // () => Promise<void> — pause agent mid-execution
  respondToInterrupt,     // (response: string) => Promise<void> — resume with user input
} = useAgent(template?, options?);

Template (first arg): 'data-science' | 'marketing' | 'support' | 'custom'. For a persona-backed app (your app's brain is an agent registered via casino_create_agent), the template is just a fallback label — the registered persona overrides it. Use 'custom' (or omit) and select the brain with the persona option below. The template does not pick the agent.

Options:

| Option | Type | What it does | |---|---|---| | 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. | | 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.) | | 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. | | 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. | | tools | string[] | Whitelist specific tool names (finer than capabilities). | | targetAgent(s) | string / string[] | Deprecated (Strands swarm, removed in P0-73). Use persona instead. |

// Persona-backed, multi-surface app: each surface gets its own brain + isolated conversation.
const interview  = useAgent('custom', { capabilities: ['data_access'], persona: 'interviewer', sessionKey: 'interview' });
const matchmaker = useAgent('custom', { capabilities: ['data_access'], persona: 'matchmaker',  sessionKey: 'matchmaker' });

Conversation Memory (Automatic)

useAgent automatically persists a stable session ID for each component mount. The session ID is:

  • Generated on first use via crypto.randomUUID() (with a timestamp fallback for legacy browsers)
  • Stable across turns — every call to query() passes the same session ID to the backend
  • Reused by Strands' S3SessionManager — the agent remembers prior messages, tool calls, and context across turns
  • Reset on clearMessages() — starting a truly new conversation generates a fresh session ID

This means a chat in a built app correctly handles multi-turn queries:

function FinanceChat() {
  const { query, messages, isStreaming } = useAgent(undefined, {
    targetAgents: ['finance_bot']  // agent registered via casino_create_agent
  });

  // Turn 1: "show me last month's expenses"
  // Turn 2: "now break it down by category"  ← agent remembers "last month's expenses"
  // Turn 3: "which one is the biggest?"       ← agent knows the categories from turn 2

  return (
    <ChatUI messages={messages} onSend={query} isStreaming={isStreaming} />
  );
}

Notes:

  • 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).
  • clearMessages() resets the session. Call it to start a fresh conversation. The next query() creates a new backend session.
  • 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.

Advanced: Explicit Session Management

Simplest path (0.2.1+): pass sessionKey. A stable sessionKey per surface gives each useAgent its own conversation and persists that conversation across reloads (same tab) — no localStorage plumbing or remount needed:

const chat = useAgent('custom', { capabilities: ['data_access'], sessionKey: 'support-chat' });
// Reopening/reloading this surface resumes the same conversation; a different sessionKey is a
// different conversation. Call chat.clearMessages() / startNewSession() to start fresh.

For cross-tab or multi-conversation-per-surface persistence, manage the session ID at the application level:

import { useEffect, useState } from 'react';
import { useAgent } from 'flowstack-sdk';

function PersistentChat() {
  // Option 1: Resume the most recent session from localStorage
  const [sessionId] = useState(() => {
    const existing = localStorage.getItem('my-app:chat:session');
    if (existing) return existing;
    const fresh = crypto.randomUUID();
    localStorage.setItem('my-app:chat:session', fresh);
    return fresh;
  });

  // The useAgent hook will create its own sessionIdRef automatically,
  // but you can also invoke the lower-level `executeQueryWithConfig` with
  // an explicit sessionId for full control (see API reference).
  const { query, messages, clearMessages } = useAgent(undefined, {
    targetAgents: ['finance_bot']  // agent registered via casino_create_agent
  });

  const handleNewChat = () => {
    const fresh = crypto.randomUUID();
    localStorage.setItem('my-app:chat:session', fresh);
    clearMessages(); // resets the in-hook sessionIdRef as well
    window.location.reload(); // optional: force remount to pick up new session
  };

  return (
    <>
      <button onClick={handleNewChat}>+ New Chat</button>
      <ChatUI messages={messages} onSend={query} />
    </>
  );
}

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.

When Conversation Memory Breaks

If your built app appears to "forget" every turn:

  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.
  2. Check that you're not remounting the component between every query (e.g., conditional rendering based on isStreaming can cause remounts).
  3. Check for multiple useAgent instances in the same page. Before 0.2.1 they all shared one tenant-wide session, so surfaces bled into each other (a matchmaker reply showing up in an interview). Fix: give each instance a distinct sessionKey (0.2.1+) so conversations stay isolated.
  4. Check the browser console for SDK warnings about crypto.randomUUID not being available (very old browsers).

ChatMessage fields:

| Field | Type | Description | |-------|------|-------------| | id | string | Unique message ID | | role | 'user' \| 'assistant' | Message sender | | content | string | Message text | | isStreaming | boolean | Whether still receiving content | | statusLine | string \| undefined | Live status text during streaming (tool progress, agent status) | | toolCalls | ToolCall[] | Tool executions in this message | | visualizations | VisualizationData[] | Charts/images produced | | searchResults | SearchResultsData[] | Search results |

useDatasets

Dataset operations.

const {
  datasets,        // DatasetInfo[] — { id, name, rows, columns, schema?, columnNames?, createdAt?, updatedAt? }
  isLoading,       // boolean
  error,           // string | null
  uploadDataset,   // (file, name?) => Promise<DatasetInfo | null>
  downloadDataset, // (name) => Promise<Blob | null>
  deleteDataset,   // (name) => Promise<boolean>
  refreshDatasets, // () => Promise<void>
} = useDatasets();

DatasetInfo fields (source of truth: src/types/index.ts):

  • id: string — stable identifier
  • name: string — user-visible name
  • rows: number — row count (NOT rowCount)
  • columns: number — column count (NOT columnCount)
  • schema?: Record<string, ColumnSchema> — optional per-column schema
  • columnNames?: string[] — optional ordered column names
  • createdAt?: string, updatedAt?: string — ISO8601 timestamps

useVisualizations

Visualization management.

const {
  visualizations,       // VisualizationData[] — { name, type, imageUrl, imageBase64, format, createdAt }
  isLoading,            // boolean
  error,                // string | null
  refreshVisualizations,// () => Promise<void>
} = useVisualizations();

useReports

Report management.

const {
  reports,        // ReportInfo[] — { name, content, format, createdAt }
  isLoading,      // boolean
  error,          // string | null
  refreshReports, // () => Promise<void>
} = useReports();

useModels

ML model management.

const {
  models,        // ModelInfo[] — { name, framework, metrics, createdAt }
  isLoading,     // boolean
  error,         // string | null
  refreshModels, // () => Promise<void>
} = useModels();

useSites

Published site management — list, create, stage files, publish to CDN, delete.

const {
  sites,        // PublishedSiteInfo[]
  isLoading,    // boolean
  error,        // string | null
  createSite,   // (params: CreateSiteParams) => Promise<PublishedSiteInfo | null>
  addFile,      // (siteId, path, content) => Promise<boolean>
  publishSite,  // (siteId) => Promise<PublishedSiteInfo | null>
  deleteSite,   // (siteId) => Promise<boolean>
  refreshSites, // () => Promise<void>
} = useSites();

PublishedSiteInfo fields:

| Field | Type | Description | |-------|------|-------------| | 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) |

Two publishing workflows:

// Quick publish — pass files inline
await createSite({
  name: 'My Dashboard',
  siteType: 'on_demand',
  files: { 'index.html': '<html>...</html>', 'styles.css': 'body { ... }' },
});

// Staged publish — add files incrementally, then publish
const site = await createSite({ name: 'My App' });
await addFile(site.id, 'index.html', '<html>...</html>');
await addFile(site.id, 'src/App.tsx', 'export default function App() { ... }');
await publishSite(site.id);

useDataSources

External data source connections.

const {
  dataSources,       // DataSource[]
  isLoading,         // boolean
  error,             // string | null
  createDataSource,  // (config) => Promise<DataSource | null>
  testConnection,    // (sourceId) => Promise<boolean>
  deleteDataSource,  // (sourceId) => Promise<boolean>
  refreshDataSources,// () => Promise<void>
} = useDataSources();

Supports: PostgreSQL, MongoDB, MySQL, Snowflake, BigQuery, S3.

useQuery

Simple one-shot query execution (no streaming).

const {
  executeQuery,  // (prompt: string) => Promise<QueryResult>
  result,        // QueryResult | null
  isLoading,     // boolean
  error,         // string | null
} = useQuery();

useUserManagement

Admin user management.

const {
  users,           // ManagedUser[]
  stats,           // UserStats | null — { totalUsers, activeUsers, newUsersThisMonth }
  isLoading,       // boolean
  error,           // string | null
  canManageUsers,  // boolean
  pagination,      // { page, limit, totalCount, hasMore }
  refreshUsers,    // (params?) => Promise<void>
  getUser,         // (userId) => Promise<ManagedUser | null>
  updateUser,      // (userId, updates) => Promise<boolean>
  suspendUser,     // (userId, reason?) => Promise<boolean>
  reactivateUser,  // (userId) => Promise<boolean>
  deleteUser,      // (userId) => Promise<boolean>
  getUserActivity, // (userId, limit?) => Promise<UserActivityLog[]>
  refreshStats,    // () => Promise<void>
  setPage,         // (page) => void
  setSearch,       // (search) => void
  setRoleFilter,   // (role) => void
  setStatusFilter, // (status) => void
} = useUserManagement();

Role hierarchy: owner > admin > member > viewer

useAuthGuard

Programmatic route protection.

const {
  isAllowed,      // boolean
  isLoading,      // boolean
  shouldRedirect, // boolean
  redirectTo,     // string | undefined
} = useAuthGuard({
  requireAuth: true,
  requireWorkspace: true,
  redirectTo: '/login',
});

useFlowstackStatus

Backend connection monitoring.

const {
  status,          // 'connected' | 'disconnected' | 'checking'
  isConnected,     // boolean
  latency,         // number | null (ms)
  error,           // string | null
  checkConnection, // () => Promise<void>
} = useFlowstackStatus({
  pollInterval: 30000,
  autoPoll: true,
});

Additional Hooks (0.5+)

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.

  • useAgent() — useAgent Hook The main hook for interacting with AI agents.
  • useAgents() — useAgents Hook Fetches the catalog of available agents from the backend.
  • useAuth() — useAuth Hook Provides authentication functionality including login, register, and Google OAuth.
  • useAuthGuard() — useAuthGuard Hook Provides auth guard logic without rendering components.
  • useAutomations() — CRUD for agent cron automations (P0-85).
  • useCollection() — Direct MongoDB access for built-app components.
  • useCollectionExplorer() — Browse, query, export, and delete a specific MongoDB collection.
  • useConnections() — Manage external service connections (Google, Reddit, Strava, Twitter, GitHub).
  • useConversations() — fetches the user's past Casino builder conversations from GET /library/conversations.
  • useDataOverview() — Unified summary of all user-owned data.
  • useDataSources() — useDataSources Hook Provides data source management for connecting to external databases.
  • useDatasets() — useDatasets Hook Provides dataset management functionality including upload, download, and deletion.
  • useFlowstackStatus() — useFlowstackStatus Hook Monitors connection status and health of the Flowstack backend.
  • useIntegrations() — CRUD for HTTP API integrations (P0-79).
  • useIntentAgent() — useIntentAgent Hook Creates and manages agents dynamically based on user intent.
  • useModels() — useModels Hook Provides ML model management functionality.
  • useOllamaDetection() — Detect a local Ollama instance and list available models.
  • useProviderCredentials() — Manage LLM provider credentials (BYOK + Ollama).
  • usePublicCollection() — anonymous public submissions for built apps.
  • useQuery() — useQuery Hook A lower-level hook for executing queries without managing chat history.
  • useReports() — useReports Hook Provides report management functionality.
  • useSiteVersions() — See source for usage.
  • useSites() — useSites Hook Provides published site management — list, create, stage files, publish to CDN, delete.
  • useToolInvocation() — Direct tool invocation hook.
  • useUserCollections() — List all MongoDB collections the user owns.
  • useUserManagement() — useUserManagement Hook Provides user management functionality for developers to track and manage users who sign up through their apps.
  • useVisualizations() — useVisualizations Hook Provides access to workspace visualizations.
  • useWorkspace() — useWorkspace Hook Provides workspace management functionality.

useIntegrations

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.

import { useIntegrations } from 'flowstack-sdk';

const { integrations, create, update, remove, isLoading } = useIntegrations();

// Register Shopify
await create({
  name: 'Shopify',
  description: 'Shopify Admin API for order and product management',
  base_url: 'https://my-store.myshopify.com/admin/api/2024-01',
  auth_type: 'bearer',          // 'bearer' | 'api_key_header' | 'api_key_query' | 'basic' | 'none'
  auth_config: { token: 'shpat_xxx' },
  endpoints: [
    { name: 'list_orders', method: 'GET',  path: '/orders.json' },
    { name: 'get_order',   method: 'GET',  path: '/orders/{id}.json' },
    { name: 'cancel_order',method: 'POST', path: '/orders/{id}/cancel.json' },
  ],
});

// Update credentials
await update(id, { auth_config: { token: 'new_token' } });

// Remove
await remove(id);

Return value:

{
  integrations: Integration[];   // [{integration_id, name, base_url, auth_type, endpoint_count, ...}]
  isLoading: boolean;
  error: string | null;
  create:  (input) => Promise<Integration | null>;
  update:  (id, input) => Promise<boolean>;
  remove:  (id) => Promise<boolean>;
  get:     (id) => Promise<Integration | null>;  // includes full endpoint list
  refresh: () => Promise<void>;
}

useAutomations

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.

Schedule format: 5-field Unix cron "minute hour dom month dow"

import { useAutomations } from 'flowstack-sdk';

const { automations, create, pause, resume, runNow, getRuns } = useAutomations();

// Daily sales digest — weekdays at 9 AM Eastern
await create({
  name: 'Daily sales digest',
  prompt: "Pull yesterday's orders from Shopify, summarize revenue by product and region, flag any anomalies.",
  schedule: '0 9 * * 1-5',
  timezone: 'America/New_York',
  target_agents: ['shopify_analyst'],   // routes to a specific agent persona; omit for default
  output_config: {
    type: 'email',
    to: '[email protected]',
    subject_template: 'Sales digest — {date}',
  },
});

// Run once right now (ignores schedule)
await runNow(automationId);

// Pause / resume
await pause(automationId);
await resume(automationId);

// Get last 10 run results
const runs = await getRuns(automationId, 10);
// [{run_id, status, started_at, duration_ms, credits_used, output_summary, output_url}]

Output config options:

| type | Delivers via | Extra fields | |---|---|---| | "silent" | Stored only (default) | — | | "email" | Email | to, subject_template, format | | "webhook" | HTTP POST | url, headers, format | | "file" | Downloadable URL in output_url | format ("csv" | "json" | "pdf") |

Return value:

{
  automations: Automation[];   // [{automation_id, name, schedule, status, last_run_at, run_count, ...}]
  isLoading: boolean;
  error: string | null;
  create:  (input) => Promise<Automation | null>;
  update:  (id, input) => Promise<boolean>;
  remove:  (id) => Promise<boolean>;
  pause:   (id) => Promise<boolean>;
  resume:  (id) => Promise<boolean>;
  runNow:  (id) => Promise<{ invoked: boolean } | null>;
  getRuns: (id, limit?) => Promise<AutomationRun[]>;
  refresh: () => Promise<void>;
}

Private Messaging (DMs)

Built-app users can exchange private, one-to-one messages. The DM store is server-owned and ACL'd: the backend pins from to the caller's JWT identity, only ever returns threads the caller participates in, and gates sends on a mutually-consented (open) thread — neither party ever learns the other's contact details.

Private messaging is a built-app capability. It requires an appScope on the FlowstackProvider config; in a Casino personal session the backend returns 403. All symbols below ship in 0.2.2+.

Security: 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.

useThreads — list a user's threads

import { useThreads } from 'flowstack-sdk';

con