Skip to content

Multi-Tenancy Architecture

Authrim’s multi-tenancy architecture allows you to manage multiple isolated customer environments (tenants) within a single deployment. This page explains the relationship between environments, tenants, and clients, along with the domain naming patterns.

Architecture Overview

Authrim’s multi-tenancy model is based on three key concepts:

flowchart TB
    subgraph env["Environment (Separate Instance)"]
        subgraph tenant1["Tenant: acme"]
            client1a["Client: web-app"]
            client1b["Client: mobile-app"]
            client1c["Client: api-service"]
        end
        subgraph tenant2["Tenant: widget-co"]
            client2a["Client: dashboard"]
            client2b["Client: api"]
        end
        subgraph tenant3["Tenant: default"]
            client3a["Client: admin"]
        end
    end

    style env fill:#f0f8ff
    style tenant1 fill:#fff8dc
    style tenant2 fill:#fff8dc
    style tenant3 fill:#fff8dc

1. Environment (Infrastructure Level)

Each environment runs as a completely separate Workers instance.

  • Purpose: Lifecycle stage separation (development, testing, staging, production)
  • Isolation: Separate codebase deployment, separate databases, separate KV stores
  • Domain: Each environment has its own BASE_DOMAIN
  • Independence: Environments never share data or configuration

Example:

Production: BASE_DOMAIN = "authrim.com"
Staging: BASE_DOMAIN = "staging.authrim.com"
Test: BASE_DOMAIN = "test.authrim.com"
Development: BASE_DOMAIN = "dev.authrim.com"

2. Tenant (Isolation Boundary)

A tenant is an isolated customer workspace within an environment.

  • Purpose: Customer/organization data isolation
  • Scope: All data (users, sessions, tokens, policies) is scoped to a tenant
  • Domain: Identified by subdomain (e.g., acme.authrim.com)
  • Isolation: Complete logical separation — tenants cannot see or access each other’s data

Key Characteristics:

  • Multi-tenant mode is always enabled when BASE_DOMAIN is set
  • Each tenant has isolated:
    • User directory
    • Client applications
    • Sessions and tokens
    • Audit logs
    • Authorization policies
  • No cross-tenant data sharing (unless explicitly federated)

3. Client (Application)

A client is an application registered within a tenant.

  • Purpose: Represents an application that authenticates users
  • Scope: Belongs to exactly one tenant
  • Types: Web apps, mobile apps, SPAs, backend services, M2M applications
  • Protocols: OAuth 2.0, OIDC, SAML 2.0

Example:

  • Tenant: acme
    • Client: web-dashboard (OIDC, authorization_code + PKCE)
    • Client: mobile-app (OIDC, authorization_code + PKCE)
    • Client: api-service (OAuth 2.0, client_credentials)
    • Client: legacy-app (SAML 2.0 SP)

Domain Patterns

Valid Patterns

Authrim uses subdomain-based tenant isolation. Only single-level subdomains are allowed.

Production Environment (BASE_DOMAIN = "authrim.com")

DomainTenant IDDescription
authrim.comPRIMARY_TENANT_ID or defaultNaked domain (primary tenant)
acme.authrim.comacmeSimple tenant name
acme-corp.authrim.comacme-corpHyphenated tenant name
acme-prod.authrim.comacme-prodTenant with suffix
tenant123.authrim.comtenant123Alphanumeric tenant
a.authrim.comaSingle-character tenant

Staging Environment (BASE_DOMAIN = "staging.authrim.com")

DomainTenant IDDescription
staging.authrim.comdefaultNaked domain
acme.staging.authrim.comacmeTenant in staging
widget-co.staging.authrim.comwidget-coAnother tenant

Test Environment (BASE_DOMAIN = "test.authrim.com")

DomainTenant IDDescription
test.authrim.comdefaultNaked domain
acme.test.authrim.comacmeTenant in test environment

Invalid Patterns (Rejected)

❌ Sub-subdomains are NOT allowed

Multi-level subdomains are rejected to prevent ambiguity and maintain security.

DomainErrorReason
dev.acme.authrim.cominvalid_format (400)Sub-subdomain not allowed
api.acme.authrim.cominvalid_format (400)Sub-subdomain not allowed
auth.tenant.staging.authrim.cominvalid_format (400)Sub-subdomain not allowed
acme.other.comtenant_not_found (404)BASE_DOMAIN mismatch
-acme.authrim.cominvalid_format (400)Hyphen at start
acme-.authrim.cominvalid_format (400)Hyphen at end
tenant_name.authrim.cominvalid_format (400)Underscore not allowed

Why sub-subdomains are prohibited:

  1. Ambiguity: dev.acme.authrim.com — is dev a service prefix or part of the tenant name?
  2. Simplicity: Single-level subdomains are easier to understand and manage
  3. Security: Prevents potential DNS-based attacks and misconfiguration
  4. Architecture: Environments are separate instances, not subdomains

Solution: Use hyphens in tenant names instead:

  • dev.acme.authrim.com → ✅ acme-dev.authrim.com
  • api.tenant.authrim.com → ✅ tenant-api.authrim.com

Environment Configuration

Environment Variables

Each environment instance requires these variables:

Terminal window
# Base domain for multi-tenant mode
BASE_DOMAIN="authrim.com"
# Default tenant ID (used when no tenant specified)
DEFAULT_TENANT_ID="default"
# Primary tenant for naked domain access (optional)
PRIMARY_TENANT_ID="main"
# User ID format (uuid or nanoid)
USER_ID_FORMAT="nanoid"

Naked Domain Routing

When accessing the naked domain (e.g., authrim.com), Authrim uses this priority:

  1. PRIMARY_TENANT_ID if set → routes to that tenant
  2. DEFAULT_TENANT_ID if set → routes to that tenant
  3. Fallback to "default" tenant

Example:

Terminal window
BASE_DOMAIN="authrim.com"
PRIMARY_TENANT_ID="main"
DEFAULT_TENANT_ID="default"
  • authrim.com → routes to tenant "main"
  • acme.authrim.com → routes to tenant "acme"
  • widget.authrim.com → routes to tenant "widget"

Relationship Examples

Example 1: SaaS Platform

Scenario: A SaaS company with multiple customers

Production Environment (BASE_DOMAIN = "myplatform.com"):
├── Tenant: acme
│ ├── Client: web-dashboard (OIDC)
│ ├── Client: mobile-app (OIDC)
│ └── Client: reporting-api (OAuth M2M)
├── Tenant: widget-co
│ ├── Client: admin-panel (OIDC)
│ └── Client: public-api (OAuth M2M)
└── Tenant: default
└── Client: platform-admin (OIDC)
Staging Environment (BASE_DOMAIN = "staging.myplatform.com"):
├── Tenant: acme
│ ├── Client: web-dashboard (testing)
│ └── Client: mobile-app (testing)
└── Tenant: test-customer
└── Client: demo-app (testing)

Domains:

  • Production: acme.myplatform.com, widget-co.myplatform.com
  • Staging: acme.staging.myplatform.com, test-customer.staging.myplatform.com

Example 2: Single Tenant (Company Internal)

Scenario: A company using Authrim for internal authentication

Production Environment (BASE_DOMAIN = "auth.company.com"):
└── Tenant: company (via PRIMARY_TENANT_ID)
├── Client: employee-portal (OIDC)
├── Client: mobile-app (OIDC)
├── Client: legacy-intranet (SAML)
└── Client: api-gateway (OAuth M2M)
Test Environment (BASE_DOMAIN = "auth-test.company.com"):
└── Tenant: company
└── Client: employee-portal (testing)

Configuration:

Terminal window
# Production
BASE_DOMAIN="auth.company.com"
PRIMARY_TENANT_ID="company"
# Users access via naked domain: auth.company.com → tenant "company"

Example 3: Multi-Environment Development

Scenario: A development team with multiple environments

Development (BASE_DOMAIN = "dev.authrim.com"):
├── Tenant: alice-dev
│ └── Client: test-app
└── Tenant: bob-dev
└── Client: test-app
Test/CI (BASE_DOMAIN = "test.authrim.com"):
├── Tenant: ci-runner-1
│ └── Client: automated-tests
└── Tenant: ci-runner-2
└── Client: automated-tests
Staging (BASE_DOMAIN = "staging.authrim.com"):
└── Tenant: demo
├── Client: web-app
└── Client: mobile-app
Production (BASE_DOMAIN = "authrim.com"):
├── Tenant: customer-a
│ ├── Client: web-app
│ └── Client: mobile-app
└── Tenant: customer-b
├── Client: dashboard
└── Client: api

Best Practices

1. Environment Separation

Do:

  • Use separate BASE_DOMAIN for each environment
  • Deploy each environment as a separate Workers instance
  • Use environment-specific databases and KV stores

Don’t:

  • Mix test and production data in the same environment
  • Use sub-subdomains to represent environments (e.g., test.acme.authrim.com)

2. Tenant Naming

Do:

  • Use lowercase alphanumeric characters and hyphens
  • Keep tenant names short and descriptive
  • Use hyphens for multi-word names: acme-corp, widget-co

Don’t:

  • Start or end with hyphens: -acme, acme-
  • Use special characters: acme_corp, acme@company
  • Create sub-subdomains: dev.acme.authrim.com

3. Client Management

Do:

  • Register each application as a separate client
  • Use appropriate client types (confidential for backend, public for SPA/mobile)
  • Enable PKCE for all public clients
  • Use client_credentials grant for M2M services

Don’t:

  • Share client credentials across tenants
  • Use the same client_id in multiple environments (use separate clients)
  • Disable PKCE for public clients

Security Considerations

Tenant Isolation

  • Database-level isolation: All queries are automatically scoped to tenant ID
  • Session isolation: Sessions cannot be shared across tenants
  • Token isolation: Access tokens are bound to the issuing tenant
  • Audit isolation: Audit logs are separated by tenant

Domain Validation

  • Host header validation: Authrim validates the Host header on every request
  • BASE_DOMAIN enforcement: Only requests matching *.BASE_DOMAIN are accepted
  • Sub-subdomain rejection: Multi-level subdomains are rejected with 400 Bad Request
  • CORS enforcement: CORS policies are tenant-scoped

Cross-Tenant Access

By default, cross-tenant access is prohibited. If you need federated access:

  1. Use external IdP bridge: Configure social login or enterprise SSO
  2. SAML federation: Set up SAML trust between tenants
  3. Token exchange: Implement OAuth 2.0 token exchange (RFC 8693)

Migration Scenarios

From Single-Tenant to Multi-Tenant

If you started with a single-tenant deployment and want to migrate:

  1. Set BASE_DOMAIN: Configure your base domain (e.g., authrim.com)
  2. Set PRIMARY_TENANT_ID: Point naked domain to existing tenant
  3. Migrate users: All existing users belong to the primary tenant
  4. Add new tenants: Create new tenants via Admin API
  5. Update DNS: Configure wildcard DNS for *.authrim.com

Example:

Terminal window
# Before (single-tenant)
ISSUER_URL="https://auth.company.com"
# After (multi-tenant)
BASE_DOMAIN="auth.company.com"
PRIMARY_TENANT_ID="company"
# Naked domain (auth.company.com) still works → routes to "company" tenant

Adding New Tenants

To add a new tenant:

  1. Create tenant via Admin API:
Terminal window
POST /admin/tenants
{
"tenantId": "new-customer",
"displayName": "New Customer Inc."
}
  1. Configure DNS: Ensure new-customer.authrim.com resolves to your Workers
  2. Register clients: Create OAuth/OIDC clients for the new tenant
  3. Test access: Verify login at new-customer.authrim.com

Troubleshooting

Common Issues

Issue: tenant_not_found error (404)

Causes:

  • Domain doesn’t match BASE_DOMAIN
  • Using sub-subdomain (e.g., dev.acme.authrim.com)
  • Tenant doesn’t exist in the database

Solution:

  • Verify BASE_DOMAIN configuration
  • Use single-level subdomain: acme.authrim.com
  • Create tenant via Admin API

Issue: invalid_format error (400)

Causes:

  • Sub-subdomain used (e.g., api.tenant.authrim.com)
  • Invalid characters in subdomain
  • Hyphen at start or end

Solution:

  • Use hyphens instead: tenant-api.authrim.com
  • Use only alphanumeric and hyphens: [a-z0-9-]+
  • Ensure no leading/trailing hyphens

Issue: missing_host error (400)

Cause: Host header is missing from the request

Solution:

  • Ensure HTTP client sends Host header
  • Check reverse proxy configuration
  • Verify Cloudflare settings

Further Reading