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
Related Documentation¶
- ADR-011: Multi-Tenancy Model - Architecture decision
- ADR-009: Credential Management - Credential isolation
- gRPC API Reference - API authentication
- Security Guide - Security considerations