tenant

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 2, 2025 License: MIT Imports: 3 Imported by: 0

README

Multi-Tenancy Support

This package implements database-level multi-tenancy for Cedros Pay Server, enabling SaaS/platform use cases like Shopify integrations, WooCommerce plugins, and enterprise deployments.

Why Multi-Tenancy?

The Problem: Without multi-tenancy support, adding tenant_id to billions of rows later = downtime nightmare + migration pain.

The Solution: Add tenant_id columns NOW (while tables are small) with backwards-compatible defaults.

Architecture

Database-Level Isolation

All tables include tenant_id column with tenant-scoped indexes:

CREATE TABLE products (
    id TEXT PRIMARY KEY,
    tenant_id TEXT NOT NULL DEFAULT 'default',
    ...
);

CREATE INDEX idx_products_tenant_active ON products(tenant_id, active);

Backwards Compatibility

  • Default Tenant: All existing data uses tenant_id='default'
  • Single-Tenant Mode: Works without any changes (uses default tenant)
  • Migration Path: Enable multi-tenancy when needed without data migration

Usage

Single-Tenant Deployment (Default)

No changes required. All data automatically uses tenant_id='default':

// Existing code continues to work unchanged
store.SaveCartQuote(ctx, quote)  // Uses default tenant

Multi-Tenant Deployment

Enable tenant extraction middleware in server configuration:

// In server.go ConfigureRouter
router.Use(tenant.Extraction)  // Extract tenant from request

Tenant Extraction Methods

The middleware extracts tenant ID using multiple methods (in priority order):

curl -H "X-Tenant-ID: acme-corp" https://api.cedrospay.com/products
# Subdomain automatically extracted
https://acme-corp.api.cedrospay.com/products → tenant_id="acme-corp"
// Auth middleware sets tenant from JWT
claims := parseJWT(token)
ctx = tenant.WithTenant(ctx, claims.TenantID)

Accessing Tenant in Code

func (h *handlers) someHandler(w http.ResponseWriter, r *http.Request) {
    tenantID := tenant.FromContext(r.Context())
    // Use tenantID for tenant-scoped queries
}

Enterprise Use Cases

1. Shopify App Integration

Shopify Store → shopify_store_12345.api.cedrospay.com
Each store gets isolated tenant with own:
- Products catalog
- Payment records
- Coupons
- Stripe connected account

2. WooCommerce Plugin

WooCommerce Site → X-Tenant-ID: woo_site_abc123
Plugin sends tenant ID with each API request
Merchant dashboard shows only their tenant's data

3. White-Label SaaS

Customer A → customer-a.payments.example.com
Customer B → customer-b.payments.example.com
Each customer sees isolated payment data

Security & Compliance

Database-Level Isolation

All queries automatically scoped by tenant:

-- Single-tenant query (old)
SELECT * FROM products WHERE active = true;

-- Multi-tenant query (new)
SELECT * FROM products WHERE tenant_id = 'acme-corp' AND active = true;

GDPR Compliance

  • Data Deletion: Delete all data for specific tenant
  • Data Export: Export all tenant data for portability
  • Isolation: Tenant data never mixes with other tenants

PCI-DSS Compliance

  • Tenant Segregation: Payment data isolated per tenant
  • Access Control: Tenant can only access their own payment records

Migration Guide

Step 1: Run Database Migration

# Apply migration 006 to add tenant_id columns
psql -d cedros_pay < migrations/006_add_multi_tenancy.sql

Important: This migration is BACKWARDS COMPATIBLE. All existing data gets tenant_id='default'.

Step 2: Enable Tenant Middleware (Optional)

For multi-tenant deployments, add middleware:

router.Use(tenant.Extraction)

For single-tenant deployments, NO CODE CHANGES needed.

Step 3: Add Tenant-Aware Queries (When Needed)

Update storage layer to filter by tenant:

// Before (single-tenant)
func (r *ProductRepository) ListProducts(ctx context.Context) ([]Product, error) {
    query := "SELECT * FROM products WHERE active = true"
    // ...
}

// After (multi-tenant aware)
func (r *ProductRepository) ListProducts(ctx context.Context) ([]Product, error) {
    tenantID := tenant.FromContext(ctx)
    query := "SELECT * FROM products WHERE tenant_id = $1 AND active = true"
    // ...
}

Performance Considerations

Composite Indexes

All tenant queries use composite indexes for optimal performance:

-- Efficiently handles tenant-scoped queries
CREATE INDEX idx_products_tenant_active ON products(tenant_id, active);

Query Planning

PostgreSQL query planner uses tenant indexes automatically:

