Trove

Multi-Tenancy

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

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

Object Model

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:

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:

// 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:

// 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.

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:

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:

// 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

LayerMechanismPrevents
Data modelTenantKey field on Bucket, Object, QuotaCross-tenant data access
QuotaPer-tenant byte and object limitsOne tenant consuming all storage
MiddlewareScopeFunc with tenant predicateApplying wrong encryption keys
AuthorizationWarden policy checks on X-Subject-IDUnauthorized API access
RoutingPer-tenant backend routingCo-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

On this page