brainy/docs/guides/framework-integration.md
David Snelling 89036deb20 refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.

Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
  BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
  PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
  (renamed data-backup.test.ts → data-portable-graph.test.ts).

The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import format was introduced in 7.32.0 and has not been adopted by any
consumer, so there are no stored documents to stay compatible with — a clean rename
beats carrying a legacy tag forward. No deprecated aliases (nothing to alias).

1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:30:06 -07:00

14 KiB

Framework Integration Guide

Brainy 3.0 is framework-first - designed from the ground up to work seamlessly with modern JavaScript frameworks. This guide shows you how to integrate Brainy into any framework.

🎯 Why Framework-First?

Traditional AI databases require complex browser polyfills and bundler configurations. Brainy 3.0 trusts your framework to handle this:

  • Zero configuration: Just import { Brainy } from '@soulcraft/brainy'
  • Framework responsibility: Let Next.js, Vite, Webpack handle Node.js polyfills
  • Cleaner code: No browser-specific entry points or conditional imports
  • Better DX: Same API everywhere - browser, server, edge

🚀 Quick Start

Install Brainy

npm install @soulcraft/brainy

Basic Integration

import { Brainy } from '@soulcraft/brainy'

// Works in any framework!
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

Basic Hook Pattern

import { useState, useEffect, useCallback } from 'react'
import { Brainy } from '@soulcraft/brainy'

function useBrainy() {
  const [brain, setBrain] = useState(null)
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    const initBrain = async () => {
      const newBrain = new Brainy({
        storage: { type: 'opfs' } // Browser storage
      })
      await newBrain.init()
      setBrain(newBrain)
      setIsReady(true)
    }

    initBrain()
  }, [])

  return { brain, isReady }
}

// Usage in component
function SearchComponent() {
  const { brain, isReady } = useBrainy()
  const [results, setResults] = useState([])

  const handleSearch = useCallback(async (query) => {
    if (!isReady) return
    const searchResults = await brain.find(query)
    setResults(searchResults)
  }, [brain, isReady])

  if (!isReady) return <div>Loading AI...</div>

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

React Context Pattern

import React, { createContext, useContext, useEffect, useState } from 'react'
import { Brainy } from '@soulcraft/brainy'

const BrainyContext = createContext()

export function BrainyProvider({ children }) {
  const [brain, setBrain] = useState(null)
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    const initBrain = async () => {
      const newBrain = new Brainy()
      await newBrain.init()
      setBrain(newBrain)
      setIsReady(true)
    }

    initBrain()
  }, [])

  return (
    <BrainyContext.Provider value={{ brain, isReady }}>
      {children}
    </BrainyContext.Provider>
  )
}

export function useBrainContext() {
  const context = useContext(BrainyContext)
  if (!context) {
    throw new Error('useBrainContext must be used within BrainyProvider')
  }
  return context
}

🟢 Vue.js Integration

Composition API

<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, onMounted } from 'vue'
import { Brainy } from '@soulcraft/brainy'

const brain = ref(null)
const isReady = ref(false)
const query = ref('')
const results = ref([])

onMounted(async () => {
  brain.value = new Brainy({
    storage: { type: 'opfs' }
  })
  await brain.value.init()
  isReady.value = true
})

const search = async () => {
  if (!isReady.value || !query.value) return
  results.value = await brain.value.find(query.value)
}
</script>

Vue 3 Plugin

// plugins/brainy.js
import { Brainy } from '@soulcraft/brainy'

export default {
  install(app, options) {
    const brain = new Brainy(options)

    app.config.globalProperties.$brain = brain
    app.provide('brain', brain)

    // Initialize on app mount
    brain.init()
  }
}

// main.js
import { createApp } from 'vue'
import BrainyPlugin from './plugins/brainy'

const app = createApp(App)
app.use(BrainyPlugin, {
  storage: { type: 'opfs' }
})

🅰️ Angular Integration

Service Pattern

// brainy.service.ts
import { Injectable } from '@angular/core'
import { BehaviorSubject, Observable } from 'rxjs'
import { Brainy } from '@soulcraft/brainy'

@Injectable({
  providedIn: 'root'
})
export class BrainyService {
  private brain: Brainy
  private readySubject = new BehaviorSubject<boolean>(false)

  ready$: Observable<boolean> = this.readySubject.asObservable()

  constructor() {
    this.initBrain()
  }

  private async initBrain() {
    this.brain = new Brainy({
      storage: { type: 'opfs' }
    })
    await this.brain.init()
    this.readySubject.next(true)
  }

  async search(query: string): Promise<any[]> {
    if (!this.readySubject.value) {
      throw new Error('Brain not ready')
    }
    return await this.brain.find(query)
  }

  async add(data: any, type: string, metadata?: any): Promise<string> {
    if (!this.readySubject.value) {
      throw new Error('Brain not ready')
    }
    return await this.brain.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) {}

  async search() {
    if (!this.query) return
    this.results = await this.brainyService.search(this.query)
  }
}

🚀 Next.js Integration

App Router (Next.js 13+)

// app/providers.jsx
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { Brainy } from '@soulcraft/brainy'

