Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The multitenancy middleware extracts and provides tenant information for multi-tenant SaaS applications. It supports various resolution strategies including subdomain, header, path, and query parameters.
Installation
import "github.com/go-mizu/mizu/middlewares/multitenancy"
Quick Start
app := mizu.New()
// Resolve tenant from subdomain
app.Use(multitenancy.New(multitenancy.SubdomainResolver()))
app.Get("/", func(c *mizu.Ctx) error {
tenant := multitenancy.Get(c)
return c.JSON(200, map[string]string{
"tenant_id": tenant.ID,
})
})
Configuration
| Option | Type | Default | Description |
|---|
Resolver | Resolver | Subdomain | Tenant resolution function |
ErrorHandler | func(*mizu.Ctx, error) error | - | Error handler |
Required | bool | true | Require tenant resolution |
Tenant Structure
type Tenant struct {
ID string
Name string
Domain string
Metadata map[string]any
}
Resolution Strategies
Subdomain Resolution
// tenant1.example.com → tenant_id: "tenant1"
app.Use(multitenancy.New(multitenancy.SubdomainResolver()))
// X-Tenant-ID: tenant1 → tenant_id: "tenant1"
app.Use(multitenancy.New(multitenancy.HeaderResolver("X-Tenant-ID")))
Path Resolution
// /tenant1/users → tenant_id: "tenant1", path: /users
app.Use(multitenancy.New(multitenancy.PathResolver()))
Query Parameter Resolution
// /users?tenant=tenant1 → tenant_id: "tenant1"
app.Use(multitenancy.New(multitenancy.QueryResolver("tenant")))
Examples
Basic Subdomain Tenant
app.Use(multitenancy.New(multitenancy.SubdomainResolver()))
app.Get("/dashboard", func(c *mizu.Ctx) error {
tenant := multitenancy.Get(c)
data := loadDashboard(tenant.ID)
return c.JSON(200, data)
})
Database Lookup
resolver := multitenancy.LookupResolver(
multitenancy.SubdomainResolver(),
func(id string) (*multitenancy.Tenant, error) {
// Look up full tenant info from database
var tenant Tenant
err := db.QueryRow(
"SELECT id, name, plan FROM tenants WHERE slug = ?",
id,
).Scan(&tenant.ID, &tenant.Name, &tenant.Plan)
if err != nil {
return nil, multitenancy.ErrTenantNotFound
}
return &multitenancy.Tenant{
ID: tenant.ID,
Name: tenant.Name,
Metadata: map[string]any{
"plan": tenant.Plan,
},
}, nil
},
)
app.Use(multitenancy.New(resolver))
Chain Multiple Resolvers
// Try subdomain first, then header, then query param
resolver := multitenancy.ChainResolver(
multitenancy.SubdomainResolver(),
multitenancy.HeaderResolver("X-Tenant-ID"),
multitenancy.QueryResolver("tenant"),
)
app.Use(multitenancy.New(resolver))
Custom Error Handler
app.Use(multitenancy.WithOptions(multitenancy.Options{
Resolver: multitenancy.SubdomainResolver(),
ErrorHandler: func(c *mizu.Ctx, err error) error {
return c.JSON(400, map[string]string{
"error": "Invalid tenant",
"message": err.Error(),
})
},
}))
Optional Tenant
app.Use(multitenancy.WithOptions(multitenancy.Options{
Resolver: multitenancy.SubdomainResolver(),
Required: false, // Allow requests without tenant
}))
app.Get("/", func(c *mizu.Ctx) error {
tenant := multitenancy.Get(c)
if tenant == nil {
return c.JSON(200, "Welcome to the platform")
}
return c.JSON(200, "Welcome, "+tenant.Name)
})
MustGet (Panic on Missing)
app.Get("/settings", func(c *mizu.Ctx) error {
// Panics if tenant not found - use in required tenant routes
tenant := multitenancy.MustGet(c)
settings := loadSettings(tenant.ID)
return c.JSON(200, settings)
})
Custom Resolver
func JWTTenantResolver() multitenancy.Resolver {
return func(c *mizu.Ctx) (*multitenancy.Tenant, error) {
claims := c.Get("jwt_claims").(jwt.MapClaims)
tenantID, ok := claims["tenant_id"].(string)
if !ok || tenantID == "" {
return nil, multitenancy.ErrTenantNotFound
}
return &multitenancy.Tenant{
ID: tenantID,
Name: tenantID,
}, nil
}
}
app.Use(jwtauth.New(jwtSecret))
app.Use(multitenancy.New(JWTTenantResolver()))
Tenant-Scoped Database
app.Get("/users", func(c *mizu.Ctx) error {
tenant := multitenancy.Get(c)
// Use tenant ID for database scoping
users, err := db.Query(
"SELECT * FROM users WHERE tenant_id = ?",
tenant.ID,
)
if err != nil {
return err
}
return c.JSON(200, users)
})
Tenant Middleware Chain
// Create tenant-aware middleware
func TenantRateLimit() mizu.Middleware {
return func(next mizu.Handler) mizu.Handler {
return func(c *mizu.Ctx) error {
tenant := multitenancy.Get(c)
limiter := rateLimiters[tenant.ID]
if !limiter.Allow() {
return c.Text(429, "Rate limit exceeded")
}
return next(c)
}
}
}
app.Use(multitenancy.New(multitenancy.SubdomainResolver()))
app.Use(TenantRateLimit())
resolver := multitenancy.LookupResolver(
multitenancy.SubdomainResolver(),
func(id string) (*multitenancy.Tenant, error) {
tenant := loadTenantFromDB(id)
return &multitenancy.Tenant{
ID: tenant.ID,
Name: tenant.Name,
Domain: tenant.Domain,
Metadata: map[string]any{
"plan": tenant.Plan,
"max_users": tenant.MaxUsers,
"features": tenant.Features,
"custom_domain": tenant.CustomDomain,
},
}, nil
},
)
// Access metadata in handlers
app.Get("/plan", func(c *mizu.Ctx) error {
tenant := multitenancy.Get(c)
plan := tenant.Metadata["plan"].(string)
return c.JSON(200, map[string]string{"plan": plan})
})
Built-in Resolvers
| Resolver | Source | Example |
|---|
SubdomainResolver() | Subdomain | tenant1.example.com |
HeaderResolver(h) | HTTP Header | X-Tenant-ID: tenant1 |
PathResolver() | URL Path | /tenant1/api/users |
QueryResolver(p) | Query Param | ?tenant=tenant1 |
ChainResolver(...) | Multiple | Tries each in order |
LookupResolver(r, fn) | Database | Enriches with lookup |
API Reference
func New(resolver Resolver) mizu.Middleware
func WithOptions(opts Options) mizu.Middleware
func Get(c *mizu.Ctx) *Tenant
func FromContext(c *mizu.Ctx) *Tenant // Alias
func MustGet(c *mizu.Ctx) *Tenant
// Resolvers
func SubdomainResolver() Resolver
func HeaderResolver(header string) Resolver
func PathResolver() Resolver
func QueryResolver(param string) Resolver
func LookupResolver(resolver Resolver, lookup func(id string) (*Tenant, error)) Resolver
func ChainResolver(resolvers ...Resolver) Resolver
// Errors
var ErrTenantNotFound = errors.New("tenant not found")
var ErrTenantInvalid = errors.New("tenant invalid")
Technical Details
Architecture
The multitenancy middleware uses Go’s context package to store and retrieve tenant information throughout the request lifecycle. It implements a resolver pattern that allows flexible tenant identification strategies.
Context Storage
The middleware stores tenant information in the request context using a private contextKey{} type. This ensures type safety and prevents key collisions with other middleware or application code.
type contextKey struct{}
// Stored in context
ctx := context.WithValue(c.Context(), contextKey{}, tenant)
Resolver Chain
The resolver pattern allows composing multiple tenant identification strategies:
- Each resolver implements the
Resolver function type
- Resolvers return
(*Tenant, error) allowing error propagation
ChainResolver tries resolvers sequentially until one succeeds
LookupResolver wraps a resolver with a database lookup function
Path Rewriting
The PathResolver automatically rewrites the request path to remove the tenant prefix:
// Original: /tenant1/api/users
// Rewritten: /api/users
// Tenant ID: "tenant1"
This allows routes to be defined without tenant prefixes while maintaining tenant isolation.
Error Handling
The middleware supports both required and optional tenant resolution:
- Required mode (default): Returns error via
ErrorHandler when tenant not found
- Optional mode: Continues request processing with
nil tenant
- Custom error handler: Allows application-specific error responses
- Context lookups are O(1) operations
- Subdomain parsing uses efficient string operations
- Header lookups use Go’s optimized HTTP header map
- Consider caching with
LookupResolver for database-backed tenants
Best Practices
- Use subdomain resolution for user-friendly URLs
- Implement database lookup for rich tenant data
- Use chain resolver for flexibility
- Always validate tenant access to resources
- Include tenant ID in all database queries
- Cache tenant lookups for performance
Testing
The middleware includes comprehensive test coverage for all resolvers and scenarios:
| Test Case | Description | Expected Behavior |
|---|
TestNew | Basic middleware creation with custom resolver | Tenant stored in context and accessible via Get() |
TestWithOptions_Required | Required tenant resolution fails | Returns 400 Bad Request when tenant not found |
TestWithOptions_NotRequired | Optional tenant resolution | Continues without tenant, Get() returns nil |
TestSubdomainResolver | Extract tenant from subdomain | Resolves “acme” from “acme.example.com” |
TestHeaderResolver | Extract tenant from HTTP header | Resolves tenant from “X-Tenant-Id” header |
TestPathResolver | Extract tenant from URL path prefix | Resolves “acme” from “/acme/api/users” and rewrites path to “/api/users” |
TestQueryResolver | Extract tenant from query parameter | Resolves tenant from “?tenant=my-tenant” |
TestLookupResolver | Database lookup enrichment | Enhances basic tenant with full data from lookup function |
TestChainResolver | Multiple resolver fallback | Tries header, then query, then subdomain in order |
TestFromContext | Alias function for Get | FromContext() returns same tenant as Get() |
TestMustGet | Get tenant or panic | Returns tenant when present |
TestMustGet_Panic | Panic on missing tenant | Panics with “tenant not found” message when tenant absent |
TestErrors | Error constants | Validates error messages for ErrTenantNotFound and ErrTenantInvalid |
TestWithOptions_ErrorHandler | Custom error handling | Uses custom error handler to return 401 Unauthorized |