Ten patches and a Proxy: taking gbrain multi-tenant

Field notes from carrying a single-user second brain onto phones: unforked, ten patches.

A billion people, one at a time

gbrain states its ambition without blinking, in the instructions Garry Tan wrote for his own coding agents: become the next Postgres for memory, built to serve a billion people. It might. He released it under MIT in April 2026 and trusts it with his own life: the production brain behind his agents holds over 140,000 pages. The design is superb. It also takes one thing for granted: those billion people arrive one install at a time. One machine, one operator at the keyboard with git and API keys, and a box that never sleeps. Even its newest shape, the company brain, keeps the assumption: a whole team shares one install, sliced by login, and an operator still runs it.

Memini is a bet on the other reading of that ambition: the same engine, unforked, one shared deployment built to serve many people at once, on their phones, none of whom will ever see a terminal. Six weeks, one developer, ten patches. These are the field notes, and they have one theme: nothing in gbrain was broken, everything was assumed, and the whole job was to carry those assumptions into a multi-user world without breaking any of them.

What gbrain is

A knowledge base that self-wires a typed knowledge graph on every write, with zero LLM calls on the write path; hybrid retrieval with reranking; a think mode that answers with citations and an explicit list of what the brain does not know yet; and a 24/7 dream cycle enriching everything overnight.

I use models all day, and every conversation ends in amnesia. gbrain cures that, provided you are the person at that keyboard. I wanted the same cure for people whose only computer is already in their pocket. So, Memini: Latin for “I remember”.

The move

The first decision was what not to do: no fork. gbrain went from v0 to v0.42 in its first quarter; a fork of a project moving that fast is a graveyard with extra steps. Instead, gbrain runs inside our backend as an in-process library, vendored at one exact commit, with ten patches a postinstall script reapplies on every install. Two properties of gbrain made that possible: it already ships a first-class Postgres engine, and its contract-first core makes the operations we need importable functions rather than CLI behaviors.

What upstream assumes is a home. Your knowledge lives in a git repo of markdown files, and config and schema packs live on disk. Our home is a cloud box whose filesystem vanishes on every deploy, built for thousands of mutually untrusting users.

One tenant context, three execution paths iOS app SwiftUI Android app Compose Cloudflare NestJS backend on Bun (Railway) HTTP request queue worker cron job Tenant context userId in async-local storage One transaction per call set_config(tenant), transaction-local + FORCE RLS RLS Proxy begin → savepoint, reserve → blocked gbrain (in-process library) pinned SHA + 10 postinstall patches Dream cycle fleet per user, tier-gated, daily spend caps External AI providers OpenRouter, Novita, OpenAI masked text; media to transcribe Redis locks, spend caps, pub/sub Postgres: system accounts, subscriptions Postgres: vault encrypted PII values Postgres: memory brain tables, halfvec HNSW FORCE row-level security

The lynchpin: one tenant, one transaction

Everything multi-tenant in Memini reduces to one mechanism. Postgres row-level security does the isolation, with FORCE applied so even the table owner cannot bypass it. gbrain met us halfway: its tables are already keyed by source_id, because upstream is multi-source by design. One user became one source. The policies read the tenant from a session setting, and we set that value with the transaction-local flavor of set_config: Postgres discards it at COMMIT or ROLLBACK automatically. On a pooled connection this is the difference between “we remembered to clean up” and “there is nothing to clean up”. A call with no user pins an empty sentinel that resolves to NULL, and NULL matches zero rows. Fail closed.

One mechanism serves three execution models: on an HTTP request the auth guard drops the user id into async-local storage, a queue worker sets the same context before touching data, and a cron job pins the context per user before doing that user’s work. The one legitimate exception, nightly maintenance that spans all users, goes through SECURITY DEFINER procedures with deterministic predicates, so cross-tenant power lives in code the migrations own, not in the application.

gbrain’s engine methods run their own SQL, and inside our pinned transaction two habits of its SQL client turn dangerous. begin() does not exist on a transaction context, because nesting happens through savepoints, so any engine method that opens its own transaction would throw. Worse, reserve() checks out a fresh pool connection that carries no tenant setting at all: silent empty reads, or a write into nobody’s data. So every gbrain call is wrapped, at a single chokepoint, in a Proxy over the sql object:

// condensed; runs inside a transaction that already pinned the tenant
get: (target, prop) => {
  if (prop === 'begin')
    return (fn) => target.savepoint((inner) => fn(wrap(inner)))
  if (prop === 'reserve')
    return () => { throw new GbrainRlsEscapeError('sql.reserve()') }
  return Reflect.get(target, prop)
}

begin becomes savepoint, recursively, so the redirect holds at any nesting depth. reserve throws a typed error instead of leaking. Because the wrap covers the whole object rather than a list of method names, an engine method added upstream next month is covered by construction. And because “by construction” deserves distrust, a build-time check scans the vendored engine source on every dependency bump. If a future gbrain version reserves its own connections somewhere new, our build fails. Production does not.

The ten patches

None of the ten patches fixes a bug. The cleanest example announced itself at three in the morning, when every user’s dream cycle failed on the same line of code. Before dreaming, gbrain takes a lock, and the lock id must be a short lowercase slug, because upstream sources have names like wiki or essays. Our source ids are user UUIDs, 36 characters of hex and hyphens. The id plays two roles: the data key that every query filters on, which accepted UUIDs happily, and the lock id, which did not. Every cycle died before its first phase, and every user woke up to a brain that had learned nothing. The patch splits the two roles at the lock, and only there:

try {
  assertValidSourceId(sourceId)  // a real slug passes through verbatim
  return `${LEGACY_CYCLE_LOCK_ID}:${sourceId}`
} catch {
  const token = createHash('sha256').update(sourceId).digest('hex').slice(0, 32)
  return `${LEGACY_CYCLE_LOCK_ID}:${token}`  // lock id only; the data key stays the UUID
}

