Multi-Tenancy
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
Related Topics
- Authentication & Authorization - Auth implementation
- Extending Built-in Modules — Organization-aware extensions
Need help? Ask in GitHub Discussions.