Intermediate Architecture

Namespace Isolation

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

Serve thousands of tenants from a single Dakera instance with absolute data isolation. Namespace isolation enforces separation at the storage layer — not just the application layer — so a misconfigured API call or a compromised key cannot access another tenant's data. This is the foundation of any multi-tenant AI memory architecture.

Set Up Tenant Isolation →
Prerequisites
  • Running Dakera server (see Quickstart)
  • Admin API key with namespace management permissions
  • Per-tenant scoped API keys for production use

Problem

In multi-tenant or multi-user applications, data isolation is not optional — it is a hard requirement for security and compliance. The failure modes when isolation is absent are severe:

  • Cross-tenant data leakage: User A's recall query returns results from User B's memory store due to a missing filter parameter or a bug in application-layer access checks
  • Noisy recall: A single large tenant's memory volume dominates search results for smaller tenants sharing the same namespace
  • Compliance failures: GDPR right-to-erasure, HIPAA data segmentation, or SOC2 access control requirements cannot be met if all tenants share a single memory partition
  • Cascading deletions: A "delete user data" operation that queries by tag instead of namespace may accidentally affect other users if tags collide

Dakera namespaces eliminate these risks by providing hard storage-level partitioning. Each namespace is its own independent memory store, knowledge graph, and search index. A key scoped to tenant-a cannot query tenant-b data regardless of what parameters are passed in the API call.

Application-Layer Checks Are Not Enough

Many teams implement tenant isolation by passing a user_id filter in every query. This relies on correct code in every call site — one bug, one missing parameter, one junior developer using the wrong query method, and data leaks across tenants. Namespace isolation enforces the boundary at the infrastructure level. There is no code path that can cross it.

Architecture

Each tenant gets an isolated namespace containing its own complete memory stack:

  • Isolated vector index — semantic search operates only within the tenant's namespace
  • Isolated knowledge graph — entity relationships are tenant-specific
  • Isolated decay configuration — each namespace can have its own memory lifecycle policy
  • Scoped API keys — one key per tenant, restricted at the API gateway to that namespace only

Namespace Architecture Diagram

API Gateway validates key routes to namespace dk-tenant-a-key scoped: tenant-a dk-tenant-b-key scoped: tenant-b dk-tenant-c-key scoped: tenant-c namespace: tenant-a vector index | knowledge graph decay policy | session data isolated namespace: tenant-b vector index | knowledge graph decay policy | session data isolated namespace: tenant-c vector index | knowledge graph decay policy | session data isolated no access no access Storage-Level Enforcement dk-tenant-a-key cannot query tenant-b or tenant-c data. No code path exists.

Step-by-Step Implementation

  • Choose your namespace naming convention
    Pick a consistent pattern before you start. Common choices: tenant-{tenant_id} for SaaS (e.g., tenant-acme-corp), user-{user_id} for per-user isolation (e.g., user-u_8f3k2j), or org-{org_id} for organizational boundaries. Avoid using human-readable names that could be guessed — use IDs.
  • Create a namespace on tenant signup
    Hook your user/tenant onboarding flow to call create_namespace() with the admin key. This should happen synchronously during signup — the tenant's namespace must exist before their first memory write. Keep the admin key server-side only; never expose it to clients.
  • Issue a scoped API key per tenant
    After creating the namespace, issue a Dakera API key scoped exclusively to that namespace. Store this key securely (environment variable or secrets manager) and provide it to your backend service for that tenant's requests. Rotate keys on schedule and on any suspected compromise.
  • Route all memory operations through the scoped key
    Your API layer should instantiate a Dakera client using the tenant's scoped key for every request. Use a per-request client instantiation or a keyed client pool — never share a single admin-key client across tenants. The namespace routing is enforced by the key, so no additional filtering code is needed.
  • Implement namespace deletion on tenant offboarding
    When a tenant deletes their account (or triggers a GDPR erasure request), call delete_namespace() with the admin key. This atomically destroys all memories, sessions, knowledge graphs, and embeddings for that tenant. No manual cleanup required — one call covers the entire data footprint.
  • Audit namespace access with list_namespaces
    Periodically call list_namespaces() to audit which namespaces exist, compare against your user database, and detect orphaned namespaces from deactivated accounts. Automate this as a weekly cron job that flags any namespace with no corresponding active tenant record.

Implementation

# --- Tenant onboarding: create isolated namespace ---
curl -X POST http://localhost:3300/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "tenant-acme-corp"}'