The upstream validator remains the hard guard everywhere it was designed to guard, and slugs behave exactly as before. That became the pattern for all ten patches: never widen a contract when you can derive a value that satisfies it. Nobody dreamed that night. Everybody dreamed the next.

Two patches carry the product story. gbrain’s type taxonomy comes from schema packs, YAML files resolved from disk. Memini users calibrate their brain during onboarding: they pick the entity types their life actually contains and can add a few of their own. The compiled pack cannot live on our disk, so it lives in the engine’s own config table and materializes to the ephemeral filesystem on demand. And since fact extraction upstream gates on a hardcoded allowlist of canonical types, we flipped it to a denylist of structural ones, so user-defined types produce facts like first-class citizens.

The rest, in one breath: re-exports so the library embeds cleanly, a provider recipe so the nightly LLM traffic goes only to a directly contracted no-train provider, a cost cap taught not to hard-fail on models it cannot price, resume checkpoints made opt-in because what is sane on your own laptop is a liability on a shared box, a synthesis phase taught to group memories by embedding similarity because the field upstream groups them by is not written by anything yet, and generated type declarations so strict TypeScript compiles at all. The tenth patch was about bytes, and it earned its own section: what the database says at scale.

The brain at night

Upstream, the dream cycle is a cron on your machine: gbrain dream, one brain, your own API bill. Multiply by every user and the night becomes an orchestration problem. A queue fans the cycle out per user. Phases are gated by subscription tier: everyone gets extraction, and the synthesis phases that spend serious tokens light up on paid plans. A Redis counter enforces a per-day spend ceiling, so one runaway night cannot eat a month of margin. Watermark cursors let a cycle killed mid-drain resume where it stopped. And the completion verdict is written only when the cycle actually ran: a failed night must not stamp the day as done, or the retry window silently closes and a user’s brain quietly stops learning. That is how multi-tenant failure sounds: nothing crashes in front of you, it fails for everyone else, at night, politely.

What the database says at scale

We measured before launch, on test corpora, with a caveat attached: doc dumps are not representative of personal notes. The finding held anyway: the database is not dominated by user text. Chunks and facts together were 76% of it, because every chunk and every fact carries an embedding, and every embedding is stored twice: once in the row, once inside the HNSW graph that makes vector search fast.

That made half-precision the biggest lever. Switching embeddings from vector to halfvec halved the vector bytes with no re-embedding and no measurable recall loss: about 16% of the whole database and 19% of the hot graph RAM back, for one small patch. The patch exists because of a Postgres subtlety: CREATE INDEX validates the operator class against the column type before IF NOT EXISTS short-circuits on the name, so the upstream DDL errors on a halfvec column even when the index already exists.

Today’s honest design: one global HNSW per vector column serves all tenants, and isolation is a row-level-security post-filter, not graph pruning. That is the right first architecture, and it has a ceiling. We wrote the ceiling down as three walls on the way to 100,000 users: Postgres connections, worker concurrency for the nightly fleet, and RAM for the hot graphs. The plan for the third wall: partition chunks and facts per tenant, and load a user’s graph when they show up, evicting it after inactivity. We are deliberately not building that yet. The capacity math depends on how real people’s notes behave. First a real cohort, then partitions.

The build, in numbers

Around the engine sits the surface a phone expects: Google and Apple sign-in, chat streamed over SSE where a dropped connection replays the finished answer instead of billing a second generation, push notifications, subscriptions, and voice capture with per-tier quotas. Before a note’s text leaves for an external model, the pipeline detects the identifiers it can recognize by shape: emails, phone numbers, card and bank numbers, US Social Security numbers, IP addresses. Those become placeholders, and the raw values live in a separate encrypted vault, isolated per user. Names and running prose still travel, because that is what makes an answer useful. So does a photo or a voice note on its way to be turned into text, since there is nothing to mask until the text exists. The honest framing is a direction: mask more, miss less, over time.

First commit on May 25, 2026, seven weeks after gbrain went public. Forty days later: over 800 commits by one person, about 95,000 lines of TypeScript, and the iOS and Android apps growing in the same window. Most of the code was written with Claude Code. The architecture, the tenancy model, and all ten patch decisions are human; what made the speed safe was discipline: the pinned dependency, the loud guards, the build-time contract, tests against a real Postgres.

What we would offer upstream

None of this asks gbrain to become multi-tenant. That is our problem, and frankly, our product. But some of it would serve gbrain in its own single-user world, and every patch was kept mechanical and re-appliable precisely so that upstream stays upstream:

Three more are conversations rather than pull requests: the slug-legal lock token, finishing the per-source schema pack override, and the embedding-similarity grouping that stands in until atoms carry their planned concept refs. If any of this is useful, the patches are a message away: gbrain on GitHub.

Where this goes

Memini is in public beta on iOS today, with Android in closed testing right behind it: meminiai.com. The near-term engineering is the third wall. If long-term memory, tenant isolation, or the odd beauty of running someone’s single-user masterpiece for thousands of people sounds like your kind of problem, my inbox is open: [email protected].

For the other kind of reader: Memini is a subscription product whose unit economics are enforced in code, built by one person in six weeks on infrastructure that is measured rather than imagined. Happy to share the numbers: same inbox.

And a last line for Garry, if this reaches him: gbrain is the most thoughtful memory system I have read, and Memini exists because you put it in the world under MIT. Thank you. The ten patches are yours for the asking.

Memini is an independent product, not affiliated with, endorsed by, or sponsored by Garry Tan or Y Combinator. gbrain is open-source software used under the MIT license.