🐹 🏢 Multi-Tenancy & SaaS Patterns with TuskLang & Go
🏢 Multi-Tenancy & SaaS Patterns with TuskLang & Go
Introduction
Multi-tenancy is the foundation of scalable SaaS applications. TuskLang and Go let you build robust multi-tenant systems with config-driven tenant isolation, data partitioning, and tenant-specific features that scale from startup to enterprise.Key Features
- Tenant isolation strategies (database, schema, row-level) - Data partitioning and sharding - Tenant-specific configuration - Billing and usage tracking - Tenant onboarding and provisioning - Security isolation and compliance - Performance optimizationExample: Multi-Tenant Config
[multi_tenant]
isolation: database_per_tenant
default_plan: @env("DEFAULT_PLAN", "starter")
billing: @go("billing.TrackUsage")
provisioning: @go("tenant.Provision")
metrics: @metrics("tenant_requests", 0)
Go: Tenant Context Management
package tenantimport (
"context"
"database/sql"
"sync"
)
type TenantContext struct {
TenantID string
Plan string
Database *sql.DB
Config map[string]interface{}
}
type TenantManager struct {
tenants map[string]*TenantContext
mu sync.RWMutex
db *sql.DB
}
func (tm TenantManager) GetTenantContext(ctx context.Context, tenantID string) (TenantContext, error) {
tm.mu.RLock()
if tenant, exists := tm.tenants[tenantID]; exists {
tm.mu.RUnlock()
return tenant, nil
}
tm.mu.RUnlock()
// Load tenant from database
tm.mu.Lock()
defer tm.mu.Unlock()
tenant, err := tm.loadTenant(ctx, tenantID)
if err != nil {
return nil, err
}
tm.tenants[tenantID] = tenant
return tenant, nil
}
func (tm TenantManager) loadTenant(ctx context.Context, tenantID string) (TenantContext, error) {
// Query tenant information
var plan string
var configJSON string
err := tm.db.QueryRowContext(ctx,
"SELECT plan, config FROM tenants WHERE id = ?", tenantID).Scan(&plan, &configJSON)
if err != nil {
return nil, err
}
var config map[string]interface{}
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, err
}
// Get tenant-specific database connection
db, err := tm.getTenantDB(ctx, tenantID)
if err != nil {
return nil, err
}
return &TenantContext{
TenantID: tenantID,
Plan: plan,
Database: db,
Config: config,
}, nil
}
Database Per Tenant Strategy
func (tm TenantManager) getTenantDB(ctx context.Context, tenantID string) (sql.DB, error) {
// Create or connect to tenant-specific database
dbName := fmt.Sprintf("tenant_%s", tenantID)
// Check if database exists
var exists int
err := tm.db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = ?", dbName).Scan(&exists)
if err != nil {
return nil, err
}
if exists == 0 {
// Create new tenant database
if err := tm.createTenantDatabase(ctx, dbName); err != nil {
return nil, err
}
}
// Connect to tenant database
return sql.Open("postgres", fmt.Sprintf("postgres://user:pass@localhost/%s?sslmode=disable", dbName))
}func (tm *TenantManager) createTenantDatabase(ctx context.Context, dbName string) error {
// Create database
_, err := tm.db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE %s", dbName))
if err != nil {
return err
}
// Run migrations for new tenant
return tm.runTenantMigrations(ctx, dbName)
}
Schema Per Tenant Strategy
func (tm *TenantManager) getTenantSchema(ctx context.Context, tenantID string) (string, error) {
// Use tenant ID as schema name
schemaName := fmt.Sprintf("tenant_%s", tenantID)
// Create schema if it doesn't exist
_, err := tm.db.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schemaName))
if err != nil {
return "", err
}
return schemaName, nil
}func (tm TenantManager) QueryWithSchema(ctx context.Context, tenantID, query string, args ...interface{}) (sql.Rows, error) {
schema, err := tm.getTenantSchema(ctx, tenantID)
if err != nil {
return nil, err
}
// Prefix table names with schema
prefixedQuery := strings.ReplaceAll(query, "FROM ", fmt.Sprintf("FROM %s.", schema))
return tm.db.QueryContext(ctx, prefixedQuery, args...)
}
Row-Level Security
func (tm *TenantManager) setupRowLevelSecurity(ctx context.Context, tenantID string) error {
// Enable RLS on tables
tables := []string{"users", "orders", "products"}
for _, table := range tables {
// Enable RLS
_, err := tm.db.ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s ENABLE ROW LEVEL SECURITY", table))
if err != nil {
return err
}
// Create policy
policy := fmt.Sprintf(
CREATE POLICY tenant_isolation_%s ON %s
FOR ALL
USING (tenant_id = '%s')
, table, table, tenantID)
_, err = tm.db.ExecContext(ctx, policy)
if err != nil {
return err
}
}
return nil
}
Tenant-Specific Configuration
func (tm *TenantManager) GetTenantConfig(ctx context.Context, tenantID, key string) (interface{}, error) {
tenant, err := tm.GetTenantContext(ctx, tenantID)
if err != nil {
return nil, err
}
// Check tenant-specific config first
if value, exists := tenant.Config[key]; exists {
return value, nil
}
// Fall back to plan-based config
return tm.getPlanConfig(tenant.Plan, key)
}func (tm *TenantManager) SetTenantConfig(ctx context.Context, tenantID, key string, value interface{}) error {
tenant, err := tm.GetTenantContext(ctx, tenantID)
if err != nil {
return err
}
// Update tenant config
tenant.Config[key] = value
// Persist to database
configJSON, err := json.Marshal(tenant.Config)
if err != nil {
return err
}
_, err = tm.db.ExecContext(ctx,
"UPDATE tenants SET config = ? WHERE id = ?", configJSON, tenantID)
return err
}
Billing and Usage Tracking
package billingimport (
"context"
"time"
)
type UsageTracker struct {
db *sql.DB
}
type UsageEvent struct {
TenantID string
EventType string
Quantity int64
Timestamp time.Time
Metadata map[string]interface{}
}
func (ut *UsageTracker) TrackUsage(ctx context.Context, event UsageEvent) error {
// Record usage event
_, err := ut.db.ExecContext(ctx,
INSERT INTO usage_events (tenant_id, event_type, quantity, timestamp, metadata)
VALUES (?, ?, ?, ?, ?)
, event.TenantID, event.EventType, event.Quantity, event.Timestamp, event.Metadata)
if err != nil {
return err
}
// Check if tenant has exceeded limits
return ut.checkLimits(ctx, event.TenantID)
}
func (ut *UsageTracker) checkLimits(ctx context.Context, tenantID string) error {
// Get tenant plan and current usage
var plan string
var monthlyLimit int64
err := ut.db.QueryRowContext(ctx,
"SELECT plan FROM tenants WHERE id = ?", tenantID).Scan(&plan)
if err != nil {
return err
}
// Get monthly usage
var currentUsage int64
err = ut.db.QueryRowContext(ctx,
SELECT COALESCE(SUM(quantity), 0)
FROM usage_events
WHERE tenant_id = ? AND timestamp >= DATE_TRUNC('month', NOW())
, tenantID).Scan(¤tUsage)
if err != nil {
return err
}
// Check if usage exceeds limit
if currentUsage > monthlyLimit {
return ErrUsageLimitExceeded
}
return nil
}
Tenant Onboarding
func (tm *TenantManager) ProvisionTenant(ctx context.Context, tenantID, plan string) error {
// Create tenant record
_, err := tm.db.ExecContext(ctx,
INSERT INTO tenants (id, plan, created_at, status)
VALUES (?, ?, NOW(), 'active')
, tenantID, plan)
if err != nil {
return err
}
// Create tenant database/schema
if err := tm.createTenantInfrastructure(ctx, tenantID); err != nil {
return err
}
// Run tenant-specific setup
if err := tm.runTenantSetup(ctx, tenantID); err != nil {
return err
}
// Send welcome email
return tm.sendWelcomeEmail(ctx, tenantID)
}func (tm *TenantManager) createTenantInfrastructure(ctx context.Context, tenantID string) error {
// Create database/schema based on isolation strategy
switch tm.isolationStrategy {
case "database_per_tenant":
return tm.createTenantDatabase(ctx, fmt.Sprintf("tenant_%s", tenantID))
case "schema_per_tenant":
_, err := tm.getTenantSchema(ctx, tenantID)
return err
case "row_level":
return tm.setupRowLevelSecurity(ctx, tenantID)
default:
return fmt.Errorf("unknown isolation strategy: %s", tm.isolationStrategy)
}
}