EXPLAIN SELECT * FROM products WHERE tenant_id = 'acme' AND active = true;
-- Uses: idx_products_tenant_active

Future Enhancements

Tenant Management Table (Planned)

CREATE TABLE tenants (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    stripe_account_id TEXT,    -- Connected Stripe account
    solana_wallet TEXT,          -- Tenant's payment wallet
    rate_limits JSONB,           -- Per-tenant rate limits
    features JSONB,              -- Feature flags per tenant
    active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP NOT NULL
);

Tenant Provisioning API (Planned)

POST /admin/tenants
{
  "id": "acme-corp",
  "name": "Acme Corporation",
  "stripeAccountId": "acct_xxx",
  "solanaWallet": "xxx..."
}

Tenant-Specific Settings (Planned)

type TenantSettings struct {
    RateLimits      RateLimitSettings
    Features        FeatureFlags
    StripeAccountID string
    SolanaWallet    string
}

Testing

Run tenant isolation tests:

go test ./internal/tenant -v

Verify tenant index performance:

EXPLAIN ANALYZE
SELECT * FROM products WHERE tenant_id = 'test' AND active = true;

Troubleshooting

Issue: Tenant not being extracted

Solution: Ensure tenant extraction middleware is enabled:

router.Use(tenant.Extraction)

Issue: Cross-tenant data leakage

Solution: Verify all queries filter by tenant_id:

-- ❌ Wrong: Missing tenant filter
SELECT * FROM products WHERE active = true;

-- ✅ Correct: Includes tenant filter
SELECT * FROM products WHERE tenant_id = $1 AND active = true;

Issue: Performance degradation

Solution: Ensure tenant indexes exist:

\di+ idx_products_tenant_active  -- Check index exists
EXPLAIN SELECT ...               -- Verify index is used

Standards & Best Practices

  • Tenant ID Format: Alphanumeric + hyphens/underscores only (max 64 chars)
  • Default Tenant: Always use 'default' for single-tenant mode
  • Context Propagation: Always pass tenant via context, never global variables
  • Index Strategy: Composite indexes with tenant_id as first column

Documentation

Index

Constants

View Source
const DefaultTenantID = "default"

DefaultTenantID is used for single-tenant deployments and backwards compatibility

Variables

This section is empty.

Functions

func Extraction

func Extraction(next http.Handler) http.Handler

Extraction handles tenant ID extraction from HTTP requests Supports multiple extraction methods (in priority order):

  1. X-Tenant-ID header (explicit tenant specification)
  2. JWT claims (tenant_id field in auth token)
  3. Subdomain (tenant1.api.cedrospay.com → tenant1)
  4. Default tenant for backwards compatibility

This middleware is OPTIONAL - single-tenant deployments don't need it. Multi-tenant deployments should enable it via config.

func FromContext

func FromContext(ctx context.Context) string

FromContext retrieves the tenant ID from the request context Returns DefaultTenantID if no tenant is set (backwards compatible)

func WithTenant

func WithTenant(ctx context.Context, tenantID string) context.Context

WithTenant adds the tenant ID to the context

Types

type FeatureFlags

type FeatureFlags struct {
	GaslessTransactions bool
	RefundsEnabled      bool
	CouponsEnabled      bool
	WebhooksEnabled     bool
}

FeatureFlags controls tenant-specific feature access

type NoopValidator

type NoopValidator struct{}

NoopValidator always returns true (for single-tenant deployments)

func (NoopValidator) GetTenantSettings

func (NoopValidator) GetTenantSettings(ctx context.Context, tenantID string) (TenantSettings, error)

func (NoopValidator) IsValidTenant

func (NoopValidator) IsValidTenant(ctx context.Context, tenantID string) (bool, error)

type RateLimitSettings

type RateLimitSettings struct {
	RequestsPerMinute int
	ConcurrentQuotes  int
	MaxCartSize       int
}

RateLimitSettings holds tenant-specific rate limits

type TenantSettings

type TenantSettings struct {
	ID              string
	Name            string
	StripeAccountID string // Connected Stripe account
	SolanaWallet    string // Tenant's payment receiving wallet
	Active          bool
	RateLimits      RateLimitSettings
	Features        FeatureFlags
}

TenantSettings holds tenant-specific configuration

type Validator

type Validator interface {
	// IsValidTenant checks if tenant exists and is active
	IsValidTenant(ctx context.Context, tenantID string) (bool, error)

	// GetTenantSettings retrieves tenant-specific settings
	GetTenantSettings(ctx context.Context, tenantID string) (TenantSettings, error)
}

Validator checks if a tenant ID is valid and active This is a placeholder interface for future tenant management

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL