AI Knowledge Brain for Teams — Multi-tenant SaaS that turns raw conversation logs into a living, queryable knowledge graph.
Imagine your team has a smart assistant with a perfect memory. Every time your team discusses something — a decision, a technical choice, a preference — OneBrainer listens, extracts the key facts, and builds a web of connections between them.
When you later ask "Why did we choose PostgreSQL over MongoDB?", OneBrainer doesn't just search text — it activates the relevant knowledge nodes and spreads through the connection web, surfacing facts you forgot you even discussed. It also dreams: cross-pollinating ideas between unrelated topics to suggest insights your team hasn't considered.
In short: OneBrainer is a team memory that thinks.
Dashboard — the three-layer knowledge model at a glance (L1 entries → L2 facts → L3 briefs), plus Librarian and Dreamer run state:
Brain — self-generated observations about the knowledge graph, and the gaps the brain knows it has:
More: docs/screenshots/
This is an independent R&D project, published openly so the architecture can be read, criticised and reused. It is feature-complete for the v5.2.0 scope described below, and it is not a hosted commercial service.
One thing is worth knowing before you clone it:
- Prompt injection is contained, not solved. The Librarian ingests untrusted text and
feeds it to an LLM. Every such call now fences the payload with a per-call random nonce
and validates the reply against a strict schema before anything is written
(
src/lib/llm-safety.ts), so a compromised model cannot write outside the contract. It can still influence which plausible facts get extracted — see SECURITY.md.
Running it needs one API key and nothing else. Every model call goes through
src/lib/llm-client.ts, which ships two adapters: the official Anthropic SDK, and any
OpenAI-compatible /chat/completions endpoint. The second is deliberately the wide
door — the same shape reaches OpenAI, OpenRouter, Groq, Together, vLLM and a local
Ollama, so switching model or vendor is a base URL and a model name rather than code:
# Claude
ANTHROPIC_API_KEY=sk-ant-...
# GLM 5.2 (or anything else) via OpenRouter
OPENAI_API_KEY=<openrouter key>
OPENAI_BASE_URL=https://openrouter.ai/api/v1
LLM_MODEL=z-ai/glm-5.2
# Local, free, no key
OPENAI_BASE_URL=http://localhost:11434/v1
LLM_MODEL=<your ollama model>The provider is auto-detected from whichever key is present; LLM_PROVIDER pins it.
- Architecture Overview
- The Three-Layer Knowledge Model
- Core Subsystems
- Tech Stack
- Project Structure
- Database Schema
- API Reference
- MCP (Model Context Protocol)
- Security Model
- Benchmark Harness
- GDPR Compliance
- Scheduler
- Setup & Deployment
- Environment Variables
- Development Guide
- Roadmap
┌─────────────────────────────────────────────────────────────┐
│ Client (SPA) │
│ Next.js 16 + React 19 + Tailwind 4 + shadcn/ui │
│ Tabs: Overview | Briefs | Knowledge | Ledger | Brain | │
│ Dreamer | Agents | Contest | GDPR | Connectors │
└────────────────────────┬────────────────────────────────────┘
│ REST API (51 routes)
┌────────────────────────▼────────────────────────────────────┐
│ API Layer (Next.js Route Handlers) │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │auth- │ │withHandler() │ │CORS / Rate Limiter │ │
│ │helpers.ts│ │error wrapper │ │/ Audit Logger │ │
│ └──────────┘ └──────────────┘ └────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ Core Subsystems │
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Librarian │ │ Brain │ │ Dreamer │ │ Scheduler│ │
│ │ L1 → L2 │ │ Query │ │ Sparks │ │ (croner) │ │
│ │ extraction │ │ Engine │ │ (bandit) │ │ │ │
│ └─────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌─────▼──────────────▼──────────────▼──────────────▼─────┐ │
│ │ Prisma ORM │ │
│ │ SQLite (WAL mode) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌──────────▼──────────┐
│ llm-client.ts │
│ Anthropic / OpenAI- │
│ compatible endpoint │
└─────────────────────┘
Reverse proxy: Caddy (:81 → localhost:3000). Only the Next.js app is exposed externally. No wildcard port forwarding.
OneBrainer stores knowledge in three progressively refined layers:
The immutable source of truth. Every conversation digest, decision log, or meeting note is stored here verbatim.
Ledger {
ts: string // ISO timestamp (backdateable)
agentId: string // which agent produced this
topic: string // e.g. "backend-architecture"
kind: string // "digest" | "decision-log" | "meeting-notes"
content: string // raw text
processed: boolean // has the Librarian consumed this?
}
Ledger entries are never deleted or modified — they form an append-only audit trail.
The Librarian extracts four typed objects from L1:
| Type | Purpose | Key Fields |
|---|---|---|
| Fact | Atomic knowledge nugget | entity, attribute, statement, confidence (high/medium/low), supersededBy (chain) |
| Decision | Choices the team made | decision, rationale, status, outcome, lesson |
| Preference | How the team works | scope (global/topic), statement, active |
| ProjectState | Volatile current state | topic, key, value, expiresAt |
| Object | Generator | Purpose |
|---|---|---|
| Brief | Librarian | Delta-brief per topic: active decisions + non-stale facts + project state + preferences |
| Association | Librarian + Hebbian learning | factA ↔ factB links with strength, weight, and fire count |
| Dispute | Librarian | Detected contradictions between new and existing facts |
| Spark | Dreamer | Cross-topic associative insights (analogy, contradiction, opportunity, risk, missing-link, optimization) |
| Insight | Brain | Self-generated observations about the knowledge graph (gaps, orphaned facts, high-activity clusters) |
| NeuralActivity | Brain query | Log of every fact activation during spreading, for debugging and plasticity |
File: src/lib/librarian.ts (~575 lines)
The Librarian processes unprocessed ledger entries and extracts structured knowledge.
Pipeline:
- Fetch all
processed=falseledger entries for a workspace - For each entry, call the configured LLM to extract: Facts, Decisions, Disputes, Preferences
- Auto-associate new facts with existing facts using LLM (label: supports/contradicts/extends/related/causes/requires)
- Supersede chain: if a new fact contradicts an existing one, the old fact is marked
staleand the new one becomes the head of the chain - Brief rebuild: after all extractions, rebuild the delta-brief for affected topics
- Mark ledger entries as
processed=true
Manual trigger: POST /api/librarian
Automated: Scheduler (configurable cron, default: every 4 hours)
Run log: Every Librarian execution is tracked in LibrarianRun (startedAt, endedAt, status, counts).
File: src/lib/brain-query.ts (~450 lines)
The Brain implements spreading activation over the Fact-Association graph — a simplified model of biological neural activation.
Query Pipeline:
- Keyword extraction: The query is tokenised on a strict character allow-list
(
[^a-z0-9áéíóöőúüű]+) and stop-words are dropped. - Keyword seeding: A single DB-level query finds non-stale, non-superseded facts whose
topic,entity,attributeorstatementcontains at least one keyword (capped at 200 seeds). Seeds are scored by where the match landed — topic ×3, entity ×2, attribute ×1, plus one point per statement hit — then normalised tomin(score / 10, 1.0). 2b. Semantic seeding (optional, off unlessEMBEDDING_MODELis set): the query is embedded once and compared against the stored fact vectors. Matches above a floor (0.25 cosine) are rescaled onto the same 0–1 activation scale and merged into the seed set; where both passes found the same fact, the stronger signal wins rather than the two being added together. See Semantic seeding. - Spreading activation (iterative, default 3 iterations):
- Neighbour activation +=
source_activation × association.activationWeight × 0.3(decay) - Facts and associations are lazy-loaded per iteration — only the activated neighbourhood enters memory, never the whole graph
- After each iteration activations are renormalised so the maximum stays at 1.0
- Facts below the threshold (default 0.05) are pruned from the result set
- Neighbour activation +=
- Hebbian learning: every association that fired gets
activationWeight += 0.02 × activationWeight(capped at 1.0) andfireCount + 1, applied as a single batchedCASEupdate. - Results: Top-N facts ranked by activation, each with
isSeedand a human-readablereasontrace explaining how it was reached.
API: POST /api/brain/query
{
"query": "Why did we choose PostgreSQL?",
"limit": 10,
"iterations": 3,
"activationThreshold": 0.05
}| Field | Type | Default | Notes |
|---|---|---|---|
query |
string | — | Required |
limit |
int 1–50 | 10 | Max results |
iterations |
int 1–5 | 3 | Spreading iterations |
activationThreshold |
float 0–1 | 0.05 | Prune cutoff |
Rate limited to 20 requests/minute.
Neural stats returned:
{
"neural": {
"totalActivated": 23,
"seedFacts": 4,
"spreadFacts": 19,
"associationsFired": 47,
"hebbianUpdates": 47,
"iterations": 3,
"activationThreshold": 0.05,
"lazyLoaded": true
},
"seeding": {
"strategy": "hybrid",
"keywordSeeds": 4,
"semanticSeeds": 7,
"semanticOnlySeeds": 5,
"vectorsScanned": 218
}
}seeding reports how the seed set was actually built, including
semanticOnlySeeds — the facts the keyword pass alone would have missed. Without
that number there is no way to tell a hybrid query from a hybrid query whose
embedding call quietly failed, and both would be reported as "hybrid".
Not implemented: LLM-based query expansion before seeding (see Roadmap). Query expansion and semantic seeding solve the same problem from opposite ends; embeddings turned out to be both cheaper and less blind, so expansion is unlikely to be built.
Seeding happens before spreading, and the graph cannot return what was never seeded. So the weakness of a purely lexical seed is not that it ranks badly — it is that a fact phrased differently never enters the network at all, and neither does anything associated with it. Ask about "the payment provider" and a fact that says "Barion" stays dark, along with its whole neighbourhood.
Setting EMBEDDING_MODEL gives every fact a vector, so a query can also seed by
meaning. Both passes run and are merged — keyword matches keep their weight,
because an exact shared term is the one thing embeddings are worst at.
Deliberate properties:
- Optional. Unset, the query behaves exactly as it did before. No key is needed to see the system work, and the default clone costs nothing to run.
- Cheap. Only the distilled L2 fact layer is embedded — one sentence each, once, when written. A query costs one vector, not a scan of your history.
- Local. Any OpenAI-compatible
/embeddingsendpoint serves it, including an Ollama onlocalhost. Configured separately from the completion provider, so the Librarian can run on Claude while the vectors run on your own machine. - Private. The raw ledger is never embedded. Only curated facts are.
- Reversible. Vectors are fingerprinted with their source text and model, so editing a fact or switching models re-embeds rather than silently answering from a vector for a sentence that no longer exists.
EMBEDDING_MODEL=nomic-embed-text
EMBEDDING_BASE_URL=http://localhost:11434/v1The Librarian keeps vectors current on every run. For a knowledge base that
predates the setting, backfill with POST /api/brain/embeddings and check
coverage with GET /api/brain/embeddings.
Other Brain endpoints:
GET /api/brain/graph— Export the full association graph for visualizationGET /api/brain/associations— List associations with filtersGET /api/brain/neural-stats— Aggregated neural activity statisticsGET /api/brain/insights— Self-generated observations (gaps, orphans, clusters)GET /api/brain/gaps— Knowledge gap analysisPOST /api/brain/plasticity— Manual Hebbian weight adjustment
File: src/lib/dreamer.ts (~370 lines)
The Dreamer generates novel insights by cross-pollinating knowledge between different topics using an ε-greedy bandit selection strategy.
Algorithm:
- Topic pair selection (ε-greedy, ε=0.15):
- With probability 0.15: explore a random pair (weighted by UCB1 score)
- With probability 0.85: exploit the pair with highest estimated value
- Budget: 30 pair-evaluations per run
- Topic count capped at 50 (by fact count) to avoid O(n²) explosion
- Cross-topic collision: For each selected pair (topicA, topicB), collect top facts from each, call the configured LLM to generate insights
- Spark generation: Each insight becomes a
Sparkwith:kind: analogy | contradiction | opportunity | risk | missing-link | optimizationscore: LLM-assessed relevance (0-1)- Auto-associations between cross-topic facts
- Bandit feedback loop: When users rate sparks (
POST /api/sparks/rate), the SparkWeight for that topic pair is updated (hit/miss tracking)
Manual trigger: POST /api/dreamer/run
Automated: Scheduler (configurable cron, default: daily at 3 AM)
Sparks (from Dreamer) are actionable or thought-provoking connections between topics:
- "The caching strategy used in
payment-servicecould optimize theuser-authtoken refresh pattern" (analogy) - "Topic A says we use microservices, but Topic B shows a monolith deployment config" (contradiction)
Insights (from Brain) are structural observations about the knowledge graph:
- Orphaned facts (no associations)
- High-activity clusters (facts that fire frequently)
- Knowledge gaps (topics with few facts but many ledger entries)
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.x |
| Language | TypeScript | 5.x |
| Runtime | Bun | latest |
| UI Components | shadcn/ui (New York style) | Radix primitives |
| Styling | Tailwind CSS | 4.x |
| Icons | Lucide React | 0.525+ |
| Animations | Framer Motion | 12.x |
| Database | SQLite (WAL mode) | via Prisma |
| ORM | Prisma Client | 6.x |
| Auth | NextAuth.js v4 | JWT sessions |
| State (client) | Zustand | 5.x |
| State (server) | TanStack Query | 5.x |
| Validation | Zod | 4.x |
| Forms | React Hook Form + @hookform/resolvers | 7.x |
| Tables | TanStack Table | 8.x |
| Markdown | @mdxeditor/editor, react-markdown | 3.x / 10.x |
| Scheduling | croner | 10.x |
| LLM client | src/lib/llm-client.ts |
provider-agnostic |
| LLM providers | Anthropic SDK · any OpenAI-compatible endpoint | — |
| Reverse Proxy | Caddy | — |
| Password Hashing | bcryptjs | 3.x |
onebrainer/
├── Caddyfile # Reverse proxy config (:81 → :3000)
├── .env.example # All env vars documented
├── prisma/
│ ├── schema.prisma # 24 models, full multi-tenant
│ └── seed.ts # Demo workspace + user seed
├── db/
│ └── custom.db # SQLite database (gitignored)
├── src/
│ ├── app/
│ │ ├── layout.tsx # Root layout (theme, session, fonts)
│ │ ├── page.tsx # Single-page dashboard (731 lines)
│ │ └── api/
│ │ ├── auth/ # NextAuth + register + password reset
│ │ ├── workspaces/ # CRUD
│ │ ├── ledger/ # L1 ingestion
│ │ ├── facts/ # L2 read
│ │ ├── decisions/ # L2 read + review
│ │ ├── disputes/ # List + resolve
│ │ ├── preferences/ # CRUD
│ │ ├── briefs/ # Per-topic briefs
│ │ ├── brain/ # query, graph, associations, stats, insights, gaps, plasticity
│ │ ├── librarian/ # Trigger + run log
│ │ ├── dreamer/ # Trigger
│ │ ├── sparks/ # List + rate
│ │ ├── mcp/ # Model Context Protocol endpoint
│ │ ├── agents/ # Agent key management
│ │ ├── scheduler/ # External cron tick
│ │ ├── settings/ # Workspace settings (scheduler config)
│ │ ├── stats/ # Dashboard statistics
│ │ ├── search/ # Full-text search across knowledge
│ │ ├── activity/ # Neural activity timeline
│ │ ├── user/ # Profile management
│ │ ├── gdpr/ # Privacy, consent, audit, export, erase, retention
│ │ ├── contest/ # Contests, challenges, leaderboard, achievements
│ │ ├── benchmark/ # Seed questions + run benchmark
│ │ ├── health/ # Health check
│ │ └── docs/ # API documentation endpoint
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives (40+ components)
│ │ ├── tabs/ # Dashboard tab components
│ │ │ ├── overview-tab.tsx
│ │ │ ├── briefs-tab.tsx
│ │ │ ├── knowledge-tab.tsx
│ │ │ ├── ledger-tab.tsx
│ │ │ ├── disputes-tab.tsx
│ │ │ ├── dreamer-tab.tsx
│ │ │ ├── agents-tab.tsx
│ │ │ ├── types.ts
│ │ │ └── helpers.ts
│ │ ├── brain/ # Brain visualization tab
│ │ ├── gdpr-tab.tsx
│ │ ├── contest-tab.tsx
│ │ ├── connectors-tab.tsx
│ │ ├── login-dialog.tsx
│ │ ├── profile-dialog.tsx
│ │ └── workspace-switcher.tsx
│ └── lib/
│ ├── auth.ts # NextAuth configuration (JWT, session version)
│ ├── auth-helpers.ts # requireAuth(), getWorkspaceId(), verifyWorkspaceAccess()
│ ├── api-handler.ts # withHandler() — error wrapping, 1MB body limit
│ ├── errors.ts # AppError hierarchy (Auth/Forbidden/Validation/NotFound/Conflict/RateLimit)
│ ├── cors.ts # CORS origin management
│ ├── rate-limiter.ts # IP-based in-memory rate limiter
│ ├── audit.ts # Fire-and-forget audit logging (10 event types)
│ ├── logger.ts # Structured logger (LOG_LEVEL)
│ ├── password.ts # Shared Zod password schema (Hungarian rules)
│ ├── env.ts # Startup env validation
│ ├── db.ts # Prisma client singleton
│ ├── time.ts # Canonical timestamp parsing and formatting
│ ├── sql-tables.ts # Physical table names for the raw statements
│ ├── embeddings.ts # Optional embedding provider + vector maths
│ ├── fact-vectors.ts # Vector storage, backfill, semantic seeding
│ ├── brain-query.ts # Spreading activation query engine
│ ├── brain-graph.ts # Graph export for visualization
│ ├── brain-insights.ts # Self-generated observations
│ ├── brain-stats.ts # Neural activity statistics
│ ├── brain-gaps.ts # Knowledge gap analysis
│ ├── librarian.ts # L1→L2 extraction pipeline
│ ├── dreamer.ts # ε-greedy cross-topic insight generation
│ ├── scheduler.ts # Cron scheduler (croner)
│ ├── task-lock.ts # Prevent concurrent librarian/dreamer runs
│ ├── benchmark.ts # Benchmark harness (ingest→librarian→query→judge)
│ ├── benchmark-seed.ts # 10 LongMemEval-style seed questions
│ ├── reset-tokens.ts # Password reset token management
│ ├── pagination.ts # Cursor/offset pagination helpers
│ ├── seed-workspace.ts # Workspace seeding utility
│ ├── seed-contest.ts # Contest seeding utility
│ ├── use-workspace-id.ts # Client-side workspace ID hook
│ └── utils.ts # cn() and misc utilities
└── public/ # Static assets
24 Prisma models organized into groups:
| Model | Purpose |
|---|---|
User |
Accounts with sessionVersion for session invalidation |
Workspace |
Tenant boundary — all data is scoped to a workspace |
WorkspaceMember |
User↔Workspace with RBAC role (owner/admin/member) |
WorkspaceSettings |
Per-workspace scheduler configuration |
Agent |
Machine agents with keyHash auth for MCP/programmatic access |
| Model | Layer | Purpose |
|---|---|---|
Ledger |
L1 | Append-only raw ingestion |
Fact |
L2 | Typed knowledge with supersede chains and activation tracking |
Decision |
L2 | Team decisions with calibration loop (outcome/lesson) |
Preference |
L2 | Team working preferences |
ProjectState |
L2 | Volatile state with TTL |
Dispute |
L2 | Detected contradictions (workflow object, not error) |
Brief |
L3 | Computed delta-brief per topic (marked dirty on change) |
| Model | Purpose |
|---|---|
Association |
Fact↔Fact links with Hebbian weights, fire count, labels |
NeuralActivity |
Every fact activation event (for debugging/plasticity) |
BrainQuery |
Query log with context and usefulness feedback |
Spark |
Dreamer-generated cross-topic insights |
SparkWeight |
Bandit state per topic pair (trials/hits for ε-greedy) |
Insight |
Brain-generated structural observations |
LibrarianRun |
Extraction run log |
| Model | Purpose |
|---|---|
Contest |
Competition definitions |
ContestEntry |
Workspace participation |
Challenge |
Tasks within a contest |
Achievement |
Earned badges per workspace |
| Model | Purpose |
|---|---|
Consent |
User consent records (GDPR Art. 7) |
DataExport |
Export request tracking (GDPR Art. 20) |
AuditLog |
Fire-and-forget action log (10 event types) |
All 51 API routes use structured JSON responses via withHandler():
{
"data": { ... },
"error": { "code": "VALIDATION_ERROR", "message": "...", "details": {} },
"meta": { "timestamp": "2025-01-15T10:30:00.000Z", "requestId": "m1abc2-def345" }
}| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
Public | Register new user (email/name/password) |
| POST | /api/auth/forgot-password |
Public | Request password reset email |
| POST | /api/auth/reset-password |
Public | Reset password with token |
| ALL | /api/auth/[...nextauth] |
Varies | NextAuth.js endpoints (signIn, signOut, session) |
| Method | Route | Auth | RBAC | Description |
|---|---|---|---|---|
| GET | /api/workspaces |
Required | — | List user's workspaces |
| POST | /api/workspaces |
Required | — | Create workspace |
| GET | /api/workspaces/[id] |
Required | Member | Get workspace details |
| PATCH | /api/workspaces/[id] |
Required | Owner/Admin | Update workspace |
| DELETE | /api/workspaces/[id] |
Required | Owner | Delete workspace + cascade |
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/ledger |
Required | Ingest entries (supports backdated ts) |
| GET | (via stats) | Required | Ledger entries included in workspace stats |
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/facts |
Required | List facts (filter: topic, confidence, stale) |
| GET | /api/decisions |
Required | List decisions (filter: topic, status) |
| POST | /api/decisions/review |
Required | Calibration: add outcome/lesson to a decision |
| GET | /api/disputes |
Required | List open/resolved disputes |
| POST | /api/disputes/resolve |
Required | Resolve a dispute (ruling + winner) |
| GET/POST | /api/preferences |
Required | List/create preferences |
| GET | /api/briefs |
Required | List all topic briefs |
| GET | /api/briefs/[topic] |
Required | Get delta-brief for a specific topic |
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/brain/query |
Required | Neural spreading activation query |
| GET | /api/brain/graph |
Required | Export association graph (D3/vis.js compatible) |
| GET | /api/brain/associations |
Required | List associations (filter: label, minStrength) |
| GET | /api/brain/neural-stats |
Required | Aggregated neural activity stats |
| GET | /api/brain/insights |
Required | Brain-generated observations |
| GET | /api/brain/gaps |
Required | Knowledge gap analysis |
| POST | /api/brain/plasticity |
Required | Manual Hebbian weight adjustment |
| GET | /api/brain/embeddings |
Required | Semantic seed coverage (facts vs. embedded) |
| POST | /api/brain/embeddings |
Required | Backfill missing or outdated fact vectors |
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/librarian |
Required | Trigger L1→L2 extraction |
| GET | /api/librarian-runs |
Required | List extraction run history |
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/dreamer/run |
Required | Trigger cross-topic insight generation |
| GET | /api/sparks |
Required | List sparks (filter: kind, delivered, rating) |
| POST | /api/sparks/rate |
Required | Rate a spark (1-5 → bandit feedback) |
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/mcp |
Agent keyHash | MCP discovery endpoint |
| POST | /api/mcp |
Agent keyHash | MCP tool calls (ingest, query, brief, topics) |
| Method | Route | Auth | RBAC | Description |
|---|---|---|---|---|
| GET | /api/agents |
Required | — | List workspace agents (never returns keyHash) |
| POST | /api/agents/[id]/rotate |
Required | Owner/Admin | Replace an agent's key; returns the new one once |
Agents themselves are created by seeding a workspace, each with its own randomly generated key returned once at creation. There is no create-agent or delete-agent endpoint yet — rotation is what contains a leaked key, since rotating to a key nobody holds disables the old one.
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/contest/contests |
Public | List active contests |
| POST | /api/contest/contests |
Required | Create contest (owner only) |
| GET | /api/contest/contests/[id] |
Public | Contest details |
| PATCH | /api/contest/contests/[id] |
Required | Update contest (owner only) |
| DELETE | /api/contest/contests/[id] |
Required | Delete contest (owner only) |
| GET | /api/contest/challenges |
Public | List challenges for a contest |
| POST | /api/contest/challenges |
Required | Create challenge (contest owner) |
| POST | /api/contest/enter |
Required | Enter workspace into contest |
| POST | /api/contest/score |
Required | Submit score for a challenge |
| GET | /api/contest/leaderboard |
Public | Contest leaderboard |
| GET | /api/contest/achievements |
Required | Workspace achievements |
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/gdpr/privacy |
Required | Privacy policy |
| GET/POST | /api/gdpr/consent |
Required | View/record consents |
| GET | /api/gdpr/audit |
Required | Audit log (paginated) |
| POST | /api/gdpr/export |
Required | Request data export (GDPR Art. 20) |
| POST | /api/gdpr/erase |
Required | Request account erasure (GDPR Art. 17) |
| GET | /api/gdpr/retention |
Required | Data retention policy |
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/health |
Public | Health check |
| GET | /api/stats |
Required | Workspace statistics (counts, topics, timeline) |
| GET | /api/search |
Required | Full-text search across facts, decisions, ledger |
| GET | /api/activity |
Required | Neural activity timeline |
| GET | /api/user/profile |
Required | Current user profile |
| PATCH | /api/user/profile |
Required | Update profile |
| GET | /api/settings |
Required | Workspace settings (scheduler config) |
| PUT | /api/settings |
Required | Update settings (Owner/Admin) |
| POST | /api/scheduler/tick |
Bearer token | External cron trigger |
| GET | /api/docs |
Public | API documentation |
| Method | Route | Auth | RBAC | Description |
|---|---|---|---|---|
| GET | /api/benchmark/seed |
Required | — | Get benchmark seed questions |
| POST | /api/benchmark/run |
Required | Owner/Admin | Run full benchmark pipeline |
OneBrainer exposes an MCP endpoint for AI agent integration (Claude Desktop, IDE plugins, custom agents).
Authentication: Agent keyHash — the client sends a Bearer token, the server hashes it with SHA-256 and looks it up in the Agent table.
POST /api/mcp
Authorization: Bearer <agent-api-key>
Content-Type: application/json
{
"tool": "query",
"args": { "context": "Why did we choose PostgreSQL?" }
}
Available tools:
ingest— Add entries to the ledgerquery— Neural brain querybrief— Get a topic's delta-brieftopics— List all topics in the workspace
CORS: Configured separately from API CORS via MCP_ALLOWED_ORIGINS env var. Server-to-server SSE connections (Claude Desktop) bypass CORS.
- NextAuth.js v4 with JWT sessions (not database sessions)
- Session invalidation:
User.sessionVersionis incremented on password change; JWT callback rejects stale versions - Multi-tenant isolation: Every API route uses
getWorkspaceId()→verifyWorkspaceAccess()— data is always scoped to the authenticated user's workspace - RBAC: 5 routes enforce Owner/Admin role (workspace update/delete, agent create/delete, benchmark run)
withHandler()wrapper on all 50+ routes:- Structured error responses (AppError hierarchy)
- 1MB body size limit
- Request ID generation for tracing
- Lifecycle logging
- Rate limiting: IP-based in-memory (via
x-real-ip→ rightmostx-forwarded-for)- Login: 5 attempts per 15 minutes per email
- Registration: 5 per 15 minutes per IP
- Password reset: 3 per 15 minutes per email
- General API: configurable per-route
-
Zod schemas on all request bodies/query params
-
Password policy (shared schema): min 8 chars, 1 uppercase, 1 digit, 1 special char, Hungarian uppercase support
-
SQL injection: every route goes through Prisma's parameterised query builder, with one deliberate exception.
src/lib/brain-query.tsuses$queryRawUnsafe/$executeRawUnsafein three places (seed keyword matching, batched Hebbian updates, batched activation updates), because SQLite has no efficient parameterised equivalent for a variable-lengthOR ... LIKEchain or a bulkCASEupdate.Those statements are safe today, but by construction rather than by escaping:
extractKeywords()tokenises on the allow-list[^a-z0-9áéíóöőúüű]+, so a quote character can never reach the interpolation, and every other interpolated value is a number produced internally. If you widen that regex, you introduce SQL injection. A comment marks each call site.
- API routes:
API_ALLOWED_ORIGINS(comma-separated). Dev: localhost only. Prod: same-origin by default. - MCP endpoint: Separate
MCP_ALLOWED_ORIGINSfor AI client origins. - Caddy: Single reverse proxy to Next.js only — no wildcard port forwarding.
- Fire-and-forget via
src/lib/audit.ts - 10 event types: auth.login, auth.register, auth.logout, auth.password_change, workspace.create, workspace.update, agent.create, agent.delete, gdpr.export, gdpr.erase
- Stored in
AuditLogtable with userId, IP, user agent
- Cryptographically random tokens (uuid v4)
- 1-hour expiry
- Single-use (consumed on reset)
- Tracked in
src/lib/reset-tokens.ts
- API keys are stored as SHA-256 hashes (
Agent.keyHash) - The raw key is shown only once at creation time
- Workspace isolation: agent keys are workspace-scoped
Purpose: Measure brain/query recall quality against known-answer questions.
Files: src/lib/benchmark.ts, src/lib/benchmark-seed.ts
Pipeline:
Evidence Sessions → Ledger (backdated) → Librarian (extract) → Brain Query → LLM Judge
Question Types (LongMemEval-inspired):
| Type | Tests | Description |
|---|---|---|
single_session |
4 | Facts from a single session are recalled |
multi_session |
3 | Facts spanning multiple sessions are connected |
temporal |
3 | Temporal ordering and recency are respected |
Judge: the configured LLM scores each result 0.0–1.0 based on whether the returned facts contain the expected answer, with partial credit.
Cost: depends on the configured provider and model. On a small model such as
gpt-4o-mini (extraction and judge alike) a 50-question run is roughly $0.50; a
local model via OPENAI_BASE_URL costs nothing but electricity.
Cooldown: 5 minutes per workspace between benchmark runs.
Run: POST /api/benchmark/run (Owner/Admin only)
OneBrainer implements core GDPR requirements:
| Article | Implementation |
|---|---|
| Art. 6 (Lawful basis) | Consent records per user (Consent model) |
| Art. 7 (Conditions for consent) | Explicit consent tracking with granted/revoked timestamps |
| Art. 15 (Right of access) | Audit log + data export |
| Art. 17 (Right to erasure) | /api/gdpr/erase — cascading delete of user data |
| Art. 20 (Data portability) | /api/gdpr/export — generates downloadable export |
| Art. 30 (Records of processing) | AuditLog table with IP, user agent, timestamps |
Data retention: Configurable per workspace via /api/gdpr/retention.
The scheduler automates recurring Librarian and Dreamer runs using croner.
Architecture:
- In production: croner creates native timer-based Cron jobs
- In development: timers disabled (Turbopack compatibility); use manual trigger buttons in the UI
- External tick:
POST /api/scheduler/tickwithSCHEDULER_SECRETBearer token (for cron-job.org, AWS EventBridge, etc.) - Task locking:
acquireTaskLock()/releaseTaskLock()prevents concurrent runs
Default schedules:
| Task | Cron | Description |
|---|---|---|
| Librarian | 0 */4 * * * |
Every 4 hours |
| Dreamer | 0 3 * * * |
Daily at 3 AM |
Per-workspace: Each workspace has its own enabled/disabled flags and custom cron expressions in WorkspaceSettings.
- Bun runtime (latest)
- Node.js 20+ (for Next.js)
- Caddy (for reverse proxy, optional in dev)
# 1. Clone and install
git clone <repo> && cd onebrainer
bun install
# 2. Configure environment
cp .env.example .env
# Edit .env — at minimum set DATABASE_URL
# 3. Initialize database
bun run db:push
bun run db:seed
# 4. Start development server
bun run dev
# → http://localhost:3000
# 5. (Optional) Start Caddy for production-like proxy
caddy run
# → http://localhost:81# Build
bun run build
# Run standalone server
NODE_ENV=production bun run start
# Or with Caddy
caddy run # proxies :81 → :3000bun run db:push # Push schema changes (dev)
bun run db:migrate:dev # Create migration (dev)
bun run db:migrate:deploy # Apply migrations (prod)See .env.example for the complete documented list.
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | — | SQLite connection string (file:./db/custom.db) |
LLM_PROVIDER |
No | (auto-detect) | anthropic / openai |
LLM_MODEL |
No | per provider | Overrides the provider's default model |
ANTHROPIC_API_KEY |
* | — | Selects and authenticates the Anthropic adapter |
OPENAI_API_KEY |
* | — | Selects and authenticates the OpenAI-compatible adapter |
OPENAI_BASE_URL |
No | https://api.openai.com/v1 |
Point at Groq, OpenRouter, Ollama, vLLM… |
NEXTAUTH_SECRET |
Prod | Insecure dev fallback | JWT signing secret |
NEXTAUTH_URL |
Prod | http://localhost:3000 |
Public app URL |
SCHEDULER_SECRET |
No | (disabled) | Bearer token for /api/scheduler/tick |
LOG_LEVEL |
No | info |
debug / info / warn / error |
API_ALLOWED_ORIGINS |
No | Dev: localhost only | Comma-separated CORS origins |
MCP_ALLOWED_ORIGINS |
No | Dev: localhost only | Separate CORS for MCP endpoint |
NEXT_PUBLIC_APP_URL |
No | — | Public URL for MCP discovery |
Validation: src/lib/env.ts validates at startup. Missing required vars = hard failure in production, warning in dev.
- Create
src/app/api/my-feature/route.ts - Use
withHandler()for error wrapping:import { withHandler } from '@/lib/api-handler'; import { requireAuth, getWorkspaceId } from '@/lib/auth-helpers'; export const POST = withHandler(async (req) => { const userId = await requireAuth(); const workspaceId = await getWorkspaceId(req); // ... your logic return { success: true }; });
- Add Zod validation for request bodies
- Test with
bun run lint
Every data-accessing route MUST:
const userId = await requireAuth();
const workspaceId = await getWorkspaceId(req);
// All Prisma queries include: { where: { workspaceId, ... } }Development convenience: In dev mode, getWorkspaceId() falls back to query param ?workspace=1 or header x-workspace-id: 1 without auth (for curl/Postman testing). In production, auth is always mandatory.
All errors extend AppError:
throw new ValidationError("Invalid input", { field: "email" }); // 400
throw new AuthError("Not authenticated"); // 401
throw new ForbiddenError("Insufficient permissions"); // 403
throw new NotFoundError("Workspace"); // 404
throw new ConflictError("Email already registered"); // 409
throw new RateLimitError("Too many requests"); // 429withHandler() catches these and returns structured JSON responses.
Never call a provider SDK directly — go through src/lib/llm-client.ts. It picks the
adapter, applies the per-provider quirks (Claude rejects temperature; the
OpenAI-compatible adapter needs a base URL) and keeps every SDK import lazy, which is
also what avoids the Turbopack TDZ crash that module-level imports caused here.
import { complete } from '@/lib/llm-client';
import { parseLLMJson, injectionGuard, newNonce, wrapUntrusted } from '@/lib/llm-safety';
const nonce = newNonce();
const response = await complete({
context: 'myfeature.extract', // shows up in logs
effort: 'low', // Claude reasoning depth; ignored elsewhere
temperature: 0.1, // OpenAI-compatible only (Claude rejects it)
system: `${injectionGuard(nonce)}\n\n<your instructions and output schema>`,
user: wrapUntrusted(userSuppliedText, nonce),
});
// Never JSON.parse() a model reply directly — validate against a Zod schema.
const result = parseLLMJson(response.text, MySchema, 'myfeature.extract');
if (!result) return fallback(); // malformed or hostile reply — degrade, don't throwTwo rules that are not optional: fence anything user-supplied with
wrapUntrusted() + injectionGuard(), and validate every reply with
parseLLMJson() before it touches the database. See SECURITY.md.
Adding a provider: implement one complete*() function in llm-client.ts, add it
to the LLMProvider union and the switch, and give it a default model. Nothing
outside that file needs to change.
- Use shadcn/ui components from
src/components/ui/ - Dashboard is a single-page app in
src/app/page.tsxwith tab navigation - Client state:
useWorkspaceId()hook for workspace context - Styling: Tailwind CSS 4 with
cn()utility - Light/dark mode:
next-themes - Animations: Framer Motion
Two suites, split by what they need rather than by what they cover.
bun run test # offline: no database, no network, no API key
bun run test:db # builds a throwaway SQLite file from the migrations
bun run test:all # bothEvery test is a plain script that exits non-zero on failure — no runner, no config, no dependency. Both suites run in CI on every push.
Offline (test/) covers what can be decided without a database: prompt
injection containment, MCP role gating, provider resolution, timestamp
normalisation, vector maths, and two static scans of the source tree — one that
checks every raw SQL table name against the migrations, one that fails if a key
or key hash is ever written into src/ or prisma/.
Database (test/db/) applies the migrations to a temporary file, exactly as
a fresh clone does, and runs the real engine against it. Deliberately not
prisma db push: pushing builds the shape the client expects, which is the one
shape that cannot reveal a mismatch between the schema and what a deployment
actually gets.
That distinction is not theoretical. Two blockers found on 2026-08-08 lived
precisely in it — raw SQL naming tables the migrations do not create, and a
column the client selects that no migration ever added. Both passed lint,
tsc --noEmit and next build; both broke every fresh clone. The first
assertion in test/db/brain-query.test.ts is that a query returns anything at
all, because that is the assertion that was missing.
Not covered yet: the routes themselves, over HTTP, with sessions and status
codes. The data layer beneath them is tested for tenant isolation, but a
fetch()-level test of "user A gets 403 on workspace B" needs a running server
and does not exist.
- Three-layer knowledge model (Ledger → Facts/Decisions → Briefs)
- Neural spreading activation query engine (lazy-loaded, batched Hebbian updates)
- ε-greedy Dreamer with bandit feedback loop
- MCP endpoint with agent keyHash auth
- Multi-tenant RBAC (owner/admin/member)
- GDPR compliance (consent, export, erase, audit)
- Prompt-injection containment on every LLM call (nonce fencing + schema validation)
- Provider-agnostic LLM client (Anthropic SDK · any OpenAI-compatible endpoint)
- Role-gated MCP pipeline tools (
run_dreamer,run_librarian) - Benchmark harness (LongMemEval-style)
- Contest system
- Full security audit (17 findings fixed)
- Optional fact-level embeddings for hybrid seeding (semantic + keyword)
- Backdatable ledger entries, so imported history keeps its real timeline
- Database-backed test suite that applies the migrations, not the schema
- Structured outputs on the Anthropic adapter (
output_config.format) instead of parsing JSON out of free text — the uniform text contract is what keeps the adapters interchangeable today - Route-level tests over HTTP — status codes, sessions, and a
fetch()-level tenant isolation check; the data layer under them is covered, the wire is not - Measure the hybrid seed against the keyword seed on the same question set —
seeding.semanticOnlySeedsmakes the contribution visible per query, but a published number needs a full run - Real-time WebSocket notifications
- File/document ingestion (PDF, DOCX, Markdown)
- Multi-language support (i18n)
- Team collaboration features (comments, @mentions)
- Mobile-responsive dashboard improvements
- Usage analytics and billing (Stripe integration)
MIT © 2026 Molnár Barna.
Built by Molnár Barna (@deltafly).
OneBrainer is an independent R&D project, built to answer a question that keeps coming up in practice: if an AI agent has no memory between sessions, how much of a team's reasoning quietly evaporates?
The design bet is that memory quality is decided at write time, not at read time — a curated L2 layer written by a single authority (the Librarian) beats a large pile of embeddings searched at query time. Everything else in this repo follows from that bet.
Embeddings are optional here for the same reason: they seed a query, they do not decide what is worth remembering. That decision stays at write time.
Feedback, criticism and issues are welcome.

