The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
28 KiB
Vue.js Integration Guide
Complete guide to integrating Brainy with Vue.js applications, covering Vue 3, Nuxt.js, Composition API, Options API, and advanced patterns.
Runtime: Brainy 8.0 runs on Node.js 22+ and Bun, server-side only — it is not a browser library. In Vue apps, Brainy lives behind an HTTP endpoint (a Nuxt server route, or any Node/Bun backend), and your components call that endpoint. The composables, plugin, and store below are wrappers around
fetch; the single Brainy instance is created once on the server (see Nuxt Server Route).
🚀 Quick Start
Installation
npm create vue@latest my-brainy-app
cd my-brainy-app
npm install
npm install @soulcraft/brainy
Basic Setup
The composable talks to a Brainy-backed endpoint (see Nuxt Server Route). It is always "ready" because there is no client-side initialization — the server owns the Brainy instance.
// src/composables/useBrainy.js
import { ref } from 'vue'
export function useBrainy(endpoint = '/api/brain') {
const error = ref(null)
const search = async (query, options = {}) => {
error.value = null
try {
const res = await fetch(`${endpoint}/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, options })
})
if (!res.ok) throw new Error(`Search failed: ${res.status}`)
return (await res.json()).results
} catch (err) {
error.value = err.message
console.error('Brainy search failed:', err)
return []
}
}
const add = async (data, type, metadata) => {
const res = await fetch(`${endpoint}/add`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data, type, metadata })
})
return (await res.json()).id
}
const stats = async () => {
const res = await fetch(`${endpoint}/stats`)
return await res.json()
}
return { search, add, stats, error: readonly(error) }
}
🧩 Composition API
Search Component
<!-- src/components/Search.vue -->
<template>
<div class="search-container">
<div class="search-input">
<input
v-model="query"
@input="handleSearch"
placeholder="Search..."
class="input"
/>
</div>
<div v-if="loading" class="loading">
Searching...
</div>
<div v-else class="results">
<div
v-for="result in results"
:key="result.id"
class="result-item"
>
<h3>{{ result.data }}</h3>
<div class="result-meta">
<span>Score: {{ (result.score * 100).toFixed(1) }}%</span>
<span v-if="result.metadata?.type">
Type: {{ result.metadata.type }}
</span>
</div>
</div>
<div v-if="query && !loading && results.length === 0" class="no-results">
No results found for "{{ query }}"
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useBrainy } from '../composables/useBrainy'
import { debounce } from '../utils/debounce'
const { search: searchBrain } = useBrainy()
const query = ref('')
const results = ref([])
const loading = ref(false)
const search = async (searchQuery) => {
if (!searchQuery.trim()) {
results.value = []
return
}
loading.value = true
try {
results.value = await searchBrain(searchQuery)
} catch (error) {
console.error('Search error:', error)
results.value = []
} finally {
loading.value = false
}
}
const debouncedSearch = debounce(search, 300)
const handleSearch = () => debouncedSearch(query.value)
// Watch for query changes
watch(query, (newQuery) => {
if (!newQuery.trim()) {
results.value = []
loading.value = false
}
})
</script>
<style scoped>
.search-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.search-input {
margin-bottom: 1.5rem;
}
.input {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
}
.input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
color: #6b7280;
}
.spinner {
width: 1.5rem;
height: 1.5rem;
border: 2px solid #e5e7eb;
border-top: 2px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 0.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.results {
space-y: 1rem;
}
.result-item {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.result-item h3 {
margin: 0 0 0.5rem 0;
font-size: 1.125rem;
font-weight: 600;
}
.result-meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
color: #6b7280;
}
.no-results {
text-align: center;
padding: 2rem;
color: #6b7280;
}
</style>
Data Management
<!-- src/components/DataManager.vue -->
<template>
<div class="data-manager">
<h2>Data Management</h2>
<div>
<!-- Add Data Form -->
<form @submit.prevent="addData" class="add-form">
<h3>Add New Data</h3>
<div class="form-group">
<label for="data">Data:</label>
<textarea
id="data"
v-model="newItem.data"
placeholder="Enter data..."
required
></textarea>
</div>
<div class="form-group">
<label for="type">Type:</label>
<select id="type" v-model="newItem.type">
<option value="concept">Concept</option>
<option value="document">Document</option>
<option value="person">Person</option>
<option value="project">Project</option>
</select>
</div>
<div class="form-group">
<label for="tags">Tags (comma-separated):</label>
<input
id="tags"
v-model="newItem.tags"
placeholder="tag1, tag2, tag3"
/>
</div>
<button type="submit" :disabled="!newItem.data.trim()">
Add Data
</button>
</form>
<!-- Stats -->
<div v-if="stats" class="stats">
<h3>Statistics</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Total Items:</span>
<span class="stat-value">{{ stats.totalItems }}</span>
</div>
<div class="stat-item">
<span class="stat-label">Storage Type:</span>
<span class="stat-value">{{ stats.storageType }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useBrainy } from '../composables/useBrainy'
const { add, stats: fetchStats } = useBrainy()
const newItem = reactive({
data: '',
type: 'concept',
tags: ''
})
const stats = ref(null)
onMounted(loadStats)
const addData = async () => {
try {
const metadata = {
addedAt: new Date().toISOString(),
tags: newItem.tags.split(',').map(tag => tag.trim()).filter(Boolean)
}
await add(newItem.data, newItem.type, metadata)
// Reset form
newItem.data = ''
newItem.type = 'concept'
newItem.tags = ''
// Refresh stats
await loadStats()
} catch (error) {
console.error('Failed to add data:', error)
}
}
const loadStats = async () => {
try {
stats.value = await fetchStats()
} catch (error) {
console.error('Failed to load stats:', error)
}
}
</script>
<style scoped>
.data-manager {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.add-form {
background: #f9fafb;
padding: 1.5rem;
border-radius: 0.5rem;
margin-bottom: 2rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
}
.form-group textarea {
height: 100px;
resize: vertical;
}
button {
background: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.stats {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.stat-item {
display: flex;
justify-content: space-between;
}
.stat-label {
font-weight: 500;
}
.stat-value {
color: #3b82f6;
font-weight: 600;
}
</style>
🏗️ Options API
Search Component (Options API)
<!-- src/components/SearchOptions.vue -->
<template>
<div class="search-container">
<input
v-model="query"
@input="handleSearch"
placeholder="Search..."
class="search-input"
/>
<div v-if="loading" class="loading">Searching...</div>
<div class="results">
<div
v-for="result in results"
:key="result.id"
class="result-item"
>
<h3>{{ result.data }}</h3>
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
</div>
</div>
</div>
</template>
<script>
import { debounce } from '../utils/debounce'
export default {
name: 'SearchOptions',
data() {
return {
query: '',
results: [],
loading: false
}
},
methods: {
async search(query) {
if (!query.trim()) {
this.results = []
return
}
this.loading = true
try {
const res = await fetch('/api/brain/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
})
this.results = (await res.json()).results
} catch (error) {
console.error('Search error:', error)
this.results = []
} finally {
this.loading = false
}
},
handleSearch: debounce(function() {
this.search(this.query)
}, 300)
},
watch: {
query(newQuery) {
if (!newQuery.trim()) {
this.results = []
this.loading = false
}
}
}
}
</script>
🔌 Vue Plugin
Global Brainy Plugin
The plugin exposes a global $searchBrain helper that calls your Brainy-backed endpoint. Brainy itself runs on the server (see Nuxt Server Route).
// src/plugins/brainy.js
export default {
install(app, options = {}) {
const endpoint = options.endpoint ?? '/api/brain'
// Add global search method (calls the server endpoint)
app.config.globalProperties.$searchBrain = async (query, searchOptions) => {
const res = await fetch(`${endpoint}/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, options: searchOptions })
})
return (await res.json()).results
}
}
}
Plugin Usage
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import BrainyPlugin from './plugins/brainy'
const app = createApp(App)
app.use(BrainyPlugin, {
endpoint: '/api/brain'
})
app.mount('#app')
<!-- Component using the plugin -->
<template>
<div>
<input v-model="query" @input="search" />
<div v-for="result in results" :key="result.id">
{{ result.data }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
query: '',
results: []
}
},
methods: {
async search() {
// Use the global method
this.results = await this.$searchBrain(this.query)
}
}
}
</script>
🏰 Nuxt.js Integration
Nuxt's server engine (Nitro) is the natural home for Brainy: it runs on Node/Bun, so the Brainy instance lives in server/, and pages/components call its routes.
Nuxt Server Route
// server/utils/brain.js (server-only — Nitro never bundles this into the client)
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
}
// server/api/brain/search.post.js
import { getBrain } from '../../utils/brain'
export default defineEventHandler(async (event) => {
const { query, options } = await readBody(event)
const brain = await getBrain()
return { results: await brain.find(query, options) }
})
// server/api/brain/add.post.js
import { getBrain } from '../../utils/brain'
export default defineEventHandler(async (event) => {
const { data, type, metadata } = await readBody(event)
const brain = await getBrain()
return { id: await brain.add({ data, type, metadata }) }
})
// server/api/brain/stats.get.js
import { getBrain } from '../../utils/brain'
export default defineEventHandler(async () => {
const brain = await getBrain()
return await brain.stats()
})
Nuxt Composable
// composables/useBrainy.js
export const useBrainy = () => {
const search = async (query, options = {}) => {
return (await $fetch('/api/brain/search', {
method: 'POST',
body: { query, options }
})).results
}
const add = async (data, type, metadata) => {
return (await $fetch('/api/brain/add', {
method: 'POST',
body: { data, type, metadata }
})).id
}
const stats = async () => {
return await $fetch('/api/brain/stats')
}
return { search, add, stats }
}
Nuxt Page Example
<!-- pages/search.vue -->
<template>
<div>
<h1>Search</h1>
<input
v-model="query"
@input="handleSearch"
placeholder="Search..."
/>
<div v-if="searching" class="loading">Searching...</div>
<div class="results">
<div v-for="result in results" :key="result.id" class="result">
<h3>{{ result.data }}</h3>
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
</div>
</div>
</div>
</template>
<script setup>
const { search } = useBrainy()
const query = ref('')
const results = ref([])
const searching = ref(false)
const handleSearch = debounce(async () => {
if (!query.value.trim()) {
results.value = []
return
}
searching.value = true
try {
results.value = await search(query.value)
} catch (error) {
console.error('Search failed:', error)
} finally {
searching.value = false
}
}, 300)
</script>
🛠️ Advanced Patterns
Global State Management with Pinia
The store caches results and stats client-side; all Brainy work happens behind the server endpoints.
// stores/brainy.js
import { defineStore } from 'pinia'
export const useBrainyStore = defineStore('brainy', () => {
const error = ref(null)
const stats = ref(null)
const search = async (query, options = {}) => {
error.value = null
try {
const res = await fetch('/api/brain/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, options })
})
return (await res.json()).results
} catch (err) {
error.value = err.message
throw err
}
}
const add = async (data, type, metadata) => {
const res = await fetch('/api/brain/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data, type, metadata })
})
const { id } = await res.json()
await loadStats() // Refresh stats
return id
}
const loadStats = async () => {
try {
const res = await fetch('/api/brain/stats')
stats.value = await res.json()
} catch (err) {
console.error('Failed to load stats:', err)
}
}
return {
error: readonly(error),
stats: readonly(stats),
search,
add,
loadStats
}
})
Real-time Search Component
<!-- src/components/RealTimeSearch.vue -->
<template>
<div class="realtime-search">
<div class="search-bar">
<input
ref="searchInput"
v-model="query"
@input="handleInput"
@focus="showResults = true"
@blur="handleBlur"
placeholder="Search..."
class="search-input"
/>
<div v-if="loading" class="search-spinner">⟳</div>
</div>
<Transition name="dropdown">
<div
v-if="showResults && (results.length > 0 || query.trim())"
class="search-dropdown"
>
<div
v-for="(result, index) in results"
:key="result.id"
:class="['result-item', { active: selectedIndex === index }]"
@mousedown="selectResult(result)"
@mouseenter="selectedIndex = index"
>
<div class="result-content">
<span class="result-text">{{ result.data }}</span>
<span class="result-score">{{ (result.score * 100).toFixed(0) }}%</span>
</div>
</div>
<div v-if="query.trim() && results.length === 0 && !loading" class="no-results">
No results found
</div>
</div>
</Transition>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { useBrainyStore } from '../stores/brainy'
import { debounce } from '../utils/debounce'
const emit = defineEmits(['select'])
const brainyStore = useBrainyStore()
const searchInput = ref(null)
const query = ref('')
const results = ref([])
const loading = ref(false)
const showResults = ref(false)
const selectedIndex = ref(-1)
const search = async (searchQuery) => {
if (!searchQuery.trim()) {
results.value = []
return
}
loading.value = true
try {
const searchResults = await brainyStore.search(searchQuery, { limit: 10 })
results.value = searchResults
} catch (error) {
console.error('Search error:', error)
results.value = []
} finally {
loading.value = false
}
}
const debouncedSearch = debounce(search, 200)
const handleInput = () => {
selectedIndex.value = -1
debouncedSearch(query.value)
}
const handleBlur = () => {
// Delay hiding to allow clicking on results
setTimeout(() => {
showResults.value = false
}, 150)
}
const selectResult = (result) => {
query.value = result.data
showResults.value = false
emit('select', result)
}
// Keyboard navigation
const handleKeydown = (event) => {
if (!showResults.value) return
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
selectedIndex.value = Math.min(selectedIndex.value + 1, results.value.length - 1)
break
case 'ArrowUp':
event.preventDefault()
selectedIndex.value = Math.max(selectedIndex.value - 1, -1)
break
case 'Enter':
event.preventDefault()
if (selectedIndex.value >= 0) {
selectResult(results.value[selectedIndex.value])
}
break
case 'Escape':
showResults.value = false
searchInput.value?.blur()
break
}
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
</script>
<style scoped>
.realtime-search {
position: relative;
width: 100%;
}
.search-bar {
position: relative;
display: flex;
align-items: center;
}
.search-input {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-spinner {
position: absolute;
right: 0.75rem;
animation: spin 1s linear infinite;
color: #6b7280;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-height: 300px;
overflow-y: auto;
}
.result-item {
padding: 0.75rem;
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
}
.result-item:last-child {
border-bottom: none;
}
.result-item:hover,
.result-item.active {
background: #f3f4f6;
}
.result-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.result-text {
flex: 1;
margin-right: 0.5rem;
}
.result-score {
font-size: 0.875rem;
color: #6b7280;
}
.no-results {
padding: 1rem;
text-align: center;
color: #6b7280;
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>
📊 Performance Optimization
Lazy Loading
<!-- src/components/LazyBrainComponent.vue -->
<template>
<div>
<button v-if="!loaded" @click="loadBrain" class="load-button">
Load AI Search
</button>
<Suspense v-else>
<template #default>
<SearchComponent />
</template>
<template #fallback>
<div class="loading">Loading AI...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const loaded = ref(false)
const SearchComponent = defineAsyncComponent(() =>
import('./Search.vue')
)
const loadBrain = () => {
loaded.value = true
}
</script>
Virtual Scrolling for Large Results
<!-- src/components/VirtualResults.vue -->
<template>
<div ref="container" class="virtual-container" @scroll="onScroll">
<div :style="{ height: totalHeight + 'px' }" class="virtual-spacer">
<div
:style="{ transform: `translateY(${offsetY}px)` }"
class="virtual-content"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="result-item"
:style="{ height: itemHeight + 'px' }"
>
<h3>{{ item.data }}</h3>
<p>Score: {{ (item.score * 100).toFixed(1) }}%</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
items: Array,
itemHeight: { type: Number, default: 80 }
})
const container = ref(null)
const scrollTop = ref(0)
const containerHeight = ref(400)
const totalHeight = computed(() => props.items.length * props.itemHeight)
const visibleStart = computed(() => Math.floor(scrollTop.value / props.itemHeight))
const visibleEnd = computed(() => Math.min(
visibleStart.value + Math.ceil(containerHeight.value / props.itemHeight) + 1,
props.items.length
))
const visibleItems = computed(() =>
props.items.slice(visibleStart.value, visibleEnd.value)
)
const offsetY = computed(() => visibleStart.value * props.itemHeight)
const onScroll = () => {
scrollTop.value = container.value.scrollTop
}
onMounted(() => {
containerHeight.value = container.value.clientHeight
})
</script>
<style scoped>
.virtual-container {
height: 400px;
overflow: auto;
}
.virtual-spacer {
position: relative;
}
.virtual-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.result-item {
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
🧪 Testing
Component Testing with Vitest
The component talks to the server over fetch, so mock the endpoint, not Brainy itself.
// tests/components/Search.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import Search from '../src/components/Search.vue'
// Mock the server endpoint
beforeEach(() => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
results: [{ id: '1', data: 'Test result', score: 0.9 }]
})
})
})
describe('Search Component', () => {
let wrapper
beforeEach(() => {
wrapper = mount(Search)
})
it('renders search input', () => {
expect(wrapper.find('input').exists()).toBe(true)
})
it('performs search when input changes', async () => {
const input = wrapper.find('input')
await input.setValue('test query')
await input.trigger('input')
// Wait for debounced search
await new Promise(resolve => setTimeout(resolve, 350))
expect(global.fetch).toHaveBeenCalled()
expect(wrapper.text()).toContain('Test result')
})
})
E2E Testing with Playwright
// tests/e2e/search.spec.js
import { test, expect } from '@playwright/test'
test('search functionality works', async ({ page }) => {
await page.goto('/')
// Wait for brain to initialize
await page.waitForSelector('[data-testid="search-input"]')
// Perform search
await page.fill('[data-testid="search-input"]', 'test query')
// Wait for results
await page.waitForSelector('[data-testid="search-results"]')
// Check results
const results = await page.locator('[data-testid="result-item"]')
await expect(results).toHaveCountGreaterThan(0)
})
🚀 Production Tips
Bundle Optimization
Keep Brainy out of the client bundle — it belongs to the server build only. Mark it external for SSR so the bundler resolves it at runtime instead of inlining it.
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
ssr: {
external: ['@soulcraft/brainy']
}
})
Error Handling
Wrap the endpoint call with retry/backoff so transient network failures don't surface to the user.
// src/composables/useBrainyWithErrorHandling.js
import { ref } from 'vue'
export function useBrainyWithErrorHandling(endpoint = '/api/brain') {
const error = ref(null)
const search = async (query, options = {}, maxRetries = 3) => {
error.value = null
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(`${endpoint}/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, options })
})
if (!res.ok) throw new Error(`Search failed: ${res.status}`)
return (await res.json()).results
} catch (err) {
error.value = err.message
if (attempt === maxRetries) throw err
// Exponential backoff before retrying
await new Promise(resolve => setTimeout(resolve, 2000 * (attempt + 1)))
}
}
}
return {
error: readonly(error),
search
}
}
🎯 Complete Example
Here's a complete Vue 3 application structure:
my-brainy-vue-app/
├── server/ # Server-side (Node/Bun) — hosts Brainy
│ ├── utils/
│ │ └── brain.js # Shared Brainy instance (getBrain)
│ └── api/brain/
│ ├── search.post.js
│ ├── add.post.js
│ └── stats.get.js
├── src/
│ ├── components/
│ │ ├── Search.vue
│ │ ├── DataManager.vue
│ │ └── RealTimeSearch.vue
│ ├── composables/
│ │ ├── useBrainy.js
│ │ └── useBrainyWithErrorHandling.js
│ ├── stores/
│ │ └── brainy.js
│ ├── utils/
│ │ └── debounce.js
│ ├── App.vue
│ └── main.js
├── tests/
│ ├── components/
│ └── e2e/
├── vite.config.js
└── package.json
This provides a complete, production-ready Vue.js application: Brainy runs on the server, and the client talks to it over HTTP.
📚 Next Steps
- Framework Integration Guide - Multi-framework patterns
- Production Deployment - Deploy to production
- API Reference - Complete API documentation
- Examples Repository - More examples