---
title: "Multi-Tenancy"
description: "Tenant isolation with per-tenant keys, quota enforcement, and scoped middleware."
---

Trove provides built-in support for multi-tenant storage through tenant keys on data models, per-tenant quota enforcement, and middleware scoping. When deployed with the Forge extension, tenant identification is handled automatically via the `X-Subject-ID` header.

## Tenant Key Model

Every storage model carries a `TenantKey` field that associates the record with a specific tenant:

### Bucket Model

```go
type Bucket struct {
    ID        string `json:"id"`
    Name      string `json:"name"`
    TenantKey string `json:"tenant_key,omitempty"`
    // ... other fields
}
```

### Object Model

```go
type Object struct {
    ID        string `json:"id"`
    BucketID  string `json:"bucket_id"`
    Key       string `json:"key"`
    TenantKey string `json:"tenant_key,omitempty"`
    // ... other fields
}
```

### Quota Model

Quotas are tracked per-tenant with both byte and object count limits:

```go
type Quota struct {
    TenantKey    string `json:"tenant_key"`
    UsedBytes    int64  `json:"used_bytes"`
    LimitBytes   int64  `json:"limit_bytes"`
    ObjectCount  int64  `json:"object_count"`
    LimitObjects int64  `json:"limit_objects"`
    UpdatedAt    time.Time `json:"updated_at"`
}
```

## Per-Tenant Quota Enforcement

When a tenant's usage exceeds their configured limits, `Put` operations return `trove.ErrQuotaExceeded`:

```go
// Upload fails when quota is exceeded
_, err := t.Put(ctx, "uploads", "large-file.bin", data)
if errors.Is(err, trove.ErrQuotaExceeded) {
    // Tenant has exceeded their storage quota
    http.Error(w, "Storage quota exceeded", http.StatusInsufficientStorage)
}
```

Quota limits apply to both total bytes and total object count. The Forge extension enforces these limits automatically in the upload handlers.

## Tenant Identification

### With Forge Extension

When running inside Forge, the tenant is identified by the `X-Subject-ID` HTTP header, which is typically set by authentication middleware (e.g., from a JWT or API key).

```
PUT /api/trove/buckets/uploads/objects/report.pdf
X-Subject-ID: tenant-abc
Content-Type: application/pdf
```

The extension automatically stamps every created bucket and object with the subject ID as the `TenantKey`.

### Standalone Mode

Without Forge, set the tenant context explicitly using the store's tenant-aware list options or by managing the `TenantKey` field directly when writing metadata:

```go
// Filter objects by tenant when listing
objects, err := store.ListObjects(ctx, "uploads",
    store.WithTenantKey("tenant-abc"),
)
```

## Middleware Scoping by Tenant

Use `ScopeFunc` to apply middleware only to specific tenants. This enables per-tenant encryption keys, compression settings, or scanning policies.

```go
import "github.com/xraph/trove/middleware"

// Extract tenant from context
tenantScope := &middleware.ScopeFunc{
    Fn: func(ctx context.Context, bucket, key string) bool {
        tenantID := ctx.Value("tenant_id").(string)
        return tenantID == "acme-corp"
    },
    Desc: "tenant(acme-corp)",
}

// Encrypt only Acme Corp's data with their key
t, _ := trove.Open(drv,
    trove.WithScopedMiddleware(tenantScope,
        encrypt.New(encrypt.WithKeyProvider(acmeKeyProvider)),
    ),
)
```

### Per-Tenant Encryption Keys

Combine `ScopeFunc` with a dynamic `KeyProvider` that returns different keys per tenant:

```go
type TenantKeyProvider struct {
    vault VaultClient
}

func (p *TenantKeyProvider) Key(ctx context.Context) ([]byte, error) {
    tenantID := ctx.Value("tenant_id").(string)
    return p.vault.GetKey(ctx, "trove/keys/"+tenantID)
}

t, _ := trove.Open(drv,
    trove.WithMiddleware(
        encrypt.New(encrypt.WithKeyProvider(&TenantKeyProvider{vault: vc})),
    ),
)
```

## Integration with Warden

When used alongside the Warden extension (Forge's authorization engine), Trove checks access policies before every operation. Warden policies can be tenant-scoped:

```go
// Warden policy: only allow access to own tenant's buckets
// Policy evaluation happens in the Forge extension's middleware layer

// The X-Subject-ID header identifies the tenant
// Warden checks: does subject "tenant-abc" have permission to
// read from bucket "uploads" with tenant_key "tenant-abc"?
```

This provides defense-in-depth: tenant isolation at the data model level (TenantKey filtering), the quota level (per-tenant limits), the middleware level (scoped transformations), and the authorization level (Warden policies).

## Tenant Isolation Guarantees

| Layer | Mechanism | Prevents |
|-------|-----------|----------|
| Data model | `TenantKey` field on Bucket, Object, Quota | Cross-tenant data access |
| Quota | Per-tenant byte and object limits | One tenant consuming all storage |
| Middleware | `ScopeFunc` with tenant predicate | Applying wrong encryption keys |
| Authorization | Warden policy checks on `X-Subject-ID` | Unauthorized API access |
| Routing | Per-tenant backend routing | Co-mingling tenant data on disk |

## Cross-Tenant Prevention

- Listing operations are scoped by `TenantKey` -- tenants only see their own resources
- There is no built-in API to query across tenants
- Admin or superuser access requires explicit bypass logic in your application layer
- Quota enforcement is per-tenant and cannot be circumvented by API calls
