Tenant Quota

Per-tenant rate / byte / row quotas with enforcement helpers and audit emission on breach. Multi-tenant deployments need three things the framework's DB layer doesn't natively provide:

1. Storage caps — refuse INSERT when a tenant has consumed more than its allowance (defaultBytesCap, or a perTenantBytesCap[tenantId] override). 2. Query budgets — refuse SELECT when a tenant exceeds its rolling-window QPS or rows-read totals. 3. Isolation — every row a query reads under a claimed tenantId MUST belong to that tenant. Cross-tenant rows surface as db.tenant.crossover audit events.

Replaces the global maxRowsPerQuery knob for tenant-scoped scenarios — operators were previously forced to pick one global cap that would starve large tenants or under-cap small ones.

Storage-cap accounting: bytesUsed is computed by walking every table whose schema declares the configured tenantField and summing the textual length of every column for matching rows. The framework caches the per-tenant total for cacheTtlMs (default 30s) so a hot path doesn't pay the scan on every assert.

Query budget: sliding-window counter keyed (tenantId, windowStart). Window defaults to 60s. observe() rejects when either the QPS-equivalent call count exceeds perTenantQpsCap * window or the rows-read total exceeds perTenantTotalRowsRead.

Audit emissions: - tenant.quota.exceededassert() refused an insert/update - tenant.budget.exceededobserve() refused a query - db.tenant.crossoverinstrumentQuery saw rows belonging to the wrong tenant under the operator-claimed tenantId

SOC 2 CC6.1 ("logical access controls") + ISO 27001 A.8.1.5 ("classification of information") map directly onto this primitive — operators wire its emissions into the same audit chain auditors read.

b.tenantQuota.create(opts) #

0.7.0soc2gdpr
{
  {
    db:                 object,                    // required, b.db namespace
    tenantField:        string,                    // required, e.g. "tenantId"
    defaultBytesCap?:   number,                    // default: 1 GiB (C.BYTES.gib(1))
    perTenantBytesCap?: { [tenantId: string]: number },
    tables?:            string[],                  // override auto-detection
    audit?:             boolean,                   // default: true
    cacheTtlMs?:        number,                    // default: 30_000
  }
}

Build a per-tenant storage-cap enforcer. Returns an object exposing assert(tenantId) (throws TenantQuotaError on breach), snapshot(tenantId) (returns { tenantId, bytesUsed, bytesCap, percent }), list() (snapshot every distinct tenant), and invalidate(tenantId?) (drop the per-tenant cache so the next assert recomputes). The cache TTL trades freshness for cost on the hot path; bump it down for stricter limits.

var quota = b.tenantQuota.create({
  db:                b.db,
  tenantField:       "tenantId",
  defaultBytesCap:   b.constants.BYTES.gib(1),
  perTenantBytesCap: { "tenant-vip": b.constants.BYTES.gib(10) },
});
await quota.assert("tenant-acme");
// → { tenantId: "tenant-acme", bytesUsed: 12345, bytesCap: 1073741824, percent: 0.0000115 }

b.tenantQuota.budget(opts) #

0.7.0soc2
{
  {
    db:                      object,    // required, b.db namespace
    tenantField:             string,    // required
    perTenantQpsCap?:        number,    // default: 100 calls/sec
    perTenantTotalRowsRead?: number,    // default: 50_000 rows per window
    window?:                 number,    // default: 60_000 ms (C.TIME.minutes(1))
    audit?:                  boolean,   // default: true
  }
}

Build a per-tenant query-budget enforcer. Returns an object exposing observe(tenantId, info) (throws TenantQuotaError on breach), snapshot(tenantId) (returns the current window's counters), and reset(tenantId?) (drop counters). Sliding-window: every breach past the configured QPS or rows-read total emits tenant.budget.exceeded and refuses the call.

var budget = b.tenantQuota.budget({
  db:                     b.db,
  tenantField:            "tenantId",
  perTenantQpsCap:        100,
  perTenantTotalRowsRead: 50000,
  window:                 b.constants.TIME.minutes(1),
});
var snap = budget.observe("tenant-acme", { rowsRead: 12 });
// → { calls: 1, rowsRead: 12, windowMs: 60000 }

b.tenantQuota.instrumentQuery(opts) #

0.7.0soc2gdpr
{
  {
    rows:        object[],   // required, the query result rows
    tenantField: string,     // required, e.g. "tenantId"
    tenantId:    string,     // required, the operator-claimed tenant
    table?:      string,     // optional, recorded in the audit metadata
    audit?:      boolean,    // default: true
  }
}

Walk a result set and detect rows whose tenantField value disagrees with the operator-claimed tenantId — a multi-tenant isolation breach. Returns { ok, crossover } where crossover is the list of offending row indexes + their actual tenantId values. Audit emission db.tenant.crossover fires with a five-row sample when any breach is detected so the framework's chain-signed audit carries the forensic trail without dumping the whole result set.

var rows = [
  { _id: 1, tenantId: "tenant-acme", name: "ok" },
  { _id: 2, tenantId: "tenant-other", name: "leak" },
];
var result = b.tenantQuota.instrumentQuery({
  rows:        rows,
  tenantField: "tenantId",
  tenantId:    "tenant-acme",
  table:       "orders",
});
// → { ok: false, crossover: [{ index: 1, actualTenantId: "tenant-other" }] }

Last updated 2026-08-08T16:39:49.652Z by seeder.