App multi-usuario con aislamiento
Una app SaaS, un asistente de escritorio con varios perfiles, una herramienta interna con cientos de empleados — todas son multi-tenant desde el día 1. iLAB Memory es multi-usuario out of the box: cada método del facade toma un user_id y el orchestrator acota las queries SQL automáticamente. Esta receta muestra el patrón, el antipatrón, y los gotchas.
Lo que necesitás
Python 3.11+
hashlib de la stdlib para IDs de usuario estables.
ilab-memory
pip install ilab-memory
Un identificador estable
OIDC subject, hash del email, UUID — cualquier cosa que no cambie durante la vida del usuario.
SQLite en modo WAL
Habilitado automáticamente por ILabMemory.from_path. Lecturas concurrentes, escrituras serializadas.
Arquitectura
┌────────────────┐
│ app FastAPI │
└────────┬───────┘
│
una ILabMemory
instancia compartida
│
┌────────▼────────┐
│ ./memory.db │
│ (modo WAL) │
└─────────────────┘
▲ ▲ ▲
│ │ │
user_id user_id user_id
"u_a" "u_b" "u_c"
Una instancia, muchos tenants. El orchestrator filtra cada lectura y escritura por user_id — no hay fila compartida entre usuarios (más allá de la metadata de SQLite).
Implementación
1. Elegí un user_id estable
user_id es un string. Usá cualquier cosa estable, opaca y no-PII cuando sea posible. Un SHA-256 del email del usuario + un salt por app funciona bien; OIDC sub es mejor cuando está disponible.
import hashlib
import os
SALT = os.environ["USER_ID_SALT"] # rotalo solo en una migración coordinada
def stable_user_id(email: str) -> str:
return hashlib.sha256(f"{SALT}:{email.lower()}".encode()).hexdigest()[:24]
No uses el email crudo como user_id. Logs, backups y query plans lo exponen. Un hash es opaco, determinista e irreversible sin el salt.
2. Compartí una sola instancia de ILabMemory
Construila una vez al arranque. El orchestrator es single-threaded pero SQLite WAL permite lectores concurrentes — perfecto para apps FastAPI / desktop con concurrencia liviana.
from ilab_memory import ILabMemory
mem = ILabMemory.from_path("./memory.db")
Para cargas multi-tenant con muchas escrituras (cientos por segundo cruzando muchos usuarios), considerá sharding: una DB por tenant, lookup en request time. Mencionado como patrón avanzado — ver Architecture.
3. Guardá acotado a un usuario
Cada escritura toma user_id. El orchestrator lo guarda como columna en la fila — no hay namespace global.
user_id = stable_user_id("alice@example.com")
mem.mem_save(
user_id=user_id,
type="preference",
title="Opt-in newsletter",
content="Suscripto al digest semanal.",
topic_key=f"user/{user_id}/newsletter",
)
4. Leé acotado al mismo usuario
Las lecturas filtran por user_id. Memoria escrita bajo user_id="u_a" es invisible para user_id="u_b" — la cláusula SQL WHERE user_id = ? es no negociable.
results = mem.mem_search(
user_id=user_id,
query="newsletter",
limit=5,
)
# results contiene solo observaciones de Alice
mem_get_observation(user_id, observation_id) devuelve None si el id pertenece a otro usuario — sin excepción, sin leak. Esto se enforce a nivel del store.
5. Mostrá stats por usuario en la UI
mem_stats(user_id) devuelve counts (observations, sessions) para un tenant. Perfecto para badges "tenés N memorias" o dashboards de cuotas.
stats = mem.mem_stats(user_id=user_id)
print(stats)
# {'observations': 42, 'sessions': 3}
No hay endpoint de stats globales por diseño — privacidad y aislamiento de tenants van primero.
6. Evitá el antipatrón cross-tenant
Guardar dos usuarios bajo el mismo user_id mezcla sus memorias, rompe la relevancia de search, y puede leakear en sessions_context. Si tu app soporta múltiples identidades por humano (p.ej. trabajo + personal), dale a cada una su propio user_id.
# ❌ ANTIPATRÓN — Alice y su colega escriben a "team"
mem.mem_save(user_id="team", ...)
# ✅ Patrón — cada humano tiene su propio scope
mem.mem_save(user_id=stable_user_id("alice@example.com"), ...)
mem.mem_save(user_id=stable_user_id("bob@example.com"), ...)
Archivo completo
Código completo (listo para copiar y pegar)
from __future__ import annotations
import hashlib
import os
from typing import Mapping
from ilab_memory import ILabMemory
SALT = os.environ.get("USER_ID_SALT", "dev-only-salt")
DB_PATH = os.environ.get("ILAB_MEMORY_DB_PATH", "./memory.db")
def stable_user_id(email: str) -> str:
"""Hash opaco, determinista, salteado. Seguro de loggear."""
return hashlib.sha256(f"{SALT}:{email.lower()}".encode()).hexdigest()[:24]
def onboard(mem: ILabMemory, email: str, prefers_dark_mode: bool) -> str:
user_id = stable_user_id(email)
mem.mem_save(
user_id=user_id,
type="profile",
title="Cuenta creada",
content=f"Usuario registrado con email <private>{email}</private>.",
topic_key=f"user/{user_id}/profile",
)
mem.mem_save(
user_id=user_id,
type="preference",
title="Tema de UI",
content=f"prefers_dark_mode={prefers_dark_mode}",
topic_key=f"user/{user_id}/preferences/theme",
)
return user_id
def show_dashboard(mem: ILabMemory, user_id: str) -> Mapping[str, int]:
"""Mostrá counts en la página de settings del usuario."""
return mem.mem_stats(user_id=user_id)
def main() -> None:
with ILabMemory.from_path(DB_PATH) as mem:
alice = onboard(mem, "alice@example.com", prefers_dark_mode=True)
bob = onboard(mem, "bob@example.com", prefers_dark_mode=False)
# Cada usuario solo ve sus propios datos
alice_results = mem.mem_search(user_id=alice, query="tema", limit=5)
bob_results = mem.mem_search(user_id=bob, query="tema", limit=5)
assert all(r.id != b.id for r in alice_results for b in bob_results)
print("Stats de Alice:", show_dashboard(mem, alice))
print("Stats de Bob: ", show_dashboard(mem, bob))
if __name__ == "__main__":
main()
Nota de concurrencia
ILabMemory es single-threaded por diseño (v0.1). SQLite está en modo WAL, así que lecturas concurrentes desde múltiples threads son seguras; las escrituras se serializan por el locking de SQLite. Para cargas FastAPI con tráfico de escritura liviano-a-moderado, alcanza — envolvé escrituras pesadas en run_in_threadpool si hace falta. Para cargas multi-tenant sostenidas con alto throughput de escritura, el patrón de DB-por-tenant (un archivo por user_id) es el escalado recomendado.
¿Y ahora?
FastAPI + OpenAI
Patrón multi-usuario aplicado a la receta de chat — user_id fluye por POST /chat.
Agente de larga duración
Las mismas garantías de aislamiento, aplicadas a un loop de agente de 50+ turnos.
Architecture
Store hexagonal, Braess points, y la racional detrás del scoping por usuario.
Privacidad
Bloques <private>, orden de redacción, y qué nunca toca disco.