Most real-world applications serve multiple users, often across multiple organizations. Synap’s scoping system ensures that memories are properly isolated while still enabling shared context where appropriate. This guide explains the scope hierarchy, walks through common patterns, and shows you how to configure scoping for your application.
When you ingest a memory, Synap assigns it to a scope based on the identifiers you provide:
Pass user_id — memory is stored at USER scope
Pass customer_id (without user_id) — memory is stored at CUSTOMER scope
Pass neither — memory is stored at CLIENT scope
When you retrieve context, Synap merges memories from the narrowest applicable scope upward:
Retrieval for user_id="user_alice", customer_id="cust_acme": USER (user_alice) ← Alice's personal memories + CUSTOMER (cust_acme) ← Acme Corp's shared memories + CLIENT ← Your application's shared memories + WORLD ← General knowledge ───────────────────────── = Merged context (user memories take priority on conflicts)
When memories at different scopes conflict (e.g., a user preference contradicts a customer-level default), the narrower scope wins. User-level memories always override customer-level, which override client-level.
User scope is the most common isolation boundary. Every time you ingest a memory that belongs to a specific individual, pass their user_id.
# Ingest a memory scoped to a specific userawait sdk.memories.create( document="User: I prefer to communicate in Spanish.\nAssistant: Got it! I'll respond in Spanish from now on.", document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme", mode="fast")
To retrieve memories for that user:
# Retrieve context scoped to a specific user# This returns: user_alice memories + cust_acme memories + client memoriescontext = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme")for fact in context.facts: print(f"[{fact.scope}] {fact.content}")# Output:# [user] Prefers communication in Spanish# [customer] Acme Corp uses Slack for internal communication# [client] Product supports 12 languages including Spanish
The scope field on each returned memory tells you where it came from, so you can handle different scopes differently if needed (e.g., displaying user memories with higher visual prominence).
Customer scope represents an organization, team, or account. Memories at this scope are shared across all users within that customer.
# Ingest a memory at customer scope# Note: passing customer_id WITHOUT user_id stores at CUSTOMER scopeawait sdk.memories.create( document="Acme Corp's fiscal year ends in March. All Q4 reports are due by March 15.", document_type="document", customer_id="cust_acme", mode="fast", metadata={"source": "company-policy-doc"})
Any user within cust_acme will see this memory in their context:
# Alice sees Acme's memoriesalice_ctx = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme")# Bob also sees the same Acme memories (plus his own user memories)bob_ctx = await sdk.user.context.fetch( user_id="user_bob", customer_id="cust_acme")
You can also retrieve customer-level context without specifying a user:
# Retrieve ONLY customer-scoped memories (no user-specific memories)customer_ctx = await sdk.customer.context.fetch( customer_id="cust_acme")
Customer scope is ideal for ingesting organizational knowledge: company policies, team structures, project details, shared preferences, and onboarding materials. Ingest these documents once and all users in that organization benefit.
Client scope represents your entire application. Memories at this scope are visible to every user across all customers.
# Ingest a memory at client scope# Note: no user_id or customer_id — stores at CLIENT scopeawait sdk.memories.create( document="Our product supports SSO via SAML 2.0 and OIDC. Configuration is available in Settings > Security > SSO.", document_type="document", mode="fast", metadata={"source": "product-docs", "version": "2.4"})
Client-scoped memories appear in every user’s context, regardless of their customer:
# Alice at Acme sees client memoriesalice_ctx = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme")# Charlie at a completely different customer also sees client memoriescharlie_ctx = await sdk.user.context.fetch( user_id="user_charlie", customer_id="cust_globex")
You can retrieve only client-scoped context:
# Retrieve ONLY client-scoped memoriesclient_ctx = await sdk.client.context.fetch()
Client scope is useful for ingesting your product documentation, feature announcements, FAQ content, and any other knowledge that should be available to all users of your application.
Let’s walk through a complete example. You are building an AI assistant for a project management tool. The assistant helps team members with tasks, deadlines, and project context.
# Alice asks about sprint schedulescontext = await sdk.conversation.context.fetch( conversation_id="conv_alice_001", search_query=["when is the next sprint review?"], max_results=5, mode="fast")# The context will contain:# - [user] Alice prefers Kanban boards (may not be relevant to this query)# - [customer] Sprint reviews: Every other Friday at 2pm PT ← Most relevant# - [client] TaskFlow Pro feature info (if relevant)# Build the prompt with scope-aware formattingmemory_lines = []for fact in context.facts: scope_label = fact.scope.upper() memory_lines.append(f"[{scope_label}] {fact.content}")print("\n".join(memory_lines))# [CUSTOMER] Acme sprint reviews are every other Friday at 2pm PT# [CUSTOMER] Sprint duration is 2 weeks# [USER] Alice prefers Kanban board view
# Retrieve context for Maria -- no customer_id neededcontext = await sdk.user.context.fetch( user_id="user_maria")# Returns:# - [user] Maria is vegetarian and allergic to nuts# - [client] RecipeBot supports vegetarian, nut-free, and other dietary filters
If your application is single-user (e.g., a local desktop AI assistant), you can skip both customer and user scopes. Just use a single fixed user_id for all memories, or let everything land at client scope.
Changing primary_scope on an existing instance does not retroactively re-scope existing memories. New memories will follow the updated scope behavior, but existing memories retain their original scope assignments. See the Migration Guide for guidance on scope changes.
When developing, verify that scope isolation works correctly by testing cross-scope access patterns:
import asyncioasync def verify_scope_isolation(sdk): """Verify that user memories are properly isolated.""" # Ingest a secret for Alice await sdk.memories.create( document="User: My password recovery email is [email protected]", document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme", mode="fast" ) # Wait for ingestion to process await asyncio.sleep(3) # Retrieve as Bob -- Alice's secret should NOT appear bob_ctx = await sdk.user.context.fetch( user_id="user_bob", customer_id="cust_acme" ) alice_facts = [ f for f in bob_ctx.facts if "[email protected]" in f.content.lower() ] assert len(alice_facts) == 0, ( "ISOLATION FAILURE: Bob can see Alice's user-scoped memories!" ) print("Scope isolation verified: Bob cannot see Alice's memories.") # Retrieve as Alice -- her own memory SHOULD appear alice_ctx = await sdk.user.context.fetch( user_id="user_alice", customer_id="cust_acme" ) alice_facts = [ f for f in alice_ctx.facts if "[email protected]" in f.content.lower() ] assert len(alice_facts) > 0, ( "RETRIEVAL FAILURE: Alice cannot see her own memories!" ) print("Scope access verified: Alice can see her own memories.")
In production, PII handling (configured in your MACA config) would redact or mask the email address before storage. This test uses raw content for simplicity — in practice, the pii.handling setting would process the content before it reaches storage.
Use this flowchart to decide which scope identifiers to pass when ingesting memories:
1
Is this memory about a specific individual?
Yes — Pass user_id (and customer_id if the user belongs to an organization).No — Continue to the next question.
2
Is this memory about a specific organization or team?
Yes — Pass customer_id only (no user_id).No — Continue to the next question.
3
Is this memory about your application or product?
Yes — Pass neither user_id nor customer_id. It will be stored at client scope.No — This is likely general knowledge. Store at client scope or consider whether it should be ingested at all.
Establish a convention for user_id and customer_id values and enforce it across your application. Inconsistent IDs (e.g., "alice" vs "user_alice" vs "user-alice") create fragmented memory silos.Recommended: prefix-based IDs like user_<your_internal_id> and cust_<your_internal_id>.
Always pass customer_id when you have it
Even if you primarily use user scope, always pass customer_id alongside user_id during ingestion and retrieval. This ensures customer-scoped memories are properly accessible and the scope hierarchy works correctly.
# Good: both identifiersawait sdk.memories.create( document=conversation, document_type="ai-chat-conversation", user_id="user_alice", customer_id="cust_acme", # Always include when available mode="fast")# Less ideal: missing customer_idawait sdk.memories.create( document=conversation, document_type="ai-chat-conversation", user_id="user_alice", # customer_id omitted -- customer-scoped memories won't merge mode="fast")
Ingest shared knowledge at the right scope
A common mistake is ingesting organizational knowledge at user scope (by including a user_id). This makes the knowledge invisible to other users in the same organization. Ingest shared content at customer or client scope explicitly.
Test with multiple users early
Do not wait until production to test multi-user scenarios. Set up at least two test users and one test customer during development and verify isolation before deploying.