Feather DB Namespace Isolation: Multi-Tenant Memory Architecture
Feather DB namespaces give you per-tenant memory isolation inside a single .feather file. v0.15.3's adaptive capacity shrinks a 19-namespace deployment from 709 MB to 92 MB — a 7.7× reduction. Here's how to design multi-tenant systems that actually scale.
The multi-tenant memory problem
Multi-tenant AI systems have a specific isolation requirement that generic vector stores don't enforce well: User A's memories must be completely invisible to User B's searches — not just filtered out, but never traversed at all. At 10,000 users, a filter-after-scan approach is a performance disaster. You need true graph partitioning, not a WHERE clause bolted onto a global index.
Feather DB namespaces are that partitioning. Each namespace is a named scope with its own HNSW subgraph inside a single .feather file. Search in namespace user_123 traverses only that subgraph. It doesn't know the other namespaces exist.
What a namespace actually is
A namespace is a per-modality named scope within a single .feather file. Every modality (text, visual, audio) can have its own namespace set. The namespace is passed as a string at add time and at search time — that's the entire API surface.
import feather_db as fdb
db = fdb.DB.open("saas.feather", dim=768)
# Two users, one file, complete isolation
db.add(embed("Alice prefers dark mode."),
text="Alice prefers dark mode.",
namespace="user_alice")
db.add(embed("Bob is building a payments API."),
text="Bob is building a payments API.",
namespace="user_bob")
# Search for Alice — Bob's data is never traversed
results = db.search(embed("What UI does this user prefer?"),
k=5,
namespace="user_alice")
Under the hood, Feather maintains a per-namespace HNSW subgraph. Search latency for namespace user_alice scales with the number of memories Alice has — not with the total count across all namespaces. Adding 10,000 users does not slow down any individual user's search.
The v0.15.3 adaptive capacity win
Before v0.15.3, every namespace was pre-allocated with capacity for 1,000,000 elements. That made the math brutal for multi-tenant systems: 19 namespaces × 1M capacity = memory footprint that bore no relationship to actual usage.
v0.15.3 shipped adaptive capacity. Each namespace now starts at 4,096 elements and grows as needed. The impact on a 19-namespace deployment:
| Version | Namespaces | Memory | Reduction |
|---|---|---|---|
| Pre-v0.15.3 | 19 | 709 MB | — |
| v0.15.3+ | 19 | 92 MB | 7.7× |
A namespace holding 200 memories consumes the memory of 200 memories — not 1,000,000. This is what makes per-user namespaces practical at the hundreds-of-tenants scale you'll actually operate at during early product stages.
pip install "feather-db>=0.15.3"
Core API: add, search, forget
All three core operations accept a namespace parameter. The pattern is consistent across the entire API surface.
import feather_db as fdb
db = fdb.DB.open("tenants.feather", dim=768)
# ADD — scope memory to a namespace
def remember(user_id: str, text: str):
db.add(embed(text),
text=text,
namespace=user_id)
# SEARCH — results scoped to namespace only
def recall(user_id: str, query: str, k: int = 5):
return db.search(embed(query),
k=k,
namespace=user_id)
# FORGET — delete all memories in a namespace
def forget_all(user_id: str):
db.forget_namespace(user_id)
forget_namespace removes the entire subgraph for that namespace — vectors, HNSW graph, and metadata — and frees the adaptive capacity allocation. Use it when a user deletes their account or when a session expires and you want full cleanup.
Design patterns
Per-user isolation (the default SaaS pattern)
Use the user's account ID as the namespace key. This gives you a hard tenant boundary enforced at the graph level.
namespace = f"user_{user.account_id}"
db.add(embed(text), text=text, namespace=namespace)
results = db.search(query_vec, k=5, namespace=namespace)
Per-session scoping
For agents that need fresh context per session without bleeding across sessions, scope to a session ID. At session end, call forget_namespace or let the namespace idle for compaction.
namespace = f"session_{session_id}"
# Build session memory during conversation
for turn in conversation:
db.add(embed(turn.content), text=turn.content, namespace=namespace)
# Query during the session
results = db.search(query_vec, k=5, namespace=namespace)
# Clean up at end of session
db.forget_namespace(namespace)
Per-agent-role partitioning
Multi-agent systems often have specialized agents — a planner, a critic, a researcher — that should not share memory. Namespace by role to keep their context clean.
agents = ["planner", "critic", "researcher", "executor"]
def agent_memory(role: str, text: str):
db.add(embed(text), text=text, namespace=f"agent_{role}")
def agent_recall(role: str, query: str):
return db.search(embed(query), k=5, namespace=f"agent_{role}")
Per-topic knowledge scoping
For a product where users have distinct knowledge domains (e.g., different projects, codebases, or knowledge bases), namespace by topic within the user's scope. Combine with entity tags for secondary grouping.
def add_to_topic(user_id: str, topic: str, text: str):
namespace = f"{user_id}_{topic}"
db.add(embed(text), text=text, namespace=namespace)
# user_42_backend, user_42_frontend, user_42_infra
add_to_topic("user_42", "backend", "FastAPI service handles auth via JWT.")
add_to_topic("user_42", "infra", "Deployed on Render, Postgres on Supabase.")
Cross-namespace search
Sometimes you need to search across multiple namespaces — for admin tooling, analytics, or a super-user view. Feather doesn't support a single cross-namespace ANN call (by design — the subgraphs are separate), so the pattern is a fan-out with merge:
def search_across_namespaces(namespaces: list[str], query: str, k: int = 5):
query_vec = embed(query)
all_results = []
for ns in namespaces:
results = db.search(query_vec, k=k, namespace=ns)
for r in results:
r.meta.set_attribute("_namespace", ns)
all_results.append(r)
# Merge by score, keep top k globally
all_results.sort(key=lambda r: r.score, reverse=True)
return all_results[:k]
# Admin: search across all active users
active_users = ["user_alice", "user_bob", "user_carol"]
results = search_across_namespaces(active_users, "deployment issues", k=10)
Cross-namespace fan-out is appropriate for admin dashboards and offline analytics. It should not be in the hot path of per-user requests — those should always use single-namespace search.
Namespace-scoped compaction
Feather's compaction removes low-scoring nodes based on recall history and temporal decay. You can trigger compaction for a specific namespace without touching the rest of the file:
# Compact a single user's namespace
# Removes nodes with low stickiness and high decay
db.compact_namespace("user_alice",
min_recall_count=1,
max_age_days=90)
# Or compact all namespaces in one pass
db.compact(min_recall_count=1, max_age_days=90)
Namespace-scoped compaction is useful for long-running SaaS deployments where inactive users accumulate stale memories that waste adaptive capacity without ever surfacing in searches.
Multi-tenant SaaS: one file or many?
Two valid architectures — each with a clear use case:
| Pattern | Structure | Best for | Tradeoff |
|---|---|---|---|
| One file, many namespaces | saas.feather with namespace per user | Startups, <10K users, single-server deploy | Simple ops; one file to back up |
| One file per account | user_alice.feather, user_bob.feather | Enterprise, GDPR right-to-erasure, data residency | File-per-user management; stronger isolation |
For most early-stage SaaS products, one file with namespace-per-user is simpler and sufficient. You get the 7.7× memory savings from adaptive capacity, one backup target, and zero per-user infrastructure. When you need GDPR right-to-erasure (delete everything for a user), one file per account makes it a file deletion — no namespace surgery required.
Security: what namespace isolation is and isn't
Namespace isolation is logical, not cryptographic. The HNSW subgraph for namespace user_alice is physically stored in the same file as user_bob. A process with file-system read access to saas.feather can read all namespaces.
What this means in practice:
- Namespace isolation prevents cross-tenant data from appearing in search results — this is the common threat in multi-tenant AI products.
- Namespace isolation does not prevent a compromised process with file access from reading any namespace directly.
- For strong cryptographic isolation (healthcare, finance, legal), use separate
.featherfiles per tenant, optionally encrypted at rest via OS-level or cloud storage encryption.
# Logical isolation (most SaaS use cases)
# One file, one process, namespace enforcement in application code
db = fdb.DB.open("saas.feather", dim=768)
# Strong isolation (regulated industries)
# Separate file per tenant, each file independently encrypted/permissioned
def get_tenant_db(tenant_id: str) -> fdb.DB:
path = f"/secure/tenants/{tenant_id}.feather"
return fdb.DB.open(path, dim=768)
Putting it together: a complete SaaS pattern
import feather_db as fdb
from datetime import datetime
db = fdb.DB.open("saas_memory.feather", dim=768)
class TenantMemory:
def __init__(self, user_id: str):
self.user_id = user_id
self.namespace = f"user_{user_id}"
def add(self, text: str, category: str = "general", importance: float = 1.0):
mem = db.add(embed(text),
text=text,
namespace=self.namespace,
entity=category)
mem.meta.set_attribute("created_at", datetime.utcnow().isoformat())
mem.meta.set_attribute("importance", str(importance))
return mem
def search(self, query: str, category: str = None, k: int = 5):
return db.search(embed(query),
k=k,
namespace=self.namespace,
entity=category) # None = all categories
def forget_category(self, category: str):
db.delete_by_entity(namespace=self.namespace, entity=category)
def forget_all(self):
db.forget_namespace(self.namespace)
def compact(self, max_age_days: int = 90):
db.compact_namespace(self.namespace,
min_recall_count=1,
max_age_days=max_age_days)
# Usage
alice = TenantMemory("alice_7f3a")
alice.add("Alice works at a fintech startup.", category="work", importance=1.5)
alice.add("Alice mentioned anxiety about the Series A.", category="emotional")
results = alice.search("What is the user's work situation?")
The TenantMemory wrapper keeps namespace strings out of application code and makes the tenant boundary explicit in the type system. Each instantiation represents one tenant's memory scope — one HNSW subgraph, one adaptive capacity allocation, zero cross-tenant data exposure in search.
Install: pip install "feather-db>=0.15.3" · GitHub: github.com/feather-store/feather