# --- List all namespaces (admin audit) ---
curl http://localhost:3300/v1/namespaces \
  -H "Authorization: Bearer dk-admin-key"

# --- Tenant A: store confidential data (scoped key) ---
curl -X POST http://localhost:3300/v1/memory/store \
  -H "Authorization: Bearer dk-tenant-acme-corp-key" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "tenant-acme-corp",
    "content": "Acme Corp Q3 roadmap: launch Omega feature in September. Team of 4 engineers.",
    "importance": 0.9,
    "tags": ["roadmap", "confidential"]
  }'

# --- Tenant A: recall their own data ---
curl "http://localhost:3300/v1/memory/recall?agent_id=tenant-acme-corp&query=Q3+roadmap&top_k=5" \
  -H "Authorization: Bearer dk-tenant-acme-corp-key"

# --- Tenant B: cannot access Tenant A's namespace ---
# This returns HTTP 403 Forbidden — enforced at storage level
curl "http://localhost:3300/v1/memory/recall?agent_id=tenant-acme-corp&query=roadmap" \
  -H "Authorization: Bearer dk-tenant-betacorp-key"
# Response: {"error":"forbidden","message":"Key not authorized for namespace tenant-acme-corp"}

# --- Tenant offboarding: delete entire namespace atomically ---
curl -X DELETE http://localhost:3300/v1/namespaces/tenant-acme-corp \
  -H "Authorization: Bearer dk-admin-key"
# Deletes ALL memories, sessions, graphs, and embeddings for this tenant
from dakera import DakeraClient
import os

# Admin client — server-side only, never exposed to tenants
admin = DakeraClient(
    base_url="http://localhost:3300",
    api_key=os.environ["DAKERA_ADMIN_KEY"]
)

class TenantMemoryService:
    """
    Service layer for multi-tenant memory operations.
    Each tenant gets their own Dakera namespace and scoped key.
    """

    def __init__(self, admin_client: DakeraClient):
        self._admin = admin_client
        self._tenant_clients: dict = {}

    def provision_tenant(self, tenant_id: str) -> str:
        """Create namespace on tenant signup. Returns the namespace name."""
        ns_name = f"tenant-{tenant_id}"
        self._admin.create_namespace(ns_name)
        print(f"Provisioned namespace: {ns_name}")
        return ns_name

    def get_client(self, tenant_id: str) -> DakeraClient:
        """Get (or create) a scoped client for a tenant."""
        if tenant_id not in self._tenant_clients:
            # In production: look up per-tenant scoped API key from secrets manager
            scoped_key = os.environ.get(f"DAKERA_KEY_TENANT_{tenant_id.upper()}")
            if not scoped_key:
                raise ValueError(f"No scoped key found for tenant {tenant_id}")
            self._tenant_clients[tenant_id] = DakeraClient(
                base_url="http://localhost:3300",
                api_key=scoped_key
            )
        return self._tenant_clients[tenant_id]

    def store_memory(self, tenant_id: str, content: str, **kwargs):
        client = self.get_client(tenant_id)
        return client.store_memory(
            agent_id=f"tenant-{tenant_id}",
            content=content,
            **kwargs
        )

    def recall(self, tenant_id: str, query: str, top_k: int = 5):
        client = self.get_client(tenant_id)
        return client.recall(
            agent_id=f"tenant-{tenant_id}",
            query=query,
            top_k=top_k
        )

    def list_namespaces(self) -> list:
        """Admin: list all namespaces for audit."""
        return self._admin.list_namespaces()

    def offboard_tenant(self, tenant_id: str):
        """GDPR erasure / account deletion: atomically removes all tenant data."""
        ns_name = f"tenant-{tenant_id}"
        self._admin.delete_namespace(ns_name)
        self._tenant_clients.pop(tenant_id, None)
        print(f"Deleted namespace and all data for tenant {tenant_id}")

    def get_memory_policy(self, tenant_id: str) -> dict:
        """Check the decay/lifecycle policy for a tenant's namespace."""
        return self._admin.get_memory_policy(f"tenant-{tenant_id}")


# --- Usage ---
service = TenantMemoryService(admin)

# Tenant signup
service.provision_tenant("acme-corp")
service.provision_tenant("betacorp")

# Tenant A stores confidential data
service.store_memory(
    "acme-corp",
    content="Q3 roadmap: launch Omega feature in September. 4 engineers assigned.",
    importance=0.9,
    tags=["roadmap", "confidential"]
)

# Tenant A recalls their own data
results = service.recall("acme-corp", "Q3 product roadmap")
for r in results:
    print(f"[{r['importance']:.2f}] {r['content']}")

# Audit: list all namespaces
namespaces = service.list_namespaces()
print(f"Active namespaces: {[n['name'] for n in namespaces]}")

# Account deletion (GDPR erasure)
service.offboard_tenant("acme-corp")
import { DakeraClient } from '@dakera-ai/dakera';

const admin = new DakeraClient({
  baseUrl: 'http://localhost:3300',
  apiKey: process.env.DAKERA_ADMIN_KEY!
});

class TenantMemoryService {
  private adminClient: DakeraClient;
  private tenantClients = new Map<string, DakeraClient>();

  constructor(admin: DakeraClient) {
    this.adminClient = admin;
  }

  async provisionTenant(tenantId: string): Promise<string> {
    const ns = `tenant-${tenantId}`;
    await this.adminClient.createNamespace(ns);
    console.log(`Provisioned namespace: ${ns}`);
    return ns;
  }

  private getClient(tenantId: string): DakeraClient {
    if (!this.tenantClients.has(tenantId)) {
      const key = process.env[`DAKERA_KEY_TENANT_${tenantId.toUpperCase()}`];
      if (!key) throw new Error(`No scoped key for tenant ${tenantId}`);
      this.tenantClients.set(tenantId, new DakeraClient({ baseUrl: 'http://localhost:3300', apiKey: key }));
    }
    return this.tenantClients.get(tenantId)!;
  }

  async storeMemory(tenantId: string, content: string, opts: Record<string, unknown> = {}) {
    const client = this.getClient(tenantId);
    return client.storeMemory(`tenant-${tenantId}`, { content, ...opts });
  }

  async recall(tenantId: string, query: string, topK = 5) {
    const client = this.getClient(tenantId);
    return client.recall(`tenant-${tenantId}`, query, { top_k: topK });
  }

  async listNamespaces() {
    return this.adminClient.listNamespaces();
  }

  async offboardTenant(tenantId: string) {
    await this.adminClient.deleteNamespace(`tenant-${tenantId}`);
    this.tenantClients.delete(tenantId);
    console.log(`Deleted all data for tenant ${tenantId}`);
  }
}

// --- Usage ---
const service = new TenantMemoryService(admin);

await service.provisionTenant('acme-corp');
await service.provisionTenant('betacorp');

// Tenant A stores confidential data
await service.storeMemory('acme-corp', 'Q3 roadmap: Omega feature in September', {
  importance: 0.9,
  tags: ['roadmap', 'confidential']
});

// Tenant A recalls
const results = await service.recall('acme-corp', 'Q3 product roadmap');
console.log(results.map(r => `[${r.importance}] ${r.content}`));

// Audit
const namespaces = await service.listNamespaces();
console.log('Active:', namespaces.map(n => n.name));

// GDPR erasure
await service.offboardTenant('acme-corp');
use dakera_rs::{Client, CreateNamespaceRequest, StoreMemoryRequest, RecallRequest};
use std::collections::HashMap;

struct TenantService {
    admin: Client,
    tenant_clients: HashMap<String, Client>,
}

impl TenantService {
    fn new(admin: Client) -> Self {
        Self { admin, tenant_clients: HashMap::new() }
    }

    async fn provision_tenant(&self, tenant_id: &str) -> anyhow::Result<()> {
        let ns = format!("tenant-{}", tenant_id);
        self.admin.create_namespace(&ns, CreateNamespaceRequest::default()).await?;
        println!("Provisioned namespace: {}", ns);
        Ok(())
    }

    fn get_client(&mut self, tenant_id: &str) -> &Client {
        if !self.tenant_clients.contains_key(tenant_id) {
            let key = std::env::var(format!("DAKERA_KEY_TENANT_{}", tenant_id.to_uppercase()))
                .expect("Scoped key not found");
            self.tenant_clients.insert(
                tenant_id.to_string(),
                Client::new("http://localhost:3300", &key)
            );
        }
        &self.tenant_clients[tenant_id]
    }

    async fn store_memory(&mut self, tenant_id: &str, content: &str, importance: f64) -> anyhow::Result<()> {
        let ns = format!("tenant-{}", tenant_id);
        self.get_client(tenant_id).store_memory(&ns, StoreMemoryRequest {
            content: content.to_string(),
            importance: Some(importance),
            ..Default::default()
        }).await?;
        Ok(())
    }

