Native Multi-Tenancy with Row-Level Security
HeliosDB Lite isolates the tenants of a shared database with row-level security: each tenant gets its own role, a policy ties the rows of a shared table to current_user, and the server applies the policy to every PostgreSQL wire session and to every REST request, which runs as the role named in its token, so a tenant filter missing from application code does not expose another tenant's rows.
Superusers bypass row-level security; give each tenant its own non-superuser role and start the server with SCRAM authentication so clients cannot claim another role. Read the documentation.
Executive Summary
This embedded multi-tenancy architecture enables bootstrapped SaaS startups to build secure, scalable B2B platforms with predictable costs, allows enterprises to consolidate departmental data silos while maintaining strict access controls, and empowers ISVs to deploy white-label solutions with complete customer data separation in resource-constrained edge environments.
Problem Being Solved
Core Problem Statement
Teams need zero-trust tenant isolation built into the database layer that works transparently for developers, scales from 10 to 10,000 customers without architectural changes, and operates in resource-constrained environments without external dependencies.
HeliosDB Lite Solution
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ HeliosDB-Lite Multi-Tenant Layer │
├─────────────────────────────────────────────────────────────────┤
│ Tenant Session Context (Current Tenant ID) │
│ ┌─────────────────┬─────────────────┬─────────────────┐ │
│ │ Tenant A Context│ Tenant B Context│ Tenant C Context│ │
│ │ (UUID: aaa...) │ (UUID: bbb...) │ (UUID: ccc...) │ │
│ └─────────────────┴─────────────────┴─────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Query Rewriter (Automatic Tenant Filtering) │
│ • Inject: AND tenant_id = <session_tenant> to WHERE clauses │
│ • Apply to: SELECT, UPDATE, DELETE, JOIN conditions │
│ • Enforce: Deny queries without tenant context │
├─────────────────────────────────────────────────────────────────┤
│ Resource Quota Enforcer │
│ ┌──────────────────┬──────────────────┬──────────────────┐ │
│ │ Storage Quota │ Connection Limit │ QPS Rate Limiter │ │
│ │ (100GB per tenant│ (50 per tenant) │ (1000 per tenant)│ │
│ └──────────────────┴──────────────────┴──────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Isolation Mode Manager │
│ • Shared Schema (RLS): Single table, WHERE tenant_id filtering │
│ • DB-per-Tenant: Separate RocksDB ColumnFamilies │
│ • Schema-per-Tenant: Namespace prefixing (tenant_A.table) │
├─────────────────────────────────────────────────────────────────┤
│ SQL Query Engine (Tenant-Aware Execution) │
├─────────────────────────────────────────────────────────────────┤
│ Storage Engine (RocksDB with Tenant Isolation) │
│ • RLS Mode: Keys include tenant_id prefix │
│ • DB-per-Tenant: Separate ColumnFamilies │
│ • Schema-per-Tenant: Namespace-based keys │
└─────────────────────────────────────────────────────────────────┘
Concrete Examples with Code, Config & Architecture
Example 1: SaaS B2B Platform (Shared Schema RLS) - Embedded Configuration
Architecture:
SaaS Application Server (Rust/Axum)
↓
HeliosDB-Lite Client Library (Embedded)
├── Tenant Session Context (current_tenant_id from JWT)
├── Query Rewriter (auto-inject WHERE tenant_id = ?)
└── Resource Quota Enforcer (storage, connections, QPS)
↓
In-Process RocksDB Storage (Shared Schema Mode)
├── Table: projects (tenant_id, project_id, name, ...)
├── Table: tasks (tenant_id, task_id, project_id, ...)
└── Index: idx_tenant_project (tenant_id, project_id)
↓
Local File System (/data/heliosdb/)
Configuration (heliosdb.toml):
# HeliosDB-Lite Multi-Tenant SaaS Configuration
[database]
path = "/data/heliosdb/saas-platform.db"
memory_limit_mb = 4096
enable_wal = true
page_size = 4096
[multi_tenancy]
enabled = true
isolation_mode = "shared_schema" # Shared Schema with RLS
tenant_id_column = "tenant_id" # Standard column name for filtering
# Default resource limits (can override per tenant)
[multi_tenancy.default_limits]
max_storage_bytes = 107374182400 # 100 GB per tenant
max_connections = 50 # 50 concurrent connections per tenant
max_qps = 1000 # 1,000 queries/second per tenant
# Tenant provisioning
[multi_tenancy.provisioning]
auto_create_tenants = true # Auto-provision on first access
require_explicit_context = true # Reject queries without tenant context
# Security settings
[multi_tenancy.security]
enforce_row_level_security = true # Enable RLS filtering
deny_cross_tenant_joins = true # Prevent accidental cross-tenant queries
audit_tenant_access = true # Log all tenant context switches
[monitoring]
metrics_enabled = true
tenant_metrics = true # Track per-tenant query counts, latency
verbose_logging = false
Example 2: Enterprise Department Isolation (Database-per-Tenant) - Python Integration
Scenario: Fortune 500 enterprise with 200 departments (HR, Finance, Sales, Legal, etc.) needing absolute data isolation for compliance (HR payroll data must never be accessible to other departments), deployed on-premise with strict audit requirements, each department 50-500 users, 10-100GB data.
Architecture Pattern:
┌─────────────────────────────────────────────────────────────┐
│ Enterprise Application (Python/Flask) │
├─────────────────────────────────────────────────────────────┤
│ Department Context (from SAML/LDAP authentication) │
│ ├── HR Tenant Context │
│ ├── Finance Tenant Context │
│ ├── Sales Tenant Context │
│ └── Legal Tenant Context │
├─────────────────────────────────────────────────────────────┤
│ HeliosDB-Lite Python Bindings (FFI to Rust) │
├─────────────────────────────────────────────────────────────┤
│ Multi-Tenant Isolation Layer (Database-per-Tenant) │
│ ├── HR Database (RocksDB ColumnFamily 1) │
│ ├── Finance Database (RocksDB ColumnFamily 2) │
│ ├── Sales Database (RocksDB ColumnFamily 3) │
│ └── Legal Database (RocksDB ColumnFamily 4) │
├─────────────────────────────────────────────────────────────┤
│ In-Process RocksDB Storage Engine │
│ • Physical Isolation via ColumnFamilies │
│ • No shared data structures between departments │
│ • Separate WAL logs per tenant │
└─────────────────────────────────────────────────────────────┘
Example 3: White-Label ISV Platform (Schema-per-Tenant) - Container Deployment
Scenario: Marketing automation platform sold to 500 agencies/resellers as white-labeled product, each reseller serves 50-200 end clients, requires complete branding isolation (Reseller A's clients cannot discover Reseller B exists), deployed in Docker containers on reseller-owned infrastructure (on-premise, private cloud).
Docker Deployment (Dockerfile):
FROM rust:latest as builder
WORKDIR /app
# Copy HeliosDB-Lite source
COPY . .
# Build white-label application with multi-tenancy
RUN cargo build --release --features multi-tenancy,encryption
# Runtime stage (minimal)
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/whitelabel-saas /usr/local/bin/
# Create data directory for tenant databases
RUN mkdir -p /data/tenants
# Expose API port
EXPOSE 8080
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
VOLUME ["/data"]
ENTRYPOINT ["whitelabel-saas"]
CMD ["--config", "/etc/heliosdb/config.toml", "--data-dir", "/data"]
Docker Compose (docker-compose.yml):
version: '3.8'
services:
whitelabel-saas:
build:
context: .
dockerfile: Dockerfile
image: whitelabel-saas:latest
container_name: whitelabel-prod
ports:
- "8080:8080" # API server
volumes:
- ./data:/data # Persistent tenant data
- ./config/whitelabel.toml:/etc/heliosdb/config.toml:ro
environment:
RUST_LOG: "info,whitelabel=debug"
HELIOSDB_MULTI_TENANT: "true"
HELIOSDB_ISOLATION_MODE: "schema_per_tenant"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
networks:
- app-network
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
networks:
app-network:
driver: bridge
volumes:
tenant_data:
driver: local
Configuration for White-Label (whitelabel.toml):
[server]
host = "0.0.0.0"
port = 8080
workers = 4
[database]
path = "/data/whitelabel.db"
memory_limit_mb = 2048
enable_wal = true
page_size = 4096
[multi_tenancy]
enabled = true
isolation_mode = "schema_per_tenant" # Balance of isolation + efficiency
tenant_id_column = "tenant_id"
# White-label specific limits per reseller
[multi_tenancy.default_limits]
max_storage_bytes = 53687091200 # 50 GB per reseller
max_connections = 75 # 75 concurrent connections
max_qps = 2000 # 2,000 queries/second
[multi_tenancy.schema_isolation]
namespace_prefix = "tenant" # Schema names: tenant_<uuid>
allow_cross_schema_queries = false # Prevent reseller discovery
hide_other_schemas = true # Don't list other tenants' schemas
[encryption]
enabled = true
key_source = "env" # Read from HELIOSDB_ENCRYPTION_KEY
algorithm = "aes-256-gcm"
[audit]
enabled = true
log_tenant_provisioning = true
log_cross_tenant_attempts = true # Alert on isolation violations
Example 4: Development Environments (Tenant-as-Branch) - Go Microservice
Scenario: Engineering team with 50 developers, each needs isolated dev environment (dev, staging, prod as tenants), shared schema for consistency, instant environment creation for feature branches, deployed as Go microservice.
Go Service Integration (using cgo bindings):
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/google/uuid"
"github.com/gorilla/mux"
heliosdb "example.com/heliosdb-lite/go-bindings"
)
type Environment string
const (
EnvDev Environment = "dev"
EnvStaging Environment = "staging"
EnvProd Environment = "prod"
)
// Map environments to tenant IDs
var envTenantMap = map[Environment]uuid.UUID{
EnvDev: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
EnvStaging: uuid.MustParse("22222222-2222-2222-2222-222222222222"),
EnvProd: uuid.MustParse("33333333-3333-3333-3333-333333333333"),
}
type AppContext struct {
db *heliosdb.Connection
}
type Feature struct {
ID uuid.UUID `json:"id"`
TenantID uuid.UUID `json:"tenant_id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Environment string `json:"environment"`
}
func main() {
// Initialize HeliosDB with multi-tenancy
config := heliosdb.Config{
Path: "./app-data.db",
MemoryLimitMB: 1024,
MultiTenancy: heliosdb.MultiTenancyConfig{
Enabled: true,
IsolationMode: heliosdb.SharedSchema,
DefaultLimits: heliosdb.ResourceLimits{
MaxStorageBytes: 10 * 1024 * 1024 * 1024, // 10 GB per env
MaxConnections: 50,
MaxQPS: 1000,
},
},
}
db, err := heliosdb.Open(config)
if err != nil {
log.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Provision environments as tenants
provisionEnvironments(db)
// Initialize schema (shared across all environments)
initSchema(db)
// Setup HTTP server
ctx := &AppContext{db: db}
router := mux.NewRouter()
router.HandleFunc("/features", ctx.createFeature).Methods("POST")
router.HandleFunc("/features", ctx.listFeatures).Methods("GET")
router.HandleFunc("/env/{env}/promote", ctx.promoteEnvironment).Methods("POST")
log.Println("Microservice running on :8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
func provisionEnvironments(db *heliosdb.Connection) {
for env, tenantID := range envTenantMap {
err := db.ProvisionTenant(heliosdb.TenantConfig{
ID: tenantID,
Name: string(env),
IsolationMode: heliosdb.SharedSchema,
})
if err != nil {
log.Printf("Tenant %s already exists: %v", env, err)
} else {
log.Printf("Provisioned environment: %s", env)
}
}
}
func initSchema(db *heliosdb.Connection) {
// Run in dev environment context for schema creation
db.SetTenantContext(envTenantMap[EnvDev])
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS features (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name TEXT NOT NULL,
enabled BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
log.Fatalf("Schema creation failed: %v", err)
}
// Index for tenant isolation
_, err = db.Exec(`
CREATE INDEX IF NOT EXISTS idx_features_tenant
ON features(tenant_id, name)
`)
if err != nil {
log.Fatalf("Index creation failed: %v", err)
}
}
func (ctx *AppContext) createFeature(w http.ResponseWriter, r *http.Request) {
// Extract environment from header
env := Environment(r.Header.Get("X-Environment"))
tenantID, ok := envTenantMap[env]
if !ok {
http.Error(w, "Invalid environment", http.StatusBadRequest)
return
}
// Set tenant context
ctx.db.SetTenantContext(tenantID)
var req struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
featureID := uuid.New()
// Insert with automatic tenant_id injection
_, err := ctx.db.Exec(
`INSERT INTO features (id, tenant_id, name, enabled)
VALUES ($1, CURRENT_TENANT_ID(), $2, $3)`,
featureID, req.Name, req.Enabled,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
feature := Feature{
ID: featureID,
TenantID: tenantID,
Name: req.Name,
Enabled: req.Enabled,
Environment: string(env),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(feature)
}
func (ctx *AppContext) listFeatures(w http.ResponseWriter, r *http.Request) {
env := Environment(r.Header.Get("X-Environment"))
tenantID, ok := envTenantMap[env]
if !ok {
http.Error(w, "Invalid environment", http.StatusBadRequest)
return
}
ctx.db.SetTenantContext(tenantID)
// Query WITHOUT explicit WHERE tenant_id (auto-filtered)
rows, err := ctx.db.Query(
`SELECT id, tenant_id, name, enabled
FROM features
ORDER BY name`,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var features []Feature
for rows.Next() {
var f Feature
err := rows.Scan(&f.ID, &f.TenantID, &f.Name, &f.Enabled)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
f.Environment = string(env)
features = append(features, f)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(features)
}
func (ctx *AppContext) promoteEnvironment(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sourceEnv := Environment(vars["env"])
// Promote: dev -> staging -> prod
var targetEnv Environment
switch sourceEnv {
case EnvDev:
targetEnv = EnvStaging
case EnvStaging:
targetEnv = EnvProd
default:
http.Error(w, "Cannot promote production", http.StatusBadRequest)
return
}
sourceTenant := envTenantMap[sourceEnv]
targetTenant := envTenantMap[targetEnv]
// Copy features from source to target environment
ctx.db.SetTenantContext(sourceTenant)
rows, err := ctx.db.Query(`SELECT id, name, enabled FROM features`)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
ctx.db.SetTenantContext(targetTenant)
promoted := 0
for rows.Next() {
var id uuid.UUID
var name string
var enabled bool
rows.Scan(&id, &name, &enabled)
// Insert into target environment
_, err := ctx.db.Exec(
`INSERT INTO features (id, tenant_id, name, enabled)
VALUES ($1, CURRENT_TENANT_ID(), $2, $3)
ON CONFLICT (id) DO UPDATE SET enabled = $3`,
id, name, enabled,
)
if err == nil {
promoted++
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": fmt.Sprintf("Promoted %s to %s", sourceEnv, targetEnv),
"promoted": promoted,
})
}
Example 5: Geographic Compliance Isolation (Data Residency) - Edge Deployment
Scenario: Global SaaS serving EU, US, and APAC customers with GDPR/data residency requirements, each region deployed as separate tenant in edge locations (EU data must stay in EU), handling 100K users per region, requires audit trail of cross-region access attempts.
Edge Configuration (per region):
[database]
path = "/var/lib/heliosdb/region-eu.db" # Separate DB per region
memory_limit_mb = 2048
enable_wal = true
page_size = 4096
[multi_tenancy]
enabled = true
isolation_mode = "database_per_tenant" # Strongest isolation for compliance
tenant_id_column = "tenant_id"
# Region-specific tenant (EU, US, APAC)
[multi_tenancy.region]
region_id = "eu-west-1"
data_residency_enforcement = true # Prevent cross-region queries
audit_cross_region_access = true # Log any attempts
[multi_tenancy.default_limits]
max_storage_bytes = 1099511627776 # 1 TB per region
max_connections = 500
max_qps = 10000
[encryption]
enabled = true
key_source = { Environment = "HELIOSDB_ENCRYPTION_KEY" } # per-region key from your secret manager; cloud KMS key sources are planned
[audit]
enabled = true
log_data_access = true
log_cross_region_attempts = true
retention_days = 2555 # 7 years (GDPR requirement)
[compliance]
gdpr_mode = true
data_residency_region = "EU"
allow_cross_border_transfer = false
Adoption Strategy
Phase 1: Proof of Concept (Weeks 1-4)
Target: Validate multi-tenancy in development environment with 10-50 test tenants
Tactics:
- Deploy HeliosDB Lite with shared schema RLS mode in staging environment
- Create 10 test tenants representing real customer segments
- Implement tenant context extraction from JWT/session
- Run isolation tests: verify tenant A cannot access tenant B's data
- Measure provisioning speed, query performance with tenant filtering
- Test resource quota enforcement (storage, connection limits)
Phase 2: Pilot Deployment (Weeks 5-12)
Target: Limited production deployment with 50-200 real customers
Tactics:
- Run parallel systems (old single-tenant + new multi-tenant) for validation
- Monitor per-tenant metrics: query latency, error rates, resource usage
- Implement tenant-aware dashboards and alerting
- Collect customer feedback on isolation, performance, reliability
- Test failure scenarios: tenant quota exceeded, rapid tenant provisioning spikes
Phase 3: Full Rollout
Target: Organization-wide deployment with 1,000-10,000 tenants
Tactics:
- Gradual migration of remaining customers to multi-tenant platform
- Automated tenant provisioning integrated with signup/onboarding flow
- Implement tenant lifecycle management: suspend, archive, delete
- Deploy comprehensive monitoring: per-tenant dashboards, quota alerts, isolation audits
- Establish runbooks for common scenarios: quota increase, tenant migration, isolation incident response
- Optimize for scale: cache tenant metadata, batch quota enforcement, index tuning
Conclusion
Next Steps: Start with a 4-week proof-of-concept deploying shared schema RLS mode with 10 test tenants, validate isolation through automated leak tests, measure provisioning speed and query performance, then expand to pilot deployment with 50-200 real customers over 8 weeks before full production rollout.
Performance depends on workload and hardware. Published figures are measured on representative fixtures; see the benchmarks page and reproduce them on your own workload.