Skip to content

Tenant Isolation

Hapnd is a multi-tenant platform. Every layer of the architecture enforces tenant boundaries — there is no shared state between tenants, and cross-tenant access is architecturally impossible, not just policy-restricted.

Each aggregate is stored in a Durable Object identified by {tenantId}:{aggregateId}. The tenant ID is part of the Durable Object’s name, which determines which instance handles the request. Two tenants with an aggregate called order_123 get two completely separate Durable Objects with independent SQLite databases.

There is no API path, query, or internal mechanism that allows one tenant’s Durable Object to access another’s.

API keys are SHA-256 hashed before storage. The raw key is returned once at creation time and never stored. Authentication resolves the tenant ID from the hashed key — every subsequent operation is scoped to that tenant.

Platform metadata (projection uploads, reducer bindings, notification configuration) is stored in Cloudflare D1. Every query includes tenant_id in the WHERE clause. This is enforced at the application layer — there are no admin queries that operate across tenants.

When customer code executes in a .NET container, the container is scoped to a single tenant via Cloudflare’s container binding model. The Event record passed to your reducer or projection includes the TenantId field, but the container itself cannot access any other tenant’s data.

Compiled DLLs and archived notification logs are stored in R2 with tenant-scoped paths:

  • DLLs: {tenantId}/reducers/{id}/compiled.dll or {tenantId}/projections/{id}/compiled.dll
  • Archives: {tenantId}/{projectionId}/{date}/notifications.ndjson

There is no shared prefix or cross-tenant path traversal possible.

The StreamingHub Durable Object is namespaced by {tenantId}:{projectionId}. WebSocket connections authenticate via the API key, which resolves the tenant. A client can only connect to projections belonging to their tenant.