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:

  • Data leakage across tenants: Customer A's proprietary pricing strategy, stored as agent memory during a planning session, appears in Customer B's recalled context. This is not a hypothetical — it's a predictable failure mode of flat memory stores.
  • Privilege escalation: A read-only analytics agent gains write access to memory through a misconfigured key, allowing it to corrupt or inject false memories that influence future agent behavior.
  • GDPR deletion gaps: A user requests data deletion under GDPR Article 17. Because their memories are mixed into a shared namespace, it's impossible to surgically delete all records for that user without manual intervention.
  • Insider threat vectors: A user elevated from "viewer" to "editor" role gains access to organizational memories accumulated before their role change, creating an unintended retroactive access grant.

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

  • Design your namespace hierarchy before writing any code
    Map out the three levels: User namespaces (user-{userId}) for private memories; Team namespaces (team-{teamId}) for collaborative knowledge; Shared namespaces (shared-org, shared-docs) for read-mostly organizational knowledge. Sketch the permission matrix for each role before provisioning. Changing the hierarchy later is painful — design it correctly upfront.
  • Create namespaces with admin key during infrastructure provisioning
    Namespace creation is an admin operation. Create namespaces as part of your infrastructure provisioning (Terraform, Pulumi, or deployment scripts) — not at runtime in application code. Pre-create the shared namespaces; create tenant namespaces during your tenant onboarding flow. Never expose admin keys to application servers.
  • Issue scoped API keys per user/tenant during onboarding
    When a user signs up, your onboarding service (using the admin key) creates a scoped API key for them. The key grants: read+write to user-{userId}, read-only to shared-org, optionally read+write to team-{teamId} if they belong to a team. Store this scoped key in your secret management system (Vault, AWS Secrets Manager) — never in environment variables or code.
  • Rotate keys on role changes — do not modify existing keys
    When a user's role changes (promotion, demotion, team change), revoke their existing scoped key and issue a new one with the updated permissions. Modifying existing keys creates audit gaps. Your system must handle key rotation gracefully: issue the new key, update the stored reference, invalidate the old key — all in a single transaction to prevent access gaps.
  • Implement GDPR deletion using namespace-scoped batch_forget
    When a user requests data deletion, your admin service calls batch_forget() on all memories in their user namespace, then archives or deletes the namespace itself. Because data is namespace-isolated, you can surgically delete exactly their data without touching shared namespaces. Document this deletion flow in your privacy policy and test it quarterly.
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:3300/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:3300/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:3300/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:3300/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:3300/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:3300/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:3300/v1/namespaces/user-alice-chen \
  -H "Authorization: Bearer dk-admin-key"

# Verify namespace list after deletion
curl http://localhost:3300/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:3300", 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:3300", 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:3300', 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:3300', 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:3300", "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:3300", "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:3300", "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:3300", "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.

  • Namespace hierarchy: customer-{customerId} (tenant root), user-{customerId}-{userId} (private), team-{customerId}-{teamId} (collaborative), shared-{customerId} (tenant-wide knowledge).
  • Key provisioning: During customer onboarding, their admin user receives a super-scoped key for their tenant's namespaces. When they add users, the tenant admin issues sub-scoped keys through your API (which calls Dakera's admin endpoint server-side).
  • Audit trail: Every API key has a label tied to the user ID. Dakera logs all operations. SOC 2 auditors can verify that Customer A's keys have never accessed Customer B's namespaces.
  • GDPR compliance: When a user submits a deletion request, the platform's compliance service uses the admin key to call batch_forget() on their user namespace — completing the deletion within the required 30-day window. Deletion is logged and verifiable.
  • Penetration testing result: Scoped key enforcement meant that even a fully compromised application server could not access cross-tenant namespaces. The security team's pen test confirmed zero cross-tenant data access possible through the Dakera layer.

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

  • Namespace collisions in multi-tenant scenarios: user-alice is ambiguous in a multi-tenant system — which tenant's Alice? Always include the tenant/customer ID: user-{customerId}-{userId}. Design namespace naming conventions upfront and enforce them with validation in your provisioning service.
  • Read-only keys and stale permissions: If you issue a read-only key to an analytics system and later need to give it write access, you must create a new key. There is no key scope upgrade operation — by design. This is a security feature, not a limitation. Plan for this in your key management workflow.
  • Cross-namespace context building: An agent context that draws from both private and shared namespaces requires two separate recall calls. This is intentional — each call is independently authorized. Never try to merge these calls or work around the boundary. The separation is the security guarantee.
  • Namespace discovery: A user with a scoped key cannot discover namespaces they don't have access to — list_namespaces() with a user-scoped key only returns their authorized namespaces. This is by design for privacy. Only admin keys see all namespaces.
  • Soft deletes and GDPR timing: If your Dakera instance implements soft deletes (memories marked deleted but physically retained for a grace period), coordinate with Dakera support to understand the physical deletion timeline. For GDPR right-to-erasure compliance, you need confirmation of physical deletion, not just logical deletion. Document this in your data processing agreement.
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

Build secure multi-tenant AI with confidence

Dakera's server-enforced namespace isolation makes GDPR compliance and SOC 2 certification achievable without custom security infrastructure. Start your free instance today.

Start Building Free →