Technical documentation
Company Brain Technical Documentation
Architecture, access model and integration surface for the governed knowledge base
Last updated 19 September 2026
The Company Brain keeps your operating knowledge as a governed corpus of Markdown pages — every claim sourced, every page owned, every change approved by a person — and serves it to your team over Slack and the web, and to your agents over MCP.
This page is written for the people who have to approve it: it states what the system does and shows the mechanism that enforces it. Payload examples are illustrative.
What the Company Brain is
It is a governed corpus, not a chat wrapper over your files. Each page is Markdown with frontmatter naming its type, its owner, its status and the date it was last held true. A claim in a page points at the capture it came from — a document, a ticket, a recorded interview — so an answer can cite where it got the sentence, and a reader can go and look.
---
type: topic
owner: alice
status: approved
change_policy: gate
as_of: 2026-09-02
sources:
- capture:gdrive/2026-01-15-northwind-msa
---
## Standard discount ceiling
Account managers may approve up to 12% without escalation.
Anything above 12% goes to the commercial lead named on the
account page.
Answers are produced from that corpus, never from an unconstrained crawl. Retrieval runs first under the caller's own permissions, the agent investigates within a bounded loop, and the cited paths are checked against what was actually retrieved — an answer that cannot ground its citations is refused rather than shipped.
How a page gets there
| Step | What happens |
|---|---|
| 1. Connect | Connected systems are read under scopes you choose — Drive, Gmail, Slack, Linear, Notion, Attio, Coda, GA4 and others, each with its own import scope. |
| 2. Build | A build runs on its own branch. Pages accumulate as immutable revisions against that branch; HEAD is untouched. |
| 3. Gate | A person opens the gate. Rejection is a real path, not a formality — it sends the work back rather than forward. |
| 4. Promote | A compare-and-swap merge into HEAD, with the approved manifest frozen at prepare time. |
| 5. Answer | Slack, web chat and MCP read the same HEAD under the same permission rules. |
There is no arrow from a build straight to HEAD. Every page in the live corpus was promoted by a named person, and the promotion records what they approved.
Architecture
One deploy is one brain: a single Cloudflare Worker, a Postgres database, two object-storage buckets, and the queues that carry background work. There is no separate API service, no search cluster, and no state held in a Worker's memory between requests.
Neon Postgres 18 Workers AI
pgvector, via Hyperdrive embeddings, doc conversion
query caching off
\ /
\ /
R2 object storage ----- Brain Worker -------- AI Gateway
documents, exports SvelteKit every model call routes here
jurisdiction: eu smart placement
/ \
/ \
Workers KV 14 queues + cron
OAuth protocol state only answers, builds, ingest
no corpus, no grants one sweep a minute
The system of record and the search index are the same database. There is no window in which they disagree.
Four decisions worth the scrutiny
Embeddings are a column, not a service. Vectors live in a pgvector column beside the chunk, so an embedding commits with its revision and can never describe a revision that was rolled back. The access predicate sits inside the candidate query rather than filtering results afterwards — an unauthorised chunk never competes for a slot, so it cannot push an authorised one out of the top of the ranking.
Postgres is the only state. The build driver is jobs plus cron plus queue. A Worker evicted mid-build, a redeploy, a cancelled job — each costs one tick, not a build, because nothing in flight lived in a Worker's memory.
Chunks are content-addressed. A chunk's identity is the hash of its heading path and body, so a rebuild producing identical prose reuses the chunk and its embedding. Reruns are cheap and diffs are honest about what actually changed.
Retrieval is hybrid, then fused. A semantic arm over pgvector and a lexical arm over Postgres full-text search each return candidates under the caller's permissions; reciprocal-rank fusion merges them. One database, one query plan, one authorisation rule.
Deployment models
Two ways to run it, and the software is identical in both. What differs is custody: whose Cloudflare account it runs in, whose database holds the corpus, and who holds the keys.
A dedicated, isolated instance, operated by us — the default
How an engagement starts unless you ask otherwise. Your brain gets its own Worker, its own Postgres database, its own object storage and its own hostname, provisioned and retired as a single boundary. Nothing is shared with another client — not a database, not a schema, not a row, not a bucket, not a search index. There is no multi-tenant mode to opt out of, because there is no multi-tenant mode.
We provision the instance, apply migrations on release and keep it current. You hold your own domain, your own integration secrets — entered in the app and written straight to secrets we cannot read back — and, if you want them, your own model provider keys.
The same instance, in your own cloud
Where your policy requires the infrastructure inside your own boundary. If a regulator, a security policy or procurement requires it, the same deploy runs in your own Cloudflare account, against your own Postgres project and your own buckets. You hold the account, the data and the keys; we hand over the deployment and the release process.
It is the same codebase and the same release, not a reduced edition — the difference is who the resources belong to and who runs the deploy. In practice it asks for an infrastructure owner on your side who is comfortable with Cloudflare Workers and running database migrations on release.
What actually differs
| What | Dedicated instance (default) | In your own cloud |
|---|---|---|
| Cloudflare account | Ours, one boundary per client | Yours |
| Postgres project | Ours, dedicated to you | Yours |
| Object storage | Ours, dedicated, EU jurisdiction | Yours, EU jurisdiction |
| Model provider keys | Ours, or yours through the gateway | Yours |
| Direct database access | Our operations team | Yours |
| Migrations and releases | We run them | You run them |
| Who needs an infra owner | Nobody on your side | One engineer on your side |
| Integration secrets | Entered by your admins in the app, written straight to deployment secrets — never stored in the database, never readable back | Same |
| Git mirror target | A private repository in your own GitHub organisation | Same |
| Hostname | Your own domain | Same |
Isolation is not a tier you pay up for. Both models are a single-client deploy at its own hostname against its own database — there is no configuration in which one client's brain can read another's.
The MCP server
Agents reach the corpus over the Model Context Protocol. There are two entry points on one dispatcher.
CALLER ONE DISPATCHER TOOLS THEY SEE
/api/mcp 9 read tools
a person, OAuth 2.1 token \ / a signed-in person
\ /
One catalogue -------------- 2 ask and await
every tool defined once a question credential
/ visibility is a projection
/api/build/mcp / of that one table \
a service, scoped bearer / \ 21 read and write
bound to one build branch a build credential
Authorisation is a projection of one table, not three code paths. A tool a caller may not use is reported as an unknown tool — there is no second answer that confirms it exists.
What a person's agent can do. Find a page, read a page, search the corpus, read the current value of a tracked figure, walk a page's relationships, and list, search, read and tag source documents. Reads run under that person's own grants: connecting an agent never widens what its human can see.
What a build credential can do. Write pages, record captures and registry entries, and read its own branch — all under the same guards as the app, all onto a branch that still has to pass a human gate. The credential is issued for one build, carries a short lifetime, and is revoked when that build terminates. Its branch is resolved from the credential itself and can never be set as a parameter.
Authorising a client. The deploy is its own OAuth 2.1 issuer and publishes the standard discovery documents, so a compliant client configures itself from the hostname alone. PKCE is mandatory and checked twice. Access tokens are opaque rather than JWTs, so revoking one takes effect immediately instead of at expiry. Only an active member of the team can complete the consent screen — a valid client and a valid login are not enough if the person is no longer a member.
Any MCP client that speaks Streamable HTTP and OAuth 2.1 will connect.
One call, end to end
POST /api/mcp HTTP/1.1
Authorization: Bearer <access token>
MCP-Protocol-Version: 2025-06-18
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "read_page",
"arguments": { "path": "topics/pricing/discount-policy.md" }
}
}
HTTP/1.1 200 OK
MCP-Protocol-Version: 2025-06-18
x-request-id: 9c1f4e2a-3b77-4d10-8f5c-2a6e9d0b41ff
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"structuredContent": {
"path": "topics/pricing/discount-policy.md",
"owner": "alice",
"status": "approved",
"as_of": "2026-09-02",
"sources": ["gdrive/2026-01-15-northwind-msa"]
}
}
}
Every response carries x-request-id. One id is a complete support request.
Transport and protocol
Streamable HTTP, JSON only, stateless. There is no SSE stream and no session id to carry: each POST is a complete JSON-RPC 2.0 message, and a tool can be called without a prior initialize. This is the section to read before you write the client, because most integration surprises are here.
| Request | Result | Why |
|---|---|---|
POST /api/mcp |
200, JSON-RPC |
One message per request. Batches are refused. |
GET /api/mcp |
405, Allow: POST |
No SSE stream to open. |
DELETE /api/mcp |
405, Allow: POST |
No session to tear down. |
OPTIONS /api/mcp |
204, CORS |
Credentials are never reflected. |
| Unsupported protocol version | 400 |
Checked before authentication. |
Disallowed Origin |
403 |
Checked before CORS and before authentication. |
| A notification (no id) | 202, empty |
Nothing to answer. |
Supported protocol versions are 2025-11-25, 2025-06-18 (the default when the header is absent) and 2025-03-26. Send MCP-Protocol-Version and it is echoed on every response, errors included.
The refusal you will hit first
curl -i -H "Authorization: Bearer $TOKEN" https://brain.example.com/api/mcp
HTTP/1.1 405 Method Not Allowed
Allow: POST
MCP-Protocol-Version: 2025-06-18
x-request-id: 2b90d7c4-15ae-4a63-9f02-7c1d55e3a880
If your client insists on an SSE endpoint, point it at the same URL with POST — there is nothing else to configure.
Rate limits and errors
Read tools are capped per principal in a fixed one-minute window — page reads and lookups at the high end, searches and state reads in the middle, filing a question at the low end. Exceeding a cap returns 429 with Retry-After and a JSON-RPC error body, so retry logic can be exactly right rather than approximate. Error bodies carry a code, a message and the request id, and never a stack, a query, a path or an internal identifier.
Who can see what
Two sentences carry this section. An owner or admin is an administrative role and confers no access to content by itself. And resolution is the maximum over matching grants, with no deny rules — because a deny rule makes effective permission non-monotonic, and a permission system nobody can reason about is one nobody can review.
A grant is a subject — a person, a service, or a group — plus a scope, plus one permission from an ordered set: read, then suggest, then approve. A scope is either a path form or a category, never both.
| Form | Example | Matches |
|---|---|---|
| Exact | topics/pricing/discount-policy.md |
That page only |
| Directory | topics/ |
Everything beneath it, recursively |
| Children | topics/pricing/* |
Direct children only, no deeper |
The grammar is enforced by a constraint in the database, so a grant that cannot be parsed cannot exist as a row in the first place.
Sensitivity changes which grants reach a page
Pages carry a sensitivity tier. A broad prefix grant reaches ordinary pages but not restricted ones — reaching a restricted page requires someone to have named that page, or its category, deliberately. The highest tier is reachable by no grant at all.
Worked example. Alice holds three grants. The page she is asking for is topics/pricing/discount-policy.md, and it is marked restricted.
| Her grant | Matches the path? | Reaches restricted? | Contributes |
|---|---|---|---|
topics/ → read |
yes | no — prefix grant | — |
topics/pricing/* → suggest |
yes | no — prefix grant | — |
topics/pricing/discount-policy.md → approve |
yes | yes — named exactly | approve |
Effective permission: approve. Remove the third grant and she has nothing on this page — not read, not a preview, not a title in a list.
The degenerate case is the safe case: a caller with no matching grants produces a query that selects nothing, rather than a query missing its filter. Grants are re-read on every call and never cached, so a revocation takes effect on the next request rather than eventually.
Paths are rejected, not repaired
A page path that is not in canonical form — a homoglyph, a zero-width character, a bidirectional override, unexpected case — is refused rather than quietly normalised. A normaliser that folds is a normaliser that can be made to collide, and a collision in a path is a collision in an access rule.
A draft is not visible just because its path is
Access to a path never reveals that an unapproved draft exists against it. A request to review a build you are not part of returns exactly the same response as a build that does not exist.
Where your data lives
The corpus, its history and its embeddings sit in one Postgres database in the EU. Documents and other bulky artefacts sit in object storage pinned to the EU jurisdiction, which is a property of the bucket rather than a preference expressed in code. Embeddings are computed on Cloudflare's own inference platform, so page content is not sent to a third-party embedding provider.
| Extraction policy | Behaviour |
|---|---|
eu |
Only converters inside the policy may process a document. One that is not available in policy fails the job with a named error — the document is not quietly processed somewhere else. |
global |
The full converter set is available. The region each document was processed in is recorded either way. |
The region a document's bytes were actually processed in is written onto the extraction record. The policy is auditable after the fact, not merely configured up front.
What the system refuses to hold
Credentials, payment instruments and bank identifiers are detected on the single write path through which all immutable content enters, and refused. A matched value never enters an error message, a log line or an audit record — the audit trail records that a refusal happened, not what triggered it. In extracted documents the same categories are masked in place rather than rejecting the whole file.
Where credentials sit
Integration secrets your admins enter in the app are written straight to deployment secrets through Cloudflare's API and are never readable back; the audit record names the setting, never the value. Credentials for connected systems are stored encrypted with a per-record nonce and a key version. Plaintext never enters logs, audit payloads, job payloads or queue messages — a queue message carries a job identifier and nothing else.
HTTP surface
Everything reachable from outside the browser session, in one table.
| Method | Path | Authentication | Notes |
|---|---|---|---|
GET |
/api/health |
None | Liveness. Opens no database connection by design. |
POST |
/api/mcp |
OAuth 2.1 access token | The person door. Rate limited per principal. |
POST |
/api/build/mcp |
Service credential | The build door. No OAuth, no discovery. |
GET |
/api/kit/export |
Dedicated export credential | The whole corpus as a ZIP of Markdown. |
GET |
/.well-known/oauth-* |
None | Authorisation-server and protected-resource metadata. |
GET, POST |
/oauth/authorize, /oauth/token, /oauth/register |
Per the OAuth 2.1 flow | PKCE required. Single scope. |
POST |
/api/hooks/github |
Signed by the GitHub App | Inbound pushes are acknowledged and ignored. |
POST |
/api/slack/events, /api/slack/interactions |
Slack request signature | Five-minute replay window. |
Everything else the application serves is behind a browser session and is not part of the integration surface.
Getting your corpus out
Two exits, both live in either deployment model. A one-way mirror writes the corpus as Markdown into a private repository in your own GitHub organisation — the repository is an output, so editing it does not edit the brain. And the export endpoint above returns the same materialised tree as a ZIP on demand. What you get back is Markdown a person can read without this product.
Those exports are portability, not backup. Durability is the database's own point-in-time restore and the object store's versioning — different tools, and neither substitutes for the other.