const BrainyContext = createContext()

export function BrainyProvider({ children }) {
  const [brain, setBrain] = useState(null)
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    const initBrain = async () => {
      const newBrain = new Brainy()
      await newBrain.init()
      setBrain(newBrain)
      setIsReady(true)
    }

    initBrain()
  }, [])

  return (
    <BrainyContext.Provider value={{ brain, isReady }}>
      {children}
    </BrainyContext.Provider>
  )
}

export const useBrainy = () => useContext(BrainyContext)
// app/layout.jsx
import { BrainyProvider } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <BrainyProvider>
          {children}
        </BrainyProvider>
      </body>
    </html>
  )
}

API Routes

// app/api/search/route.js
import { Brainy } from '@soulcraft/brainy'

const brain = new Brainy({
  storage: { type: 'filesystem', path: './data' }
})
await brain.init()

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

  return Response.json({ results })
}

🔷 Svelte Integration

<!-- SearchComponent.svelte -->
<script>
  import { onMount } from 'svelte'
  import { Brainy } from '@soulcraft/brainy'

  let brain = null
  let isReady = false
  let query = ''
  let results = []

  onMount(async () => {
    brain = new Brainy({
      storage: { type: 'opfs' }
    })
    await brain.init()
    isReady = true
  })

  async function search() {
    if (!isReady || !query) return
    results = await brain.find(query)
  }
</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

import { createSignal, onMount } from 'solid-js'
import { Brainy } from '@soulcraft/brainy'

function SearchComponent() {
  const [brain, setBrain] = createSignal(null)
  const [isReady, setIsReady] = createSignal(false)
  const [query, setQuery] = createSignal('')
  const [results, setResults] = createSignal([])

  onMount(async () => {
    const newBrain = new Brainy()
    await newBrain.init()
    setBrain(newBrain)
    setIsReady(true)
  })

  const search = async () => {
    if (!isReady() || !query()) return
    const searchResults = await brain().find(query())
    setResults(searchResults)
  }

  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

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  define: {
    global: 'globalThis'
  },
  optimizeDeps: {
    include: ['@soulcraft/brainy']
  }
})

Webpack

// webpack.config.js
module.exports = {
  resolve: {
    fallback: {
      "fs": false,
      "path": require.resolve("path-browserify"),
      "crypto": require.resolve("crypto-browserify")
    }
  },
  plugins: [
    new webpack.ProvidePlugin({
      global: 'global'
    })
  ]
}

Rollup

// rollup.config.js
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'

export default {
  plugins: [
    nodeResolve({
      browser: true,
      preferBuiltins: false
    }),
    commonjs()
  ]
}

🌐 SSR/SSG Considerations

Server-Side Rendering

// Check if running in browser
if (typeof window !== 'undefined') {
  // Browser-only code
  const brain = new Brainy({
    storage: { type: 'opfs' }
  })
}

// Or use dynamic imports
const initBrainForBrowser = async () => {
  if (typeof window === 'undefined') return null

  const { Brainy } = await import('@soulcraft/brainy')
  const brain = new Brainy()
  await brain.init()
  return brain
}

Static Site Generation

// For build-time usage
import { Brainy } from '@soulcraft/brainy'

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

  // Build a search index — export the whole brain as a portable PortableGraph document
  const graph = await brain.data().then(d => d.export())

  return {
    props: { searchIndex: graph.entities }
  }
}

🔧 Framework-Specific Tips

React

  • Use useCallback for search functions to prevent re-renders
  • Consider useMemo for expensive brain operations
  • Implement cleanup in useEffect for proper memory management

Vue

  • Use shallowRef for the brain instance (it's not reactive data)
  • Consider Pinia for global brain state management
  • Use watchEffect for reactive search queries

Angular

  • Implement proper dependency injection with services
  • Use RxJS observables for reactive search
  • Consider lazy loading brain in feature modules

Next.js

  • Use dynamic imports for client-side only features
  • Consider API routes for server-side brain operations
  • Implement proper error boundaries

🚨 Common Issues & Solutions

Issue: "crypto is not defined"

Solution: Your framework should handle this automatically. If not:

// Add to your bundle config
define: {
  global: 'globalThis'
}

Issue: "fs module not found"

Solution: This is expected in browsers. Use browser-compatible storage:

const brain = new Brainy({
  storage: { type: 'opfs' } // Or 'memory' for development
})

Issue: Large bundle size

Solution: Use dynamic imports for optional features:

const brain = await import('@soulcraft/brainy').then(m => new m.Brainy())

Issue: SSR hydration mismatch

Solution: Initialize brain only on client:

useEffect(() => {
  // Browser-only initialization
  initBrain()
}, [])

🎯 Best Practices

  1. Initialize Once: Create brain instance at app level, not component level
  2. Use Context: Share brain instance across components with context/providers
  3. Handle Loading: Always show loading states during brain initialization
  4. Error Boundaries: Implement proper error handling for brain operations
  5. Memory Management: Clean up brain instances on unmount
  6. Storage Strategy: Choose appropriate storage for your deployment target

📚 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