Skip to main content

Multi-Tenancy

DEVELOPER Advanced

Implement multi-tenant applications with organization-scoped resources.

Overview

EZ-Console supports multi-tenancy through organization-based resource isolation. Resources belong to organizations, and users can be members of multiple organizations with different roles in each.

Multi-Tenancy Model

Organization Structure

Organization
├── Users (organization members)
├── Roles (organization-specific roles)
├── Resources (organization-scoped data)
└── Settings (organization-specific settings)

Resource Scoping

Resources are scoped to organizations:

  • Users can belong to multiple organizations
  • Resources belong to a single organization
  • Permissions are organization-scoped

Organization Context

Getting Organization ID

In controllers, get organization ID from header:

func (c *ProductController) ListProducts(ctx *gin.Context) {
// Get organization ID from header
orgID := ctx.GetHeader("X-Scope-OrgID")

if orgID == "" {
// Get from user's default organization
userID, _ := ctx.Get("user_id")
user, _ := c.svc.GetUser(ctx.Request.Context(), userID.(string))
orgID = user.DefaultOrganizationID
}

// Query organization-scoped resources
products, err := c.service.GetProductsByOrganization(ctx, orgID)
// ...
}

Setting Organization Context

Frontend automatically includes organization ID in requests:

// Set organization ID
localStorage.setItem('orgID', organizationId);

// API client automatically includes header
const orgID = localStorage.getItem('orgID');
if (orgID) {
config.headers['X-Scope-OrgID'] = orgID;
}

Organization-Scoped Models

Model Definition

type Product struct {
ID string `gorm:"primaryKey"`
OrganizationID string `gorm:"type:varchar(36);index"` // Organization scope
Name string
Price float64
CreatedAt time.Time
UpdatedAt time.Time
}

func (p *Product) TableName() string {
return "t_product"
}

Querying by Organization

func (s *ProductService) GetProductsByOrganization(ctx context.Context, orgID string) ([]Product, error) {
var products []Product
err := s.db.Where("organization_id = ?", orgID).Find(&products).Error
return products, err
}

Organization Management

Creating Organizations

func (s *OrganizationService) CreateOrganization(ctx context.Context, name string) (*Organization, error) {
org := &Organization{
ID: uuid.New().String(),
Name: name,
}

err := s.db.Create(org).Error
return org, err
}

Assigning Users to Organizations

func (s *OrganizationService) AddUserToOrganization(ctx context.Context, orgID, userID string, role string) error {
membership := &OrganizationMembership{
OrganizationID: orgID,
UserID: userID,
Role: role,
}

return s.db.Create(membership).Error
}

Permission Isolation

Organization-Scoped Permissions

Permissions are evaluated within organization context:

func (s *PermissionService) CheckPermission(ctx context.Context, userID, orgID, permission string) (bool, error) {
// Get user's roles in organization
roles, err := s.GetUserRolesInOrganization(ctx, userID, orgID)
if err != nil {
return false, err
}

// Check if any role has permission
for _, role := range roles {
hasPermission, err := s.RoleHasPermission(ctx, role.ID, permission)
if err != nil {
return false, err
}
if hasPermission {
return true, nil
}
}

return false, nil
}

Organization Switching

Frontend Organization Switcher

import { OrganizationSwitcher } from 'ez-console';

// Use built-in organization switcher
<OrganizationSwitcher
organizations={userOrganizations}
currentOrgID={currentOrgID}
onSwitch={(orgID) => {
localStorage.setItem('orgID', orgID);
window.location.reload();
}}
/>

Programmatic Switching

const switchOrganization = (orgID: string) => {
localStorage.setItem('orgID', orgID);
// Reload or update context
window.location.reload();
};

Best Practices

1. Always Scope Queries

// ✅ Good: Scoped query
db.Where("organization_id = ?", orgID).Find(&products)

// ❌ Bad: Global query (security risk)
db.Find(&products)

2. Validate Organization Access

func (s *Service) validateOrgAccess(ctx context.Context, userID, orgID string) error {
// Check if user belongs to organization
exists, err := s.UserInOrganization(ctx, userID, orgID)
if err != nil || !exists {
return fmt.Errorf("user not in organization")
}
return nil
}

3. Default Organization

Assign default organization to users:

user.DefaultOrganizationID = primaryOrgID

Need help? Ask in GitHub Discussions.