brainy/docs/guides/framework-integration.md
David Snelling 606445cd61 feat(8.0): API simplification — remove neural()/Db.search, one storage path key, integration→0
8.0 RC cleanup toward "one place per thing, zero-config, no deprecation":

- Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead
  legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})`
  / `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate
  entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor,
  NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept.
- Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams).
  Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` →
  now `find({ query, limit })`.
- Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases
  (`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the
  exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver
  feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native
  storage provider resolves the identical root (no split-brain).
- Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now
  applied as a post-filter on `result.score` (the documented way to bound semantic results).
- Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()`
  and failed dimension validation; they are metadata-only updates now.
- Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination
  shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an
  in-place path change that preserves the blob and the entity id, for files and directories.
- Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability.
  Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk
  flushes and emits `progress.queryable`, so imported data is queryable during the import.
- Sweep all docs, comments, and JSDoc for the removed/changed APIs.

Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
2026-06-20 13:31:11 -07:00

14 KiB

Framework Integration Guide

Brainy is framework-friendly - designed to drop into the server side of any modern JavaScript framework. This guide shows you how to integrate Brainy into framework-based apps.

Runtime: Brainy 8.0 runs on Node.js 22+ and Bun (server-side only). It is not a browser library. In framework apps, run Brainy in API routes, server components, server actions, loaders, or a dedicated backend service - never in client-side bundles.

🎯 Why Server-Side?

Brainy embeds an HNSW vector index, a graph engine, and a filesystem-backed persistence layer. These belong on the server:

  • Zero configuration: Just import { Brainy } from '@soulcraft/brainy'
  • Auto storage detection: new Brainy() auto-selects filesystem persistence on Node
  • Cleaner code: No browser polyfills, no conditional client/server imports
  • Better DX: One instance shared across your server routes

🚀 Quick Start

Install Brainy

npm install @soulcraft/brainy

Basic Integration

import { Brainy } from '@soulcraft/brainy'

// Run on the server (API route, server component, backend service)
// new Brainy() auto-detects filesystem persistence on Node
const brain = new Brainy()
await brain.init()

// Add data
await brain.add({
  data: "Framework integration is awesome!",
  type: "concept",
  metadata: { framework: "any" }
})

// Search
const results = await brain.find("framework integration")

⚛️ React Integration

Brainy runs on the server, so a React client component talks to it through an API endpoint (see the Next.js API route below). The hook fetches results; it never instantiates Brainy in the browser.

Basic Hook Pattern

import { useState, useCallback } from 'react'

function useBrainySearch(endpoint = '/api/search') {
  const [results, setResults] = useState([])
  const [loading, setLoading] = useState(false)

  const search = useCallback(async (query) => {
    if (!query) return
    setLoading(true)
    try {
      const res = await fetch(endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query })
      })
      const { results } = await res.json()
      setResults(results)
    } finally {
      setLoading(false)
    }
  }, [endpoint])

  return { results, loading, search }
}

// Usage in component
function SearchComponent() {
  const { results, loading, search } = useBrainySearch()

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        onChange={(e) => search(e.target.value)}
      />
      {loading && <div>Searching...</div>}
      <div>
        {results.map(result => (
          <div key={result.id}>
            <h3>{result.data}</h3>
            <p>Score: {(result.score * 100).toFixed(1)}%</p>
          </div>
        ))}
      </div>
    </div>
  )
}

Shared Server Instance

On the server, create one Brainy instance and reuse it across requests. This module is imported only by server code (API routes, server components), never by client components:

// lib/brain.server.js
import { Brainy } from '@soulcraft/brainy'

let brainPromise

export function getBrain() {
  if (!brainPromise) {
    brainPromise = (async () => {
      // new Brainy() auto-detects filesystem persistence on Node
      const brain = new Brainy()
      await brain.init()
      return brain
    })()
  }
  return brainPromise
}

🟢 Vue.js Integration

Vue components call a server endpoint (see the Nuxt server route in the Vue.js Integration Guide); Brainy itself runs on the server.

Composition API (client component)

<template>
  <div>
    <input v-model="query" @input="search" placeholder="Search..." />
    <div v-for="result in results" :key="result.id">
      <h3>{{ result.data }}</h3>
      <p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const query = ref('')
const results = ref([])

const search = async () => {
  if (!query.value) return
  const res = await fetch('/api/search', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: query.value })
  })
  results.value = (await res.json()).results
}
</script>

Shared Server Instance

On the server, create one Brainy instance and reuse it across requests:

// server/brain.js (server-only module)
import { Brainy } from '@soulcraft/brainy'

let brainPromise

export function getBrain() {
  if (!brainPromise) {
    brainPromise = (async () => {
      // new Brainy() auto-detects filesystem persistence on Node
      const brain = new Brainy()
      await brain.init()
      return brain
    })()
  }
  return brainPromise
}

🅰️ Angular Integration

The Angular service calls your backend over HTTP; Brainy lives in that backend, not in the browser.

Service Pattern (calls the backend)

// brainy.service.ts
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'

@Injectable({
  providedIn: 'root'
})
export class BrainyService {
  constructor(private http: HttpClient) {}

  search(query: string): Observable<{ results: any[] }> {
    return this.http.post<{ results: any[] }>('/api/search', { query })
  }

  add(data: any, type: string, metadata?: any): Observable<{ id: string }> {
    return this.http.post<{ id: string }>('/api/add', { data, type, metadata })
  }
}
// search.component.ts
import { Component } from '@angular/core'
import { BrainyService } from './brainy.service'

@Component({
  selector: 'app-search',
  template: `
    <div>
      <input
        [(ngModel)]="query"
        (input)="search()"
        placeholder="Search..."
      />
      <div *ngFor="let result of results">
        <h3>{{ result.data }}</h3>
        <p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
      </div>
    </div>
  `
})
export class SearchComponent {
  query = ''
  results: any[] = []

  constructor(private brainyService: BrainyService) {}

  search() {
    if (!this.query) return
    this.brainyService.search(this.query).subscribe(({ results }) => {
      this.results = results
    })
  }
}

The matching backend endpoint uses Brainy directly (Node/Bun):

// server: api/search
import { Brainy } from '@soulcraft/brainy'

const brain = new Brainy() // auto-detects filesystem persistence on Node
await brain.init()

export async function handleSearch(query: string) {
  return await brain.find(query)
}

🚀 Next.js Integration

In Next.js, Brainy lives in server code only: API routes, server components, or server actions. Create one shared instance in a server-only module.

Shared Server Instance

// lib/brain.server.js  (imported only by server code)
import { Brainy } from '@soulcraft/brainy'

let brainPromise

export function getBrain() {
  if (!brainPromise) {
    brainPromise = (async () => {
      const brain = new Brainy({
        storage: { type: 'filesystem', path: './data' }
      })
      await brain.init()
      return brain
    })()
  }
  return brainPromise
}

API Routes

// app/api/search/route.js
import { getBrain } from '@/lib/brain.server'

export async function POST(request) {
  const { query } = await request.json()
  const brain = await getBrain()
  const results = await brain.find(query)

  return Response.json({ results })
}

Server Action

// app/actions.js
'use server'
import { getBrain } from '@/lib/brain.server'

export async function search(query) {
  const brain = await getBrain()
  return await brain.find(query)
}

🔷 SvelteKit Integration

Brainy runs in a server-only module (*.server.js); the component fetches results from an endpoint.

// src/lib/server/brain.js  (server-only — note the .server suffix)
import { Brainy } from '@soulcraft/brainy'

let brainPromise

export function getBrain() {
  if (!brainPromise) {
    brainPromise = (async () => {
      const brain = new Brainy() // auto-detects filesystem persistence
      await brain.init()
      return brain
    })()
  }
  return brainPromise
}
// src/routes/api/search/+server.js
import { json } from '@sveltejs/kit'
import { getBrain } from '$lib/server/brain'

export async function POST({ request }) {
  const { query } = await request.json()
  const brain = await getBrain()
  return json({ results: await brain.find(query) })
}
<!-- SearchComponent.svelte -->
<script>
  let query = ''
  let results = []

  async function search() {
    if (!query) return
    const res = await fetch('/api/search', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query })
    })
    results = (await res.json()).results
  }
</script>

<div>
  <input bind:value={query} on:input={search} placeholder="Search..." />

  {#each results as result}
    <div>
      <h3>{result.data}</h3>
      <p>Score: {(result.score * 100).toFixed(1)}%</p>
    </div>
  {/each}
</div>

🌟 Solid.js Integration

The component calls a server endpoint (use SolidStart server routes, or any backend, to host Brainy):

import { createSignal } from 'solid-js'

function SearchComponent() {
  const [query, setQuery] = createSignal('')
  const [results, setResults] = createSignal([])

  const search = async () => {
    if (!query()) return
    const res = await fetch('/api/search', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: query() })
    })
    setResults((await res.json()).results)
  }

  return (
    <div>
      <input
        value={query()}
        onInput={(e) => {
          setQuery(e.target.value)
          search()
        }}
        placeholder="Search..."
      />

      <For each={results()}>
        {(result) => (
          <div>
            <h3>{result.data}</h3>
            <p>Score: {(result.score * 100).toFixed(1)}%</p>
          </div>
        )}
      </For>
    </div>
  )
}

📦 Bundler Configuration

Brainy is a server-side dependency, so keep it out of client bundles. Import it only from server-only modules (*.server.js, API routes, server components, server actions). If your bundler ever tries to pull Brainy into a client bundle, that's a sign it's being imported from a client component — move the import to a server module.

For server builds, mark Brainy as external so the bundler doesn't inline it:

// vite.config.js  (SSR build)
import { defineConfig } from 'vite'

export default defineConfig({
  ssr: {
    external: ['@soulcraft/brainy']
  }
})
// rollup.config.js  (server bundle)
export default {
  external: ['@soulcraft/brainy', 'node:fs', 'node:path', 'node:crypto']
}

🌐 SSR/SSG Considerations

Server-Side Rendering

Instantiate Brainy on the server and feed its results into the rendered page. Never construct it in client code.

// Server-side data loading (framework loader / getServerSideProps / load fn)
import { getBrain } from './brain.server'

export async function load({ url }) {
  const brain = await getBrain()
  const query = url.searchParams.get('q') ?? ''
  const results = query ? await brain.find(query) : []
  return { results }
}

Static Site Generation

// For build-time usage (runs in Node during the build)
import { Brainy } from '@soulcraft/brainy'

export async function generateStaticProps() {
  const brain = new Brainy({
    storage: { type: 'filesystem', path: './content' }
  })
  await brain.init()

  // Build search index (paginate with { limit, offset } for larger stores)
  const allContent = await brain.find({ limit: 1000 })

  return {
    props: { searchIndex: allContent }
  }
}

🔧 Framework-Specific Tips

React

  • Keep components client-side and call a Brainy-backed API route
  • Use useCallback for fetch handlers to prevent re-renders
  • Debounce keystroke-driven searches before hitting the endpoint

Vue

  • Components call an endpoint; the shared instance lives in a server module
  • Consider Pinia for caching results client-side
  • Debounce reactive search queries

Angular

  • Use HttpClient and RxJS to call the backend
  • Hold the shared Brainy instance in your Node backend, not the app
  • Consider lazy loading search features in feature modules

Next.js

  • Put Brainy in server-only modules (*.server.js), API routes, or server actions
  • Reuse one shared instance across requests
  • Implement proper error boundaries for failed fetches

🚨 Common Issues & Solutions

Issue: "fs module not found" / "crypto is not defined" in the browser

Cause: Brainy was imported into a client bundle. Brainy 8.0 is a server-side library (Node 22+/Bun) and uses Node built-ins like fs and crypto. Solution: Import Brainy only from server code — server-only modules (*.server.js), API routes, server components, or server actions. From client components, call those endpoints instead.

Issue: Large client bundle size

Cause: A client module is pulling in Brainy. Solution: Move the import { Brainy } from '@soulcraft/brainy' into a server-only module so it never reaches the browser bundle.

Issue: SSR hydration mismatch

Solution: Run the search on the server (loader / server action / API route) and pass the results down as props, so server and client render the same markup.

🎯 Best Practices

  1. Initialize Once: Create one shared Brainy instance per server process, not per request
  2. Server-Only: Import Brainy only from server modules — never from client components
  3. Endpoint Boundary: Expose search/add through API routes or server actions
  4. Handle Loading: Show loading states in the client while the fetch is in flight
  5. Error Handling: Catch and surface failed endpoint calls gracefully
  6. Storage: Use filesystem for persistence (the default on Node) or memory for ephemeral/tests

📚 Next Steps

🤝 Community Examples

Check out community examples in the examples repository:

  • React + TypeScript starter
  • Vue 3 + Composition API
  • Next.js full-stack app
  • Svelte SPA with search
  • Angular enterprise app