# Memrail: an agent-memory layer with per-client isolation in Postgres

> One function is allowed to do the thing the whole product forbids. find_cross_scope_neighbors carries this header verbatim in its migration: the ONLY legitimate cross-scope read in the system. Detecting a boundary violation requires crossing the boundary, so the build gave that capability exactly on

Source: https://saascode.ai/inside/road-to-memrail · Published: 2026-08-09 · Section: builds · Product: Memrail (https://saascode.ai/products/memrail)

---
One function is allowed to do the thing the whole product forbids. `find_cross_scope_neighbors` carries this header verbatim in its migration: *the ONLY legitimate cross-scope read in the system*. Detecting a boundary violation requires crossing the boundary, so the build gave that capability exactly one address — definer-privileged, service-role-only, asynchronous, and never on a request path. Memrail is agent-memory infrastructure for agencies, where client isolation runs inside the database engine. A product whose entire promise is engine-enforced isolation must answer what happens when it needs to check that isolation — and the answer here is a single, named, deliberately constrained exception, not a loophole.

The pairing from the other direction: the obvious implementation of the isolation check — a recursive query inside the row-level policy, walking parent links at query time — was evaluated and rejected. `current_scope_path()`, the helper that reads the caller's asserted scope, is documented in its own source as *"reads only a session setting → CANNOT recurse"*, and returns NULL when unset. The default state, with no scope asserted, is deny. Zero recursive queries exist in any policy in the product.

## Starting point

The build addresses a structural gap in how agencies run AI agents across clients. An agent working on one client's matter may accumulate context from another — not by intent, but because the memory store the agency uses has no concept of a client separate from the organization.

Memrail's answer starts with a definition that held throughout the build: a client is a data boundary, not a login. No client-facing portal exists in v1 or on the roadmap. The decision record states it directly — *"Collapsing client into org destroys the product."* The organization line is standard multi-tenancy; the client line is what the product is.

The mechanism chosen to enforce that line is a self-referential scope tree with a materialized path column — every memory chunk carrying a scope reference, isolation asserted by comparing path strings rather than walking the tree at query time. Two levels seed in v1, organization root to client, but the engine is depth-unlimited. A later level is a row, not a migration.

The research pass surfaced two attributed dependencies that did not hold on inspection. A retrieval module listed as reusable turned out to be a pattern from prior work rather than a built module — the entire vector layer was net-new wiring. A second dependency was cited with a lineage that did not exist; the capability was real, the project it was attributed to was fabricated.

## Reading the market

The failure class Memrail addresses has a documented presence in shipping products. An agent platform cross-tenant leak received a CVSS 8.6 advisory in April 2026 — evidence the risk is current.

The build's framing kept that evidence contained rather than foregrounded. The positioning is cost-of-failure rather than regulatory mandate: the value of a prevented cross-client leak against what a single incident costs an agency. Compliance framing appears in one product section, labeled and bounded.

No uniqueness claim appears here. Vector store isolation postures vary across vendors. What the build asserts is narrower: the boundary is the database engine, and the mechanism is deterministic.

## The decisions that shaped it

**Two isolation lines, both engine-enforced.** The organization line already existed in the platform baseline — a database-level assertion of the caller's organization identity. The client line required a second, independent enforcement predicate authored for this build: a helper function that reads a session variable containing the caller's scope path, asserted once by the API layer before any memory operation and consumed by every row-level policy on the memory chunk table. Because the helper reads a string rather than querying a table, it cannot re-enter the policy that calls it.

No raw select against the memory chunk table is permitted outside two definer functions — a similarity path for vector search and a recency-list path added when ordered access was needed separately. Five reads exist outside those paths and are documented as deliberate: three dashboard aggregates returning counts only, one single-row investigation lookup, and one sweep reading identifiers to enqueue scans.

One artifact of enforcing the audit trail: the retrieval function inserts a row recording that the read occurred, which means it cannot be declared read-only. It was re-declared as a writing function. The guarantee and the proof of the guarantee are the same function call.

**The isolation check is forbidden from walking the tree it protects.** The recursive implementation was evaluated as Option D in the decision record and rejected. A policy that queries the table it protects re-enters itself on every qualifying row. The rule this produced is absolute, written into the project's standing rules: no recursive query in any policy, ever. `current_scope_path()` is documented in source as reading only a session setting — it cannot recurse and returns NULL when unset, so the default state is deny.

**One function is allowed to do the thing the whole product forbids.** The contamination detector must compare a chunk against its nearest neighbors in other clients' scopes to find boundary violations. The build gave that capability exactly one address: `find_cross_scope_neighbors`, declared in its migration as the only legitimate cross-scope read in the system. The containment conditions are specific: SECURITY DEFINER, granted to the service role only, filtered to a different scope within the same organization, reachable only from the asynchronous consumer and the daily sweep, never from a request path.

