NewDakera v0.11.107 — temporal precision re-ranking, session-cohesion scoring, and reliability fixesSee what's new →
Advanced Security

Memory Permissions

⏱ ~45 min to implement 📦 Requires: Dakera v0.11+

A flat memory store with no access control is a security liability in any multi-user system. This pattern implements server-enforced namespace isolation with role-based access using scoped API keys — ensuring Customer A's data never appears in Customer B's context, private user memories stay private, and shared organizational knowledge is accessible to the right roles without being writable by everyone.

Start Building Free →
Prerequisites
  • Dakera instance running with admin API key (quickstart)
  • SDK installed: pip install dakera / npm i @dakera-ai/dakera
  • Defined access control requirements: which roles need read vs. write vs. delete access to which namespaces
  • A mechanism to provision scoped API keys per user/tenant during onboarding (your auth service or API gateway)

The Problem

Multi-tenant AI applications face a critical security challenge that most developers don't anticipate until they're in production. Without proper memory isolation:

Architecture

This pattern uses Dakera's namespace system as the primary security boundary, enforced server-side. Each tenant, user, or team gets a dedicated namespace. Scoped API keys restrict access to exactly the namespaces and operations their role requires. Admin keys can manage namespace structure but are never distributed to client-facing applications. Cross-namespace read access (for shared knowledge) is configured at the API key level, not at the application level.

PERMISSION MATRIX VISUALIZATION USER ns TEAM ns SHARED ns ADMIN ns User (own) Read + Write No access Read only No access Team Member No access Read + Write Read only No access Moderator Read only Read only Read + Write No access Admin Full access Full access Full access Full access All access control enforced server-side via scoped API keys. No client-side trust.

Permission matrix: each role gets a distinct scoped API key granting specific access levels per namespace. Access boundaries are enforced by Dakera server — no application code can bypass them.

ACCESS CONTROL FLOW Client App dk-user-key (scoped token) Dakera Gateway 1. Validate API key 2. Extract allowed namespaces 3. Check operation scope 4. Allow or 403 Forbidden Allowed Namespace user-alice (rw) 403 Forbidden user-bob (blocked) Access enforcement happens in Dakera — not in your application code. Compromised client cannot bypass it.

Access control flow: every request passes through Dakera's gateway which validates the API key, extracts allowed namespaces, checks the requested operation, and either allows or returns 403 Forbidden. Your application code is never in the enforcement path.

Implementation Steps

Warning: Never implement permission logic in your application layer

A common mistake is checking permissions in application code before calling Dakera: "if user.role == 'admin' then query all namespaces." This is defense-in-depth theater — a compromised application server bypasses all your checks. Real security requires server-side enforcement via scoped API keys. Use application-layer checks only for UX (showing/hiding UI elements), never as the actual security boundary.

Implementation

# ADMIN: Create namespace hierarchy during provisioning
curl -X POST http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "shared-org", "description": "Organization-wide knowledge base"}'

curl -X POST http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "team-engineering", "description": "Engineering team shared memory"}'

# ADMIN: Create namespace for new user during onboarding
curl -X POST http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "user-alice-chen"}'

# ADMIN: Issue scoped key for alice (rw own ns, rw team, r shared)
curl -X POST http://localhost:3000/v1/api-keys \
  -H "Authorization: Bearer dk-admin-key" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "alice-chen-key",
    "permissions": [
      {"namespace": "user-alice-chen", "scopes": ["read", "write", "delete"]},
      {"namespace": "team-engineering", "scopes": ["read", "write"]},
      {"namespace": "shared-org", "scopes": ["read"]}
    ]
  }'

# USER: Alice stores private memory (only she can read this)
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer dk-alice-scoped-key" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "user-alice-chen", "content": "My private notes: negotiating salary increase", "importance": 0.85}'

# USER: Alice attempts to access another user's namespace -- BLOCKED
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer dk-alice-scoped-key" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "user-bob-smith", "content": "Cross-tenant write attempt"}'
# Returns: 403 Forbidden

# ADMIN: GDPR deletion -- delete all memories for alice
curl -X DELETE http://localhost:3000/v1/namespaces/user-alice-chen \
  -H "Authorization: Bearer dk-admin-key"

# Verify namespace list after deletion
curl http://localhost:3000/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key"
from dakera import DakeraClient
from dataclasses import dataclass
from typing import Optional

# Admin client -- used ONLY for provisioning, never in application hot path
admin = DakeraClient(base_url="http://localhost:3000", api_key="dk-admin-key")

# --- PROVISIONING: Run once during infrastructure setup ---
def setup_shared_namespaces():
    """Create organization-level shared namespaces. Run once at deploy time."""
    shared_ns = [
        ("shared-org", "Organization-wide knowledge base"),
        ("shared-docs", "Public documentation and policies"),
        ("shared-compliance", "Compliance and legal knowledge"),
    ]
    for name, description in shared_ns:
        admin.list_namespaces()  # Verify admin access works

def onboard_user(user_id: str, team_id: Optional[str] = None, role: str = "user") -> str:
    """Provision a new user: create namespace + issue scoped key."""
    ns_name = f"user-{user_id}"

    # Create user's private namespace
    # Note: In Dakera, namespaces are created implicitly when you first store memory
    # to a new agent_id. For explicit management, use admin.create_namespace()

    # Issue scoped API key based on role
    permissions = [
        {"namespace": ns_name, "scopes": ["read", "write", "delete"]},
        {"namespace": "shared-org", "scopes": ["read"]},
        {"namespace": "shared-docs", "scopes": ["read"]},
    ]

    if team_id:
        permissions.append({"namespace": f"team-{team_id}", "scopes": ["read", "write"]})

    if role == "moderator":
        # Moderators can read all user namespaces for support purposes
        permissions.append({"namespace": "shared-org", "scopes": ["read", "write"]})

    # Store scoped key in your secrets manager -- return key ID
    # scoped_key = admin.create_api_key(label=f"{user_id}-key", permissions=permissions)
    # secrets_manager.store(f"dakera-key-{user_id}", scoped_key.secret)
    return ns_name

# --- APPLICATION: Runtime operations using scoped keys ---
class UserMemoryClient:
    """Application-level wrapper that uses scoped user keys."""

    def __init__(self, user_id: str, scoped_api_key: str):
        self.user_id = user_id
        self.user_ns = f"user-{user_id}"
        self.client = DakeraClient(base_url="http://localhost:3000", api_key=scoped_api_key)

    def store_private(self, content: str, importance: float = 0.8) -> None:
        """Store private memory -- only this user can access it."""
        self.client.store_memory(
            agent_id=self.user_ns,
            content=content,
            importance=importance,
            memory_type="semantic"
        )

    def recall_private(self, query: str, top_k: int = 5) -> list:
        """Recall from user's private namespace."""
        result = self.client.recall(
            agent_id=self.user_ns,
            query=query,
            top_k=top_k
        )
        return result.get("memories", [])

    def recall_shared(self, query: str, top_k: int = 5) -> list:
        """Recall from shared organizational namespace (read-only)."""
        result = self.client.recall(
            agent_id="shared-org",
            query=query,
            top_k=top_k
        )
        return result.get("memories", [])

    def recall_team(self, team_id: str, query: str, top_k: int = 5) -> list:
        """Recall from team namespace (read+write if member)."""
        result = self.client.recall(
            agent_id=f"team-{team_id}",
            query=query,
            top_k=top_k
        )
        return result.get("memories", [])

    def build_agent_context(self, query: str) -> str:
        """Build combined context from accessible namespaces."""
        private = self.recall_private(query, top_k=5)
        shared = self.recall_shared(query, top_k=3)

        parts = []
        if private:
            parts.append("[Your private context]
" + "
".join(m["content"] for m in private))
        if shared:
            parts.append("[Organizational knowledge]
" + "
".join(m["content"] for m in shared))

        return "

".join(parts)

# --- GDPR DELETION: Admin-only ---
def delete_user_data(user_id: str) -> None:
    """Permanently delete all memories for a user. GDPR Article 17 compliance."""
    ns_name = f"user-{user_id}"

    # Recall all memories in user namespace and batch delete
    all_memories = admin.recall(
        agent_id=ns_name,
        query="*",  # Match all
        top_k=1000
    )

    memory_ids = [m["id"] for m in all_memories.get("memories", [])]
    if memory_ids:
        admin.batch_forget(request={"agent_id": ns_name, "memory_ids": memory_ids})

    # Also revoke their API key
    # admin.revoke_api_key(f"{user_id}-key")

    print(f"Deleted {len(memory_ids)} memories for user {user_id}")

# --- Key rotation on role change ---
def update_user_role(user_id: str, new_role: str, new_team_id: Optional[str] = None) -> None:
    """Rotate API key when user's role changes."""
    # 1. Revoke old key
    # admin.revoke_api_key(f"{user_id}-key")

    # 2. Issue new key with updated permissions
    new_ns = onboard_user(user_id, team_id=new_team_id, role=new_role)

    # 3. Update stored key reference in your secrets manager
    # secrets_manager.update(f"dakera-key-{user_id}", new_key.secret)
    print(f"Rotated API key for user {user_id} with role: {new_role}")

# --- Example usage ---
# Alice (user) uses her scoped key
alice = UserMemoryClient("alice-chen", "dk-alice-scoped-key")
alice.store_private("My Q3 performance goal: improve API response time by 40%", 0.88)
context = alice.build_agent_context("performance goals")
print(context)
import { DakeraClient } from '@dakera-ai/dakera';

// Admin client -- provisioning only
const admin = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-admin-key' });

interface PermissionSpec {
  namespace: string;
  scopes: ('read' | 'write' | 'delete')[];
}

type UserRole = 'user' | 'moderator' | 'admin';

function buildPermissions(userId: string, role: UserRole, teamId?: string): PermissionSpec[] {
  const perms: PermissionSpec[] = [
    { namespace: `user-${userId}`, scopes: ['read', 'write', 'delete'] },
    { namespace: 'shared-org', scopes: ['read'] },
    { namespace: 'shared-docs', scopes: ['read'] },
  ];

  if (teamId) {
    perms.push({ namespace: `team-${teamId}`, scopes: ['read', 'write'] });
  }

  if (role === 'moderator') {
    perms.push({ namespace: 'shared-org', scopes: ['read', 'write'] });
  }

  return perms;
}

class UserMemoryClient {
  private readonly client: DakeraClient;
  private readonly userNs: string;

  constructor(userId: string, scopedApiKey: string) {
    this.userNs = `user-${userId}`;
    this.client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: scopedApiKey });
  }

  async storePrivate(content: string, importance = 0.8): Promise<void> {
    await this.client.storeMemory(this.userNs, { content, importance, memoryType: 'semantic' });
  }

  async recallPrivate(query: string, topK = 5) {
    return this.client.recall(this.userNs, query, { top_k: topK });
  }

  async recallShared(query: string, topK = 5) {
    // Will 403 if scoped key doesn't have read access to shared-org
    return this.client.recall('shared-org', query, { top_k: topK });
  }

  async recallTeam(teamId: string, query: string, topK = 5) {
    return this.client.recall(`team-${teamId}`, query, { top_k: topK });
  }

  async buildAgentContext(query: string): Promise<string> {
    const [privateResult, sharedResult] = await Promise.all([
      this.recallPrivate(query, 5),
      this.recallShared(query, 3),
    ]);

    const parts: string[] = [];
    if (privateResult.memories.length) {
      parts.push('[Your private context]
' + privateResult.memories.map(m => m.content).join('
'));
    }
    if (sharedResult.memories.length) {
      parts.push('[Organizational knowledge]
' + sharedResult.memories.map(m => m.content).join('
'));
    }
    return parts.join('

');
  }

  async listAccessibleNamespaces() {
    // Returns only namespaces the scoped key can access
    return this.client.listNamespaces();
  }
}

// GDPR deletion (admin only)
async function deleteUserData(userId: string): Promise<void> {
  const ns = `user-${userId}`;
  const allMemories = await admin.recall(ns, 'user data personal information', { top_k: 1000 });
  const ids = allMemories.memories.map(m => m.id).filter(Boolean) as string[];

  if (ids.length) {
    await admin.batchForget({ agentId: ns, memoryIds: ids });
  }
  console.log(`Deleted ${ids.length} memories for user ${userId}`);
}

// Key rotation on role change
async function updateUserRole(userId: string, newRole: UserRole, newTeamId?: string) {
  // 1. Build new permissions
  const newPerms = buildPermissions(userId, newRole, newTeamId);
  // 2. admin.revokeApiKey(`${userId}-key`) -- revoke old
  // 3. admin.createApiKey({ label: `${userId}-key`, permissions: newPerms }) -- issue new
  // 4. secretsManager.update(`dakera-key-${userId}`, newKey.secret)
  console.log(`Rotated key for ${userId} with role ${newRole}`);
}

// Usage
const alice = new UserMemoryClient('alice-chen', 'dk-alice-scoped-key');
await alice.storePrivate('My Q3 goal: improve API response time by 40%', 0.88);
const context = await alice.buildAgentContext('performance goals');
console.log(context);
use dakera_rs::{Client, StoreMemoryRequest, RecallRequest};

// Admin client (provisioning only -- never in request hot path)
let admin = Client::new("http://localhost:3000", "dk-admin-key");

// List existing namespaces
let namespaces = admin.list_namespaces().await?;
println!("Active namespaces: {:?}", namespaces);

// Alice's scoped client (rw: user-alice-chen, r: shared-org)
let alice = Client::new("http://localhost:3000", "dk-alice-scoped-key");

// Store private memory -- enforced server-side to user-alice-chen namespace
alice.store_memory("user-alice-chen", StoreMemoryRequest {
    content: "My Q3 performance goal: reduce API p99 latency by 40%.".into(),
    importance: Some(0.88),
    memory_type: "semantic".into(),
    ..Default::default()
}).await?;

// Recall from shared organizational namespace (read-only for alice)
let shared = alice.recall("shared-org", RecallRequest {
    query: "engineering standards documentation".into(),
    top_k: Some(5),
    ..Default::default()
}).await?;

// Attempt to write to shared-org -- will return 403 if alice's key is read-only
// let result = alice.store_memory("shared-org", StoreMemoryRequest { ... }).await;
// assert!(result.is_err()); // 403 Forbidden

// Verify alice cannot access bob's namespace
// let result = alice.recall("user-bob-smith", RecallRequest { ... }).await;
// assert!(result.is_err()); // 403 Forbidden

// Get memory policy for a namespace (admin operation)
let policy = admin.get_memory_policy("shared-org").await?;
println!("Shared org policy: {:?}", policy);
package main

import (
    "context"
    "fmt"

    dakera "github.com/dakera-ai/dakera-go"
)

func main() {
    ctx := context.Background()

    // Admin client -- provisioning only
    admin := dakera.NewClient("http://localhost:3000", "dk-admin-key")

    // List namespaces (admin can see all)
    namespaces, _ := admin.ListNamespaces(ctx)
    fmt.Printf("Namespaces: %v
", namespaces)

    // Alice's scoped client
    alice := dakera.NewClient("http://localhost:3000", "dk-alice-scoped-key")

    // Store private memory (alice can only write to user-alice-chen)
    alice.StoreMemory(ctx, "user-alice-chen", dakera.StoreMemoryRequest{
        Content:    "My Q3 goal: reduce API p99 latency by 40%.",
        Importance: 0.88,
        MemoryType: "semantic",
    })

    // Recall from shared namespace (read-only for alice)
    shared, _ := alice.Recall(ctx, "shared-org", dakera.RecallRequest{
        Query: "engineering standards documentation",
        TopK:  5,
    })
    fmt.Printf("Shared context: %d memories
", len(shared.Memories))

    // Attempt cross-tenant write -- blocked by Dakera (returns 403)
    _, err := alice.StoreMemory(ctx, "user-bob-smith", dakera.StoreMemoryRequest{
        Content: "Attempted cross-tenant write",
    })
    if err != nil {
        fmt.Println("Cross-tenant write blocked:", err) // 403 Forbidden
    }

    // Get memory policy
    policy, _ := admin.GetMemoryPolicy(ctx, "shared-org")
    fmt.Printf("Shared org policy: %+v
", policy)
}

// GDPR deletion helper
func deleteUserData(ctx context.Context, admin *dakera.Client, userID string) error {
    ns := fmt.Sprintf("user-%s", userID)
    memories, err := admin.Recall(ctx, ns, dakera.RecallRequest{
        Query: "all memories",
        TopK:  1000,
    })
    if err != nil {
        return err
    }

    ids := make([]string, 0, len(memories.Memories))
    for _, m := range memories.Memories {
        ids = append(ids, m.ID)
    }

    if len(ids) > 0 {
        return admin.BatchForget(ctx, ns, ids)
    }
    return nil
}

Production-ready memory isolation from day one

Dakera's namespace system gives you GDPR-compliant tenant isolation with server-enforced access control — no custom security infrastructure needed.

Try Free →

Real-World Scenario: Multi-Tenant SaaS Platform

A B2B SaaS platform deploys an AI assistant to 340 enterprise customers. Each customer has 10–500 users across multiple teams. Memory permissions are critical for their SOC 2 Type II compliance.

Before / After Memory State

Before: No Access Control
// Single namespace: "default"
// All users share one pool

// Alice's private salary note:
"salary negotiation target: $180k"

// Bob queries "salary" and
// Alice's note appears in his
// context.

// Data leak.
// SOC 2 fail.
// GDPR violation.
// Customer loses trust.
After: Namespace Isolation Active
// user-alice-chen:
"salary negotiation target: $180k"
// alice's scoped key: rw this ns

// user-bob-smith: (empty)
// bob's scoped key: rw own ns only

// Bob queries "salary":
// Dakera checks bob's key:
// 403 for user-alice-chen
// Result: zero cross-user leakage
// SOC 2 compliant. GDPR ready.

SDK Method Reference

MethodSDKPurpose in this pattern
list_namespaces()PythonAdmin: list all configured namespaces
get_memory_policy(namespace)PythonRetrieve access policy for a namespace
store_memory(agent_id, ...)PythonStore to scoped namespace (403 if unauthorized)
recall(agent_id, query, top_k)PythonRecall from scoped namespace (403 if unauthorized)
batch_forget(request)PythonAdmin: bulk delete for GDPR compliance
listNamespaces()TypeScriptAdmin: enumerate all namespaces
getMemoryPolicy(namespace)TypeScriptRetrieve namespace access policy
batchForget(request)TypeScriptAdmin: bulk memory deletion
client.list_namespaces().await?RustAdmin: list namespaces async
client.get_memory_policy("ns").await?RustRetrieve policy for namespace
admin.ListNamespaces(ctx)GoAdmin: list all namespaces
admin.GetMemoryPolicy(ctx, "ns")GoGet access policy for a namespace

Edge Cases and Gotchas

Note: Plan your namespace hierarchy for scale

A SaaS platform with 1,000 customers and 50 users each needs 50,000 user namespaces plus thousands of team and shared namespaces. Dakera handles this scale natively — namespaces are logical boundaries, not separate databases. However, your provisioning and key management infrastructure needs to handle this volume. Use Infrastructure-as-Code (Terraform) to manage namespace lifecycle at scale.

Performance Considerations

<2ms
Auth overhead per request (key validation)
50k+
Concurrent user namespaces supported
SOC 2
Audit trail available for all namespace operations
Advanced Configuration: Hierarchical namespace inheritance

For complex enterprise deployments, implement a hierarchical permission model where tenant admin keys can issue sub-scoped keys for their own tenant's namespaces:

# Enterprise hierarchy:
# global-admin -> tenant-admin -> user

# Your provisioning service gives each tenant an "admin" scoped key
# that can only issue sub-scoped keys within their tenant prefix:
# tenant-admin for acme can issue keys for:
#   - user-acme-* (any acme user namespace)
#   - team-acme-* (any acme team namespace)
#   - shared-acme (acme shared namespace)
# But NOT for:
#   - user-widgets-* (another tenant's users)
#   - shared-org (global shared namespace)

# Implementation: tenant onboarding issues tenant-admin key
# with delegate permission: "can create keys scoped to: {tenant-prefix}-*"
# Your tenant portal then calls Dakera as the tenant-admin
# to provision individual user keys -- no global admin needed at runtime

# This gives tenants self-service key management
# while keeping global admin key out of application code
Try it live

Run this pattern against a real Dakera.

The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.