Path 0 needs no accounts of any kind, and is the fastest way to see the thing work. Path 2 needs no GitHub App and no
GitHub organization unless you choose them: a personal access token on one repository is
enough, and
pnpm doctor will tell you what your .dev.vars is still missing.
What you are actually running
Three programs from onesrc/:
- The MCP Worker (
src/worker.ts), a Cloudflare Worker. It serves MCP tools to Claude and serves the in-client app UI. Uses D1 for a derived content index and, in multi-tenant mode, for orgs and members. Uses KV for OAuth state. This is paths 2 and 3. - The local runtime (
src/local.ts), a Node server that offers the same content tools over a git repository on disk, with no accounts and no cloud anything. This is path 0. - The bootstrap server (
src/bootstrap.ts), a Node script you run once. It registers a GitHub App for you and scaffolds your first brain repo, then you never run it again.
- A brain, which is an ordinary git repository full of markdown, on GitHub or on your own disk. Your knowledge lives there, in Open Knowledge Format, readable and editable without any of this software. If you stop using Isomorphic, you still have a git repo full of markdown.
Path 0: a folder on your machine
http://127.0.0.1:8788/b/notes in a browser: the same
viewer and editor the connector renders inside Claude, served as an ordinary web page, over the
same tools.
Your brain is a git repository. If the folder is not one yet, pnpm try runs
git init and commits what is already there. Every write lands as a commit, which is
what gives view_activity a history and what makes an edit batch atomic. Reads come
from the working tree, so files you edit in your own editor show up on the next call
with nothing to sync. An Obsidian vault works, as does any folder of markdown.
Path 0 has no second person. There is no org model, so members, roles, invitations, and
brain sharing are not registered, and there is no authentication, which is why it binds
to 127.0.0.1. For anyone else to reach it, use path 2 or 3.
The content index is kept at .isomorphic/index.sqlite inside the brain and gitignored
for you. It is derived data and can be deleted at any time.
Path 1: local only
pnpm app:dev needs no credentials at all. It renders the actual ui:// bytes the Worker
would serve, through the official MCP AppBridge host, over fixture data. It is the right
place to work on the viewer, the editor, the tree, or the graph.
pnpm worker:dev serves real MCP at http://localhost:8787/mcp, but it needs a credential
to reach a real repository. The cheapest is a personal access token on one repo, which is
option A of path 2’s first step and takes a couple of minutes. You can then point Claude
Desktop, Claude Code, or the MCP Inspector at it. Run pnpm doctor if you are unsure what
your checkout is still missing.
Why wrangler.jsonc is generated
wrangler.jsonc is gitignored. It holds one deployment’s identity: the Worker name, the
public URL, and the Cloudflare KV and D1 resource ids. Those are per-deployment, not
per-repository, and Wrangler will not interpolate environment variables into resource
bindings, so the file is generated from wrangler.template.jsonc by pnpm setup:config.
The default profile writes obviously-fake ids. That is correct for local work:
wrangler dev and wrangler d1 migrations apply --local run against Miniflare, which
simulates KV and D1 on disk and never resolves an id against Cloudflare’s API. The ids only
have to be real in order to deploy.
pnpm setup:config --help lists every setting, where it is read from, and its default.
Path 2: single-tenant deployment
One brain, one shared bearer token, no accounts, no roles. Whoever holds the token can read and write. This is a password on a door rather than an access-control model. It is fine for one person or a handful of trusted people, and it is far less setup than path 3.2a. Reach a brain
Two ways. Take the first unless you need the second. Option A: a token (recommended). Make a repository for your brain, or pick one you already have, then create a fine-grained personal access token at github.com/settings/personal-access-tokens scoped to that one repository, with Contents: read and write and Pull requests: read and write. Put it in.dev.vars:
administration: write permission (required to create repositories) to installations on
organizations. A free org takes a minute to create at
github.com/account/organizations/new.
- The form posts a GitHub App manifest to GitHub. The App’s permissions are declared in code rather than clicked through a UI, so you get exactly the permissions listed there and nothing else. GitHub creates the App and redirects back with a one-time code.
- That code is exchanged for the App’s credentials, which are written to
.dev.vars. The private key is converted from PKCS#1 to PKCS#8 on the way in, because the JWT library Octokit uses only accepts PKCS#8 and Workers cannot do the conversion at runtime. - You install the App on your org. The post-install callback creates and scaffolds your brain repository in a single atomic commit, and records the org and installation id.
PUBLIC_BASE_URL in .dev.vars before
running bootstrap and the OAuth callback for it gets registered on the App automatically.
Otherwise only http://localhost:8787 is registered, and you add the deployed callback
later at github.com/settings/apps/<your-slug>.
If the install fails with a permissions error, you installed on a personal account rather
than an org. Bootstrap detects this and says so.
2b. Point local dev at your real brain
Add to.dev.vars, on top of whichever of 2a you did:
GITHUB_APP_INSTALLATION_ID and BRAIN_REPO_OWNER/BRAIN_REPO_NAME,
all written by bootstrap. MCP_BEARER_TOKEN is what your MCP client sends. It is separate
from the GitHub credential: it authenticates the client to you, not you to GitHub.
Then:
http://localhost:8787/mcp with
Authorization: Bearer <MCP_BEARER_TOKEN>. Confirm with whoami and list_pages before
you deploy anything. Getting started covers connecting each host.
2c. Provision Cloudflare resources and deploy
--provision creates the KV namespace and the D1 database if they do not exist, finds them
if they do, and writes their real ids into wrangler.jsonc. Then upload the secrets, which
are deliberately not in wrangler.jsonc and never committed:
If you took option A (a token), that is two secrets:
BRAIN_REPO_OWNER and BRAIN_REPO_NAME are not secret; keep them in .dev.vars for local
runs and add them to the vars block of wrangler.template.jsonc (or set them with
wrangler secret put too, which also works) for the deployed Worker.
If you took option B (an App):
https://<worker>/mcp. Hand that URL to your users;
Getting started is the page to point them at. If you put it on a
custom domain, bind the
domain in the Cloudflare dashboard, not with a routes block in the config. A routes
entry with custom_domain: true makes wrangler dev rewrite the request host, which breaks
the OAuth provider’s host-based routing and forces a comment-out dance on every local run.
The template says this too.
2d. Automate the deploy (optional)
.github/workflows/deploy.yml deploys on push to main. It needs one secret and a set of
variables, because wrangler.jsonc is not committed and has to be regenerated in CI:
Path 3: multi-tenant deployment
Orgs, roles (viewer < editor < admin < owner), a member roster with invitations, several
brains per person, and email sign-in so nobody needs a GitHub account. This is the mode the
hosted service runs in, on the same code.
Do path 2 first and confirm it works, taking option B in 2a: multi-tenant means minting
a token per organization from one App installation, which a personal access token cannot do.
Then:
3a. Sign-in
Members sign in with Auth.js and an email magic link, so they never touch GitHub. (GitHub sign-in,IDENTITY_MODE=github, was removed; a deployment still setting it is told so at
/authorize.) Two caveats, both from running it:
- Magic links are weaker than a redirect-based provider. Email prefetchers can consume a link, and cross-browser flows are fragile. A redirect-based OIDC provider (Google, your IdP) is the better primary and is on the roadmap; the Auth.js provider slot is already there.
authjs.callback-urlcookies are sticky and will silently steer a bare/auth/signinvisit somewhere unexpected. Test in a private window.
3b. Configure and deploy
AUTH_EMAIL_REPLY_TO is optional: set it to an inbox someone
reads, since the From address usually receives nothing. Magic-link sending stays inert until AUTH_RESEND_KEY is set,
so you can bring the OAuth flow up first and add email after.
Add https://brain.example.com/oauth/github/callback to your GitHub App’s callback URLs if
bootstrap did not (it only does so when PUBLIC_BASE_URL was set at the time).
AUTO_PROVISION=true gives an unrecognized signed-in user their own org on first request.
Set it to false for an invite-only instance: an unknown user gets an error instead, and
you add people with invite_member.
3c. Your own account
Auto-provision gives a first-time user a new organization with no brain in it, which is usually not what you want if you already have a brain from path 2.src/db/seed-operator-org.sql is a fill-in-the-placeholders template that makes you the owner
of an organization holding that existing brain instead.
Apply it to both the local and remote database.
3d. Bringing on another organization
Two paths, both documented indocs/ops/onboarding-a-customer-org.md:
create_org (self-serve: a hosted org on the spot, or with github: true the org installs
the App on their own GitHub org and their brains stay under their ownership) and
pnpm onboard-org (operator-driven, scripted, for a customer’s own GitHub org).
docs/ops/adding-brains.md covers adopting an existing repository as
a brain.
Operating it
Schema changes. Managed by the Wrangler migrations framework inmigrations/.
pnpm db:migrate locally, and CI applies --remote before deploying. Never run --remote
by hand from a laptop: the deploy is schema-first for a reason, and a migration applied out
of band can leave the running code ahead of or behind the schema. Migrations must stay
backward-compatible with the currently-running code for the length of a deploy window, which
means additive changes, and expand-then-contract for renames and drops. Details in
docs/ops/d1-migrations.md.
The content index. Read tools query a derived index in D1 rather than scanning GitHub.
It is a cache and never the source of truth: every read first compares the branch HEAD to the
indexed commit and reindexes what changed. So an edit made on github.com, by another agent, or
by a merged pull request is picked up on the next read with no webhook and no manual step.
Existing brains populate the index lazily on first read; there is nothing to backfill.
Product feedback (optional). submit_feedback lets your users send a bug or an idea from
inside Claude, as an issue on a GitHub tracker. It is off unless you configure a destination,
and it deliberately does not use the platform GitHub App. That App has no issues permission
and should not gain one, since widening it would expand the scope of every installation.
Instead, point it at a repository and give it a credential of its own (a fine-grained PAT with
Issues: read and write on that one repository, or a second small App):
feedback plus the kind (bug, idea, other). A fine-grained PAT
with Issues: write creates those labels on demand, so there is nothing to pre-seed; a
narrower credential files the report unlabelled rather than failing. Two things worth knowing
about the credential: the issue is authored by whoever owns it, so a PAT on your own account
makes every user’s report look like yours (a machine account or a small GitHub App avoids
that), and a PAT expires, after which reports fail with a clear error to the reporter and
silence to you. Leave both secrets unset and the tool is not registered at all, so nobody’s
reports go anywhere unexpected. Note the
destination is a public tracker in most setups: the tool always shows the user the exact
issue text and requires an explicit confirmation before posting, publishes nothing about their
account, and records the reporter privately in D1 (feedback_reports) keyed by the opaque
report id printed in the issue. A per-user cap of ten reports a day bounds abuse.
Backups. Your knowledge is in a git repository, so it is already backed up everywhere it
is cloned. The D1 database holds only derived data plus org and membership rows. Losing the
index costs a rebuild; losing the org tables costs re-inviting people. Neither loses content.
Upgrading. Pull, pnpm install, pnpm setup:config --force (in case the template
changed), pnpm db:migrate, deploy. Read the release notes for anything flagged as a change
to the Open Knowledge Format, since that affects the files in your brain rather than just the
server.
Troubleshooting
“namespace not found” or “database not found” on deploy. You deployed with the local placeholder ids. Runpnpm setup:config --provision --force.
The App cannot create repositories. It is installed on a personal account. Only
organization installations get administration: write. Reinstall on an org.
Bootstrap wrote a private key that Octokit rejects. GitHub issues PKCS#1 and the JWT
library needs PKCS#8. Bootstrap converts on write and also migrates an older .dev.vars on
every run, so re-running pnpm bootstrap usually fixes it. Never hand-edit
GITHUB_APP_PRIVATE_KEY_BASE64.
Edited .dev.vars, and wrangler dev still uses the old values. Restart it. Wrangler’s
reload does not reliably re-read every var.
The app UI does not appear in Claude. Claude sometimes does not mount the iframe even
when the protocol exchange is byte-correct. It is a host-side issue and not fixable from the
server. Isolate it by testing the same server against a different MCP host (the MCP Inspector,
or VS Code Copilot). See docs/references.md.
The first reads of a large brain are slow. The content index builds on first read, and
the work is budgeted per request and resumes where it stopped, so a brain of a few thousand
pages converges over several reads rather than in one. After that, a read is one or two D1
statements plus one check of the branch head.
A tool you wrote as a page under tools/ does not show up. The transport is stateless and
cannot push a tool-list-changed notification, so the host only sees a new, renamed, or removed
custom tool after it reconnects. Editing an existing tool’s body takes effect immediately.
Getting help
Open a discussion for questions, or an issue for a reproducible bug. Self-hosting problems are on topic, and a gap in this page is worth reporting as one. For anything security-related, seeSECURITY.md instead.