The alternative — a detector that issues cross-scope queries ad hoc in application code — would have placed an unaudited exception in a layer the standing rules cannot reach. Naming the exception and giving it one address is what keeps the rule checkable.

**The meter runs before the spend.** The write path has a fixed, annotated order: resolve scope, meter, embed, insert, audit, enqueue scan. Metering is early by design — the source comments state the reason: *"reject over budget BEFORE any embedding spend (no OpenAI call on a rejected write)."* A write that will be refused never reaches a paid external API. The counter is a single atomic increment that returns false when the budget is exhausted. A read-modify-write pattern under concurrency is the canonical failure mode for a metered system. No concurrency test is recorded in the build log. The claim available is structural.

**The guarantee is scoped in writing.** Memrail prevents cross-client data from being returned by a Memrail read, enforced at the database engine. It does not guarantee what a consuming agent does with two clients' data in its context window. The reason this had to be explicit rather than a footnote: the half the product cannot see is the half a buyer will assume it covers.

## What fought back

**The gate read the file; the database read something else.** Two migrations insert into a `language_code` column on the translations table. The column does not exist — the table defines `locale`, with a uniqueness constraint on four fields including `locale`, and every other insert statement across more than a thousand rows uses that name. The 33 keys those two migrations attempted to insert appear nowhere in the deployed database, verified by search against the live system.

The check that should have caught this reads `.sql` files and counts a key as seeded if it appears in any of them. It never connects to a database. A key inserted into a nonexistent column is, to a file-reader, a seeded key. The gate reported zero missing keys through every subsequent pass and every build.

The method that found it was different: comparing the rendered page against the design reference in a real browser. The product's own landing headline was serving stale content in the browser while every static check was green. Static checks measure what a file says. A browser measures what the database served.

**A vocabulary with no compiler.** Four style utilities were used by built pages and defined nowhere, compiling without error and rendering nothing. The most consequential instance remains open at build close: the body font resolves to an empty value — assigned through a compile-time variable that emits no runtime class — so pages fall back to the default sans-serif. The visual review recorded the opposite; a static analyzer written after the build identified it. The visual layer has no compiler; a wrong symbol there is not an error, it is silence.

**The perimeter list and the URLs had drifted, and the verifier meant to catch it could not run.** Nine authenticated routes were placed in a folder group whose name does not appear in the request URL. The middleware guard had been written against folder names and covered only two of the eleven routes — the other nine returned 200 to an unauthenticated request. No data was exposed; the API returned 401 and row-level policies held. The failure was contained by the layer nobody was relying on for it. Separately, the deployment verification script opens with an unguarded query against a bookkeeping registry the platform's migration path does not write — it throws before reaching the account and translation checks it exists to run, and every gate criterion was confirmed by direct database query instead.

The in-app memory and search pages render an informative state for a signed-in operator rather than live data — the API authenticates by key and no session-authenticated proxy sits in front of it. Delegation revocation has no endpoint; the control is rendered disabled with an explanation. The showcase surfaces ship on their empty states: real memory chunks require real embeddings, and the provider key is configured post-deployment; seeding fabricated vectors was ruled out in writing before the situation arose. An unresolved design tension remains open: the token specification makes amber the primary interactive color, while the design guidance reserves it for the contamination-alert semantic. Applying it correctly distributes amber across interactive chrome throughout the product. The fix is identified; the operator decision is pending.

## What shipped

Memrail shipped as a complete multi-tenant build — two agency roles, an administration plane, onboarding, billing on flat subscription with in-application write metering, an audit trail with export, a machine-callable interface, and every string translated.

The differentiating layer required net-new work: a scope-tree isolation engine enforced inside the database, an asynchronous contamination detector with exactly one sanctioned cross-boundary read, a delegation handoff that produces a hashed manifest and an audit event, and per-client write budgets metered atomically.

The cross-client isolation probe ran three times — at schema deployment, at debug, and as the authoritative verification gate. A chunk written under one client's scope, queried from a second client's scope, returned zero rows; the same query from the correct scope returned it; an unknown scope path raised. The engine held every time it was measured. The build's failures were consistently in the layers above it — a guard list keyed to folder names, a gate that read files instead of the database, class names with no definitions.

## See it

[See Memrail →](https://memrail.saascode.ai)

## Related reading

- [Agent Memory Infrastructure for Agencies 2026 — A Buyer's Guide](https://saascode.ai/inside/agent-memory-infrastructure-for-agencies-2026-a-buyer-s.md)
- [Memrail vs Cognee — The Competitor Named Your Market. Here Is Where the Designs Differ.](https://saascode.ai/inside/memrail-vs-cognee-self-hosted-agent-memory-compared-own-the.md)
