How WeaveForge is put together

A map of the whole thing: where the code lives, what each surface can do, and which of those things still work with the network unplugged.

It is written for somebody deciding where a change belongs, and for anybody who wants to know what runs on their machine and what does not.

The shape of it

One codebase, four surfaces, one schema.

  • packages/core — the rules. Domain types and use cases with no framework and no database in them: citation keys, LaTeX section trees, the paste pipeline, the search ranking. Everything else depends on this, and it depends on nothing.
  • apps/web — the app. Screens, features, API routes, and the wiring that chooses which backend is behind them. The browser runs this; so does the desktop window, unchanged.
  • apps/desktop — the shell. Only what a window can do and a tab cannot: the keychain, a folder on your disk, a local database, a loopback API, the menu, updates.
  • apps/pitch — the public site, including this page.
  • python/weaveforge — the SDK training scripts import.
  • supabase/migrations — the schema, applied in the same order to a hosted database and to the one in your app's folder. There is no second schema.

What runs where

Three ways to run it, and they are not three versions. The same feature code sits above every one of them; what changes underneath is the database client, the identity, the auth service and the blob store — four things, swapped at startup.

In a browser Desktop, signed in Desktop, no account
Where data lives Hosted Postgres Hosted Postgres PGlite, in the app's folder
Who you are Your account Your account One local user
Attachments Object storage Object storage A local table
Integration tokens Sealed with a server key Sealed with a server key This computer's keychain
Needs a network Yes Yes No

Offline, precisely

"Works offline" is a claim worth being exact about. A copy with no account makes no outbound request at boot, and these work with the network unplugged:

Feature Offline How
Projects, notes, papers, lists, reports Yes Ordinary rows in the local database
The editor, including typing in a document Yes The CRDT log is still written; the sync channel is not opened, because there is nobody to reach
The workspace folder mirror Yes The shell reads and writes the folder directly
The workspace editor — panes, explorer, quick open Yes Pane layout, tree and fuzzy match are client logic; the documents are rows in the local database
Experiments, and training scripts logging into them Yes The loopback API answers the SDK's own routes
The MCP server Yes Same database, same rows
Reading a PDF — outline, citation links, figure links Yes Parsed from the text layer on the client; the reference list and in-text mentions are found by core's parsers
The citation popover's record — title, venue, DOI No Looked up on arXiv, Crossref, Semantic Scholar and OpenAlex, then cached for thirty days keyed by the text's fingerprint
Linking and editing an Overleaf report Yes The token sits in the keychain; the row is an ordinary row
Opening an Overleaf report's contents No It is a clone from overleaf.com — offline means no server of ours, not no Overleaf
Attachments logged from a training script No Blobs, and the loopback API only runs SQL
Sharing, supervision, anything with a second person No Account features

Why the editor needed a fix rather than a flag

The editor opens a realtime channel to carry other people's keystrokes. That channel is built from configuration compiled into the app, not from whichever database the app is talking to — so a copy with no account opened it anyway, failed on every keystroke, and put a "live sync unavailable" notice under a document nobody else can open. Now the provider takes a live flag: solo means no transport, not no history. The document is still recorded; nothing is sent.

Why Overleaf needed a keychain

Linking an Overleaf report does three jobs, and only one of them needs a server. The rows are ordinary rows. The LaTeX parse is a pure function in core. Only holding the token needs somewhere safe — and on a server that is a key the browser never sees. The equivalent this machine has is its own keychain, so the token is kept there, the clone happens in the shell, and the page never holds the credential either way. The clone code is not duplicated: the shell imports the app's own reader.

How big it is

Counted from the files git is tracking, not remembered — run npm run docs:generate to refresh it.

Where Size What lives there
packages/core 48,362 lines Domain and application logic shared by every surface
apps/web 150,449 lines The app itself: screens, features, API routes, backend wiring
apps/desktop 10,869 lines The Electron shell — what only an installed app can do
apps/pitch 803 lines The public site and this documentation
python/weaveforge 3,196 lines The SDK training scripts import
python/tests 1,523 lines Its tests
supabase/migrations 132 files The schema, as an ordered sequence
docs 87 files Documentation, this page included

215,202 lines of code in all, across 1,884 source files.

The IPC surface

Everything the desktop shell can do that a tab cannot goes through one of these, named once in apps/desktop/src/channels.ts and typed once in desktop-bridge.ts. Two are pushed the other way — main to renderer — because they are events nobody asked for: a sign-in finishing, and somebody else changing the folder.

37 named channels, reached through the 38 members of the bridge object the preload exposes:

  • weaveforge:fetch-titlefetchTitle
  • weaveforge:fetch-imagefetchImage
  • weaveforge:sign-insignIn
  • weaveforge:check-updatecheckUpdate
  • weaveforge:secret-readsecretRead
  • weaveforge:secret-writesecretWrite
  • weaveforge:secret-clearsecretClear
  • weaveforge:preference-readpreferenceRead
  • weaveforge:preference-writepreferenceWrite
  • weaveforge:db-querydbQuery
  • weaveforge:db-statedbState
  • weaveforge:db-resetdbReset
  • weaveforge:vault-choosevaultChoose
  • weaveforge:vault-rootvaultRoot
  • weaveforge:vault-forgetvaultForget
  • weaveforge:vault-readvaultRead
  • weaveforge:vault-writevaultWrite
  • weaveforge:vault-read-bytesvaultReadBytes
  • weaveforge:vault-write-bytesvaultWriteBytes
  • weaveforge:vault-listvaultList
  • weaveforge:vault-statvaultStat
  • weaveforge:vault-removevaultRemove
  • weaveforge:vault-commitvaultCommit
  • weaveforge:local-api-statelocalApiState
  • weaveforge:zotero-localzoteroLocal
  • weaveforge:semantic-ranksemanticRank
  • weaveforge:semantic-rankedsemanticRanked
  • weaveforge:tex-probetexProbe
  • weaveforge:tex-compiletexCompile
  • weaveforge:local-api-setlocalApiSet
  • weaveforge:vault-changedvaultChanged
  • weaveforge:overleaf-readoverleafRead
  • weaveforge:ink-availableinkAvailable
  • weaveforge:ink-recogniseinkRecognise
  • weaveforge:ink-haptics-availableinkHapticsAvailable
  • weaveforge:ink-hapticsinkHaptics
  • weaveforge:window-focuswindowFocus

The number is deliberately visible. A codebase this size is a cost, and a figure that only ever goes up is a figure somebody should be arguing with.

Where to put a change

  • A rule that would be true in any client — packages/core.
  • A screen, or a rule about how one behaves — apps/web/src/features.
  • Something only an installed window can do — apps/desktop/src, exposed over a named channel in channels.ts and typed once in desktop-bridge.ts.
  • A column — a new migration in supabase/migrations. Never an edit to an old one; they have already run on other people's machines.