> ## 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.

# Multi-tenancy

> Multi-tenant middleware for SaaS applications.

## 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

```go theme={null}
import "github.com/go-mizu/mizu/middlewares/multitenancy"
```

## Quick Start

```go theme={null}
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

```go theme={null}
type Tenant struct {
    ID       string
    Name     string
    Domain   string
    Metadata map[string]any
}
```

## Resolution Strategies

### Subdomain Resolution

```go theme={null}
// tenant1.example.com → tenant_id: "tenant1"
app.Use(multitenancy.New(multitenancy.SubdomainResolver()))
```

### Header Resolution

```go theme={null}
// X-Tenant-ID: tenant1 → tenant_id: "tenant1"
app.Use(multitenancy.New(multitenancy.HeaderResolver("X-Tenant-ID")))
```

### Path Resolution

```go theme={null}
// /tenant1/users → tenant_id: "tenant1", path: /users
app.Use(multitenancy.New(multitenancy.PathResolver()))
```

### Query Parameter Resolution

```go theme={null}
// /users?tenant=tenant1 → tenant_id: "tenant1"
app.Use(multitenancy.New(multitenancy.QueryResolver("tenant")))
```

## Examples

### Basic Subdomain Tenant

```go theme={null}
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

```go theme={null}
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

```go theme={null}
// 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

```go theme={null}
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

```go theme={null}
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)

```go theme={null}
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

```go theme={null}
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

```go theme={null}
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

```go theme={null}
// 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())
```

### Tenant Metadata

```go theme={null}
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

```go theme={null}
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.

```go theme={null}
type contextKey struct{}

// Stored in context
ctx := context.WithValue(c.Context(), contextKey{}, tenant)
```

### Resolver Chain

The resolver pattern allows composing multiple tenant identification strategies:

1. Each resolver implements the `Resolver` function type
2. Resolvers return `(*Tenant, error)` allowing error propagation
3. `ChainResolver` tries resolvers sequentially until one succeeds
4. `LookupResolver` wraps a resolver with a database lookup function

### Path Rewriting

The `PathResolver` automatically rewrites the request path to remove the tenant prefix:

```go theme={null}
// 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

### Performance Considerations

* 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                     |

## Related Middlewares

* [feature](/middlewares/feature) - Feature flags
* [ratelimit](/middlewares/ratelimit) - Rate limiting