    async fn recall(&mut self, tenant_id: &str, query: &str, top_k: u32) -> anyhow::Result<Vec<serde_json::Value>> {
        let ns = format!("tenant-{}", tenant_id);
        let results = self.get_client(tenant_id).recall(&ns, RecallRequest {
            query: query.to_string(),
            top_k: Some(top_k),
            ..Default::default()
        }).await?;
        Ok(results)
    }

    async fn offboard_tenant(&self, tenant_id: &str) -> anyhow::Result<()> {
        let ns = format!("tenant-{}", tenant_id);
        self.admin.delete_namespace(&ns).await?;
        println!("Deleted namespace {}", ns);
        Ok(())
    }
}
package main

import (
    "context"
    "fmt"
    "os"
    dakera "github.com/dakera-ai/dakera-go"
)

type TenantService struct {
    admin         *dakera.Client
    tenantClients map[string]*dakera.Client
}

func NewTenantService(adminKey string) *TenantService {
    return &TenantService{
        admin:         dakera.NewClient("http://localhost:3300", adminKey),
        tenantClients: make(map[string]*dakera.Client),
    }
}

func (s *TenantService) ProvisionTenant(ctx context.Context, tenantID string) error {
    ns := fmt.Sprintf("tenant-%s", tenantID)
    if err := s.admin.CreateNamespace(ctx, ns, nil); err != nil {
        return err
    }
    fmt.Printf("Provisioned namespace: %s
", ns)
    return nil
}

func (s *TenantService) getClient(tenantID string) *dakera.Client {
    if _, ok := s.tenantClients[tenantID]; !ok {
        key := os.Getenv(fmt.Sprintf("DAKERA_KEY_TENANT_%s", tenantID))
        s.tenantClients[tenantID] = dakera.NewClient("http://localhost:3300", key)
    }
    return s.tenantClients[tenantID]
}

func (s *TenantService) StoreMemory(ctx context.Context, tenantID, content string, importance float64) error {
    ns := fmt.Sprintf("tenant-%s", tenantID)
    _, err := s.getClient(tenantID).StoreMemory(ctx, ns, dakera.StoreMemoryRequest{
        Content:    content,
        Importance: importance,
    })
    return err
}

func (s *TenantService) Recall(ctx context.Context, tenantID, query string, topK int) ([]dakera.Memory, error) {
    ns := fmt.Sprintf("tenant-%s", tenantID)
    return s.getClient(tenantID).Recall(ctx, ns, dakera.RecallRequest{
        Query: query,
        TopK:  topK,
    })
}

func (s *TenantService) OffboardTenant(ctx context.Context, tenantID string) error {
    ns := fmt.Sprintf("tenant-%s", tenantID)
    return s.admin.DeleteNamespace(ctx, ns)
}

// Usage
func main() {
    svc := NewTenantService(os.Getenv("DAKERA_ADMIN_KEY"))
    ctx := context.Background()

    svc.ProvisionTenant(ctx, "acme-corp")
    svc.StoreMemory(ctx, "acme-corp", "Q3 roadmap: Omega feature in September", 0.9)

    results, _ := svc.Recall(ctx, "acme-corp", "Q3 roadmap", 5)
    for _, r := range results {
        fmt.Printf("[%.2f] %s
", r.Importance, r.Content)
    }
}

Building a multi-tenant AI product?

Dakera's namespace API handles tenant isolation out of the box — one namespace per tenant, one API call to provision.

Set Up Tenant Isolation →

Before / After Memory State

Before — shared namespace with user_id filter
-- Single namespace: default --
[user_id: acme] Q3 roadmap: Omega in Sept
[user_id: acme] Hiring 4 ML engineers
[user_id: betacorp] Competitor analysis: Acme
[user_id: betacorp] Patent filing for widget
[user_id: acme] Budget: $2.4M allocated

# A recall without user_id filter returns:
recall(query="company plans") ->
  [acme]    Q3 roadmap: Omega in Sept
  [betacorp] Competitor analysis: Acme  ← LEAK
  [acme]    Budget: $2.4M allocated
  [betacorp] Patent filing for widget   ← LEAK

One missing filter parameter leaks Betacorp's confidential data to Acme. Application-layer bugs become security incidents.

After — per-tenant namespace isolation
-- namespace: tenant-acme-corp --
[0.9] Q3 roadmap: Omega feature in Sept
[0.8] Hiring 4 ML engineers
[0.85] Budget: $2.4M allocated

-- namespace: tenant-betacorp --
[0.9] Competitor analysis: Acme Corp
[0.95] Patent filing for widget X

# Betacorp key can ONLY query tenant-betacorp
recall(key=betacorp-key, query="company plans") ->
  [0.9] Competitor analysis...
  [0.95] Patent filing...
  (acme data is architecturally unreachable)

No code path can cross namespace boundaries. Even with a bug that omits filters, cross-tenant access is impossible at the storage layer.

Common Namespace Architectures

Per-User Isolation

Each user gets their own namespace (user-{user_id}) with a key scoped to that namespace only. Total isolation — no cross-user leakage possible. Suitable for consumer apps with strict privacy requirements.

Per-Tenant with Sub-User Namespaces

Organization-level namespace (org-acme) for team-shared knowledge, plus per-user namespaces (org-acme-user-alice, org-acme-user-bob) for individual preferences. Alice's key: read/write to her user namespace, read-only to the org namespace. Suitable for B2B SaaS.

Per-Agent Isolation (see Multi-Agent Pattern)

Each agent role has a private namespace (agent-researcher, agent-writer) plus a shared collaboration namespace (team-shared). Combines isolation with collaboration. See the Multi-Agent Shared Memory pattern for full details.

Hierarchical Org Namespaces

Enterprise deployments with multiple business units: corp-{corp_id} > dept-{dept_id} > user-{user_id}. Each level has its own namespace and keys with appropriate scope. Admin key has cross-namespace audit access. Department keys have read-only to corp, read/write to dept. User keys isolated to their namespace only.

SDK Reference

OperationPythonTypeScriptPurpose
Create namespacecreate_namespace("tenant-xyz")createNamespace("tenant-xyz")Provision new tenant
List namespaceslist_namespaces()listNamespaces()Admin audit of all tenants
Get policyget_memory_policy("tenant-xyz")getMemoryPolicy("tenant-xyz")Check decay config per namespace
Store in namespacestore_memory("tenant-xyz", content, ...)storeMemory("tenant-xyz", {content, ...})Write to isolated tenant store
Recall from namespacerecall("tenant-xyz", query, top_k=5)recall("tenant-xyz", query, {top_k: 5})Search only within tenant
Delete namespacedelete_namespace("tenant-xyz")deleteNamespace("tenant-xyz")GDPR erasure / offboarding
Batch forgetbatch_forget(request)batchForget(request)Bulk delete within namespace

Real-World Example: SaaS Customer Support Platform

A customer support SaaS platform serves 500 business customers. Each business's conversations, customer profiles, and support knowledge base must be completely isolated from every other business. Here is how the namespace architecture maps to the product:

  • Namespace per business: tenant-{business_id} created on account signup. Contains all conversation histories, customer profiles, and KB articles.
  • Scoped key per business: The platform's backend uses this key for all Dakera API calls on behalf of that business. The key is stored in the platform's secrets manager, associated with the business_id.
  • Customer sub-namespacing via agent_id: Within a business's namespace, individual customers are distinguished by agent_id (e.g., customer-c_9k2m). This provides logical separation within the tenant namespace without requiring a separate namespace per customer.
  • GDPR compliance: When a customer requests data deletion, the platform calls batch_forget() for all memories with agent_id=customer-c_9k2m. When a business cancels, the platform calls delete_namespace() to atomically erase all their data in a single API call.
Namespace Deletion is Atomic and Immediate

When you call delete_namespace(), Dakera atomically removes the namespace from the routing table first, making it immediately inaccessible to any in-flight requests. The underlying data deletion is then completed asynchronously. This means you can call delete and immediately confirm to the user that their data is gone — no waiting for background jobs.

Request Routing: Namespace Access Control Flow

This diagram shows how every API request is validated against namespace permissions before any data operation is performed — ensuring tenant isolation is enforced at the infrastructure level.

API Request bearer: dk-abc123 Auth Check validate API key 401 Unauthorized Namespace Check key has access? 403 Forbidden Isolated Namespace scoped to tenant tenant_A tenant_B tenant_C no cross-tenant access possible Every request validated independently — zero implicit trust

Performance Considerations

<1ms
Namespace key validation overhead
10,000+
Namespaces per Dakera instance
<100ms
Namespace creation time

Namespace routing overhead is sub-millisecond — key validation and namespace routing add less than 1ms to every request. Dakera supports 10,000+ namespaces per instance with no degradation. Namespace creation is synchronous and completes in under 100ms, suitable for real-time user signup flows.

The main performance consideration is index size per namespace. Since each namespace has its own vector index, a namespace with 1M memories will have higher per-query latency than one with 10K. Consider setting decay policies at the namespace level to age out low-importance memories and keep each tenant's index lean.

Edge Cases

1. Namespace creation race on concurrent signups

Two users sign up simultaneously with the same organization, triggering two concurrent create_namespace("org-acme") calls. The second call will fail with a conflict error. Fix: treat namespace creation as idempotent — catch the conflict error and proceed. Or use a distributed lock on the namespace name in your signup flow.

2. Admin key exposure

The admin key has cross-namespace access. If it leaks, all tenant data is accessible. Fix: the admin key should never be used at runtime for tenant operations. Use it only for provisioning. Store it in a hardware security module (HSM) or secrets manager. Rotate it on any suspected compromise.

3. Orphaned namespaces from failed offboarding

Your offboarding flow fails after the user record is deleted but before delete_namespace() is called. The namespace persists with data but no corresponding user. Fix: use an event-driven architecture — publish a user.deleted event and have a Dakera cleanup consumer handle the namespace deletion with retry logic and a dead-letter queue for failures.

4. Namespace enumeration attack

An attacker who obtains a low-privilege API key tries to enumerate other tenant namespaces by guessing names. The 403 response leaks the existence of the namespace. Fix: use opaque UUIDs for namespace names rather than human-readable identifiers. tenant-550e8400-e29b-41d4-a716 is not guessable; tenant-acme-corp is.

5. Memory import between namespaces

A legitimate business request: migrate tenant data from tenant-acme-old to tenant-acme-new after an account restructure. There is no direct namespace-to-namespace copy API. Fix: use the admin key to read from the old namespace via recall() with a broad query, then re-store into the new namespace. For large datasets, use batch_recall() to paginate through all memories efficiently.

Never Use agent_id as the Only Isolation Mechanism

It is tempting to use a single namespace with different agent_id values to separate tenants (e.g., agent_id="tenant-acme" vs agent_id="tenant-beta"). This is not isolation. Any key with access to the namespace can query any agent_id within it. Use real namespaces with scoped keys for true tenant isolation.

Advanced Configuration — Per-Namespace Decay Policies & Rate Limiting

Per-Tenant Decay Configuration

Different tenants may need different memory lifecycle policies:

from dakera import DakeraClient

admin = DakeraClient(base_url="http://localhost:3300", api_key="dk-admin-key")

# Enterprise tenant: slow decay, long retention
admin.set_memory_policy("tenant-enterprise-corp", {
    "decay_rate": 0.01,       # very slow importance decay
    "min_importance": 0.1,    # keep everything above 0.1
    "ttl_default": None       # no default TTL
})

# Free-tier tenant: aggressive decay to save storage
admin.set_memory_policy("tenant-free-user-123", {
    "decay_rate": 0.1,        # fast decay
    "min_importance": 0.4,    # only keep important memories
    "ttl_default": 2592000    # 30-day default TTL
})

# Check a tenant's current policy
policy = admin.get_memory_policy("tenant-enterprise-corp")
print(policy)

Bulk Namespace Provisioning

import asyncio
from dakera import DakeraClient

admin = DakeraClient(base_url="http://localhost:3300", api_key="dk-admin-key")

async def provision_tenants(tenant_ids: list[str]):
    """Provision multiple tenants in parallel."""
    tasks = [admin.create_namespace(f"tenant-{tid}") for tid in tenant_ids]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for tid, result in zip(tenant_ids, results):
        if isinstance(result, Exception):
            print(f"Failed to provision {tid}: {result}")
        else:
            print(f"Provisioned: tenant-{tid}")

Namespace Inventory Reconciliation

def reconcile_namespaces(admin, active_tenant_ids: set):
    """Find and flag orphaned namespaces."""
    namespaces = admin.list_namespaces()
    tenant_namespaces = {
        ns["name"]: ns for ns in namespaces
        if ns["name"].startswith("tenant-")
    }

    # Find namespaces without active tenants
    for ns_name, ns_info in tenant_namespaces.items():
        tenant_id = ns_name.replace("tenant-", "")
        if tenant_id not in active_tenant_ids:
            print(f"ORPHANED: {ns_name} (created: {ns_info.get('created_at')})")
            # admin.delete_namespace(ns_name)  # uncomment to auto-clean

Implement Tenant Isolation Today

Dakera namespaces give you SOC2-ready data isolation in under 30 minutes. One namespace per tenant, zero cross-tenant leakage — guaranteed at the storage layer.

Set Up Tenant Isolation →