Skip to content

Multi-Tenancy

Hybrid multi-tenancy model with subscription tiers and resource isolation.

Overview

Arbiter-Bot supports multiple users with a hybrid multi-tenancy model:

Tier Isolation Resources
Free/Basic Shared binary Strict quotas, token bucket rate limiting
Pro Shared binary Relaxed limits, priority execution
Enterprise Isolated pods Dedicated CPU/memory, guaranteed throughput

Subscription Tiers

Feature Matrix

Feature Free Pro Enterprise
Basic Trading Yes Yes Yes
Copy Trading 1 max 10 max 50 max
Arbitrage Engine No Yes Yes
Priority Execution No Yes Yes
Dedicated Support No No Yes
Max Positions 5 25 100
Max Position Size $100 $1,000 $10,000
Orders/Minute 10 60 600
API Requests/Second 10 60 300

Tier Implementation

use arbiter_engine::tenant::tier::{Tier, TierLimits, Feature};

// Get limits for a tier
let limits: TierLimits = Tier::Pro.limits();
println!("Max positions: {}", limits.max_positions);
println!("Arbitrage enabled: {}", limits.arbitrage_enabled);

// Check feature access
if Tier::Free.allows_feature(Feature::Arbitrage) {
    // Won't execute - arbitrage requires Pro+
}

TierLimits Structure

pub struct TierLimits {
    pub orders_per_minute: u32,
    pub max_positions: u32,
    pub max_copy_trades: u32,
    pub arbitrage_enabled: bool,
    pub max_position_size: f64,
    pub api_rate_limit: u32,
}

User Context

Each user gets a UserContext that enforces their tier's limits.

Creating User Context

use arbiter_engine::tenant::context::{UserContext, UserId};
use arbiter_engine::tenant::tier::Tier;

// Create context for specific tier
let user_id = UserId::new();
let ctx = UserContext::new(user_id, Tier::Pro);

// Convenience constructors
let free_ctx = UserContext::free(UserId::new());
let pro_ctx = UserContext::pro(UserId::new());
let enterprise_ctx = UserContext::enterprise(UserId::new());

Validating Operations

use arbiter_engine::tenant::tier::Feature;

// Check feature access
ctx.check_feature(Feature::Arbitrage)?;

// Check rate limits (consumes a token)
ctx.check_api_rate().await?;
ctx.check_order_rate().await?;

// Validate order parameters
ctx.validate_order(order_size_usd)?;

// Validate copy trade addition
ctx.validate_copy_trade()?;

Tracking Resource Usage

// Position tracking
ctx.add_position();
ctx.remove_position();
let count = ctx.position_count();

// Copy trade tracking
ctx.add_copy_trade();
ctx.remove_copy_trade();
let count = ctx.copy_trade_count();

// Check available rate limit tokens
let api_tokens = ctx.available_api_tokens();
let order_tokens = ctx.available_order_tokens();

Rate Limiting

Rate limiting uses the token bucket algorithm to control request rates.

How Token Bucket Works

Bucket Capacity: Maximum burst size
Refill Rate: Tokens added per second

┌─────────────────────────────┐
│  Tokens: ████████░░░░░░░░░  │  8/16 tokens available
│  Refill: 10 tokens/second   │
│  Capacity: 16 tokens        │
└─────────────────────────────┘

Request arrives → consume 1 token
Token available → request allowed
Token empty → request rejected

Creating Rate Limiters

use arbiter_engine::tenant::rate_limiter::{RateLimiter, RateLimiterBuilder};

// Basic rate limiter
let limiter = RateLimiter::new(
    10,  // capacity (burst)
    5,   // tokens per second (sustained rate)
);

// Using builder
let api_limiter = RateLimiterBuilder::for_api(60).build();   // 60 req/sec
let order_limiter = RateLimiterBuilder::for_orders(30).build(); // 30 orders/min

Using Rate Limiters

// Try to acquire a token (consumes on success)
match limiter.try_acquire().await {
    Ok(()) => {
        // Request allowed, proceed
    }
    Err(RateLimitError::LimitExceeded(capacity, window)) => {
        // Rate limited, return 429
    }
}

// Check without consuming (peek)
if limiter.check().await.is_ok() {
    // Would be allowed
}

// Get available tokens
let available = limiter.available_tokens();

// Reset to full capacity
limiter.reset().await;

Rate Limit Configuration

Tier API Burst API Sustained Order Burst Order Sustained
Free 20 10/sec 10 ~0.17/sec
Pro 120 60/sec 10 1/sec
Enterprise 600 300/sec 10 10/sec

Integration Pattern

Order Execution with Tenant Context

pub struct OrderExecutor {
    // ... exchange clients
}

impl OrderExecutor {
    pub async fn execute(&self, ctx: &UserContext, order: Order) -> Result<OrderId> {
        // 1. Check feature access
        if order.is_arbitrage() {
            ctx.check_feature(Feature::Arbitrage)?;
        }

        // 2. Check rate limits
        ctx.check_order_rate().await?;

        // 3. Validate order parameters
        ctx.validate_order(order.size_usd())?;

        // 4. Check position limits
        if order.opens_new_position() {
            // Validation happens inside
            ctx.validate_order(order.size_usd())?;
        }

        // 5. Execute order
        let order_id = self.submit_order(&ctx.user_id, order).await?;

        // 6. Update tracking
        if order.opens_new_position() {
            ctx.add_position();
        }

        Ok(order_id)
    }
}

gRPC Middleware Integration

The tenant middleware extracts user context from JWT claims:

use arbiter_engine::api::tenant_middleware::TenantManager;

// Middleware extracts user_id and tier from JWT
let tenant_manager = TenantManager::new();

// In gRPC service handler
pub async fn place_order(
    &self,
    request: Request<PlaceOrderRequest>,
) -> Result<Response<PlaceOrderResponse>, Status> {
    // Extract user context from request metadata
    let ctx = self.tenant_manager.get_context(&request)?;

    // All operations go through context
    ctx.check_order_rate().await
        .map_err(|e| Status::resource_exhausted(e.to_string()))?;

    // ... proceed with order
}

Data Isolation

PostgreSQL Row-Level Security

Data isolation is enforced at the database layer using RLS policies:

-- Enable RLS on tables
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE positions ENABLE ROW LEVEL SECURITY;

-- Policy: users see only their own data
CREATE POLICY user_orders ON orders
    USING (user_id = current_setting('app.current_user_id')::uuid);

CREATE POLICY user_positions ON positions
    USING (user_id = current_setting('app.current_user_id')::uuid);

-- Set user context before queries
SET app.current_user_id = 'user-uuid-here';

Setting User Context in Queries

// Before executing user-scoped queries
sqlx::query("SET app.current_user_id = $1")
    .bind(ctx.user_id.to_string())
    .execute(&pool)
    .await?;

// Now queries are automatically filtered
let orders = sqlx::query_as::<_, Order>("SELECT * FROM orders")
    .fetch_all(&pool)
    .await?;
// Returns only orders for current user

Credential Isolation

Each user's exchange credentials are encrypted with a user-specific key:

// Credentials stored encrypted per-user
struct EncryptedCredentials {
    user_id: UserId,
    exchange: Exchange,
    encrypted_key: Vec<u8>,      // Encrypted with user's key
    key_derivation_salt: Vec<u8>, // For HKDF
}

See ADR-009 for credential management details.

Error Handling

Context Errors

pub enum ContextError {
    // Feature not available on tier
    FeatureNotAvailable(String, Feature),

    // Rate limit exceeded
    RateLimited(RateLimitError),

    // Position limit exceeded
    PositionLimitExceeded(u32),

    // Order size exceeds tier limit
    OrderSizeExceeded(f64),

    // Copy trade limit exceeded
    CopyTradeLimitExceeded(u32),
}

HTTP/gRPC Status Mapping

Error HTTP Status gRPC Status
FeatureNotAvailable 403 Forbidden PERMISSION_DENIED
RateLimited 429 Too Many Requests RESOURCE_EXHAUSTED
PositionLimitExceeded 400 Bad Request FAILED_PRECONDITION
OrderSizeExceeded 400 Bad Request INVALID_ARGUMENT

Monitoring

Metrics to Track

Metric Description
tenant_api_requests_total Total API requests by tier
tenant_rate_limit_exceeded Rate limit rejections by tier
tenant_position_count Current positions by user
tenant_quota_utilization Percentage of quota used

Alerting Thresholds

Condition Alert
Quota utilization > 80% Warning
Rate limit exceeded > 100/min Warning
Single user > 50% of shared capacity Critical

Enterprise Isolation

Enterprise users get dedicated pod deployment:

# Kubernetes deployment for enterprise tenant
apiVersion: apps/v1
kind: Deployment
metadata:
  name: arbiter-enterprise-acme
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: arbiter-engine
        resources:
          requests:
            cpu: "2"
            memory: "4Gi"
          limits:
            cpu: "4"
            memory: "8Gi"
        env:
        - name: TENANT_ID
          value: "acme-corp"
        - name: TENANT_TIER
          value: "enterprise"

Best Practices

Do

  • Always validate operations through UserContext
  • Use rate limiters for all external-facing endpoints
  • Set database user context before queries
  • Monitor quota utilization per tenant
  • Alert on approaching quota exhaustion

Don't

  • Bypass context validation for "special" users
  • Store credentials without per-user encryption
  • Allow direct database access without RLS
  • Share rate limiter instances across users
  • Trust client-provided tier information