PRE-RELEASE INFORMATION: SUBJECT TO CHANGE

Tenants, Domains, and Policies: The Core Hierarchy

Understanding how tenants, domains, and policies relate to each other is the key to mastering Housecarl. Once you grasp this three-level hierarchy, everything else falls into place.

The Three Levels

Think of Housecarl's authorization model like a filing system:

┌─────────────────────────────────────────────────────────────┐
│ TENANT: Acme Corporation                                    │  ← Organization
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DOMAIN: Engineering Team                                │ │  ← Policy Container
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ POLICY: Engineers can read project documentation    │ │ │  ← Access Rule
│ │ │ POLICY: Lead engineers can approve deployments     │ │ │
│ │ │ POLICY: Deny access to archived projects           │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DOMAIN: Product Team                                    │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ POLICY: Product managers can read all roadmaps     │ │ │
│ │ │ POLICY: Product team can edit their own specs      │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Let's break down each level.

Level 1: Tenant (The Organization)

A tenant is your organization's isolated workspace in Housecarl. Think of it as your own private authorization server.

What is a Tenant?

If you're building a SaaS application, each of your customers would be a tenant:

  • Acme Corporation is a tenant
  • Beta Industries is a tenant
  • Your personal project is a tenant

Each tenant is completely isolated from the others. Acme's policies can't affect Beta's resources, and vice versa.

Real-World Example

Let's say you're building a project management SaaS. You have two customers:

Acme Corporation (Tenant ID: 550e8400-e29b-41d4-a716-446655440000)

  • Has 50 users
  • Manages 20 projects
  • Has custom authorization rules for their compliance team

Beta Industries (Tenant ID: 660e8400-e29b-41d4-a716-446655440001)

  • Has 10 users
  • Manages 5 projects
  • Has simpler authorization needs

These two tenants share the same Housecarl instance, but their data and policies are completely separate. A user from Acme can never access Beta's resources through Housecarl, even if they somehow knew the resource URIs.

How Tenant Isolation Works

When an authorization request comes in, Housecarl extracts the tenant from the resource URI:

Authorization Request:
  User: alice@acme.com
  Action: read
  Resource: hc://domain/550e8400-e29b-41d4-a716-446655440000/projects/website-redesign
                          The domain UUID tells Housecarl which policies to use

Even if Alice works for both Acme and Beta, when she's accessing Acme resources, only Acme's policies are evaluated. This is secure by architecture - you don't need to write policies to enforce tenant isolation.

What Gets Created with a Tenant

When you sign up for Housecarl (or create a tenant via housectl admin create), you automatically get:

  1. The tenant itself - Your organizational container
  2. A root domain - Your first policy container (more on this below)
  3. Starter policies - Basic policies that grant the creator full access

This means you can start using Housecarl immediately without manual setup.

Level 2: Domain (The Policy Container)

A domain is a logical grouping of policies. Think of it as a folder that contains related access rules.

Why Domains Exist

You could put all your policies in one big pile, but that becomes unmanageable quickly. Domains let you organize policies by:

  • Team: engineering domain, product domain, finance domain
  • Product: web-app domain, mobile-app domain, api domain
  • Environment: production domain, staging domain, development domain
  • Function: admin-access domain, read-only domain, contractor-access domain

Domain Hierarchy: Inheritance

Here's where domains get powerful: they can inherit from other domains. This is called superior domain relationships.

                    ┌─────────────┐
                    │   global    │  ← Base policies everyone inherits
                    │  (domain)   │
                    └──────┬──────┘
                           │ inherits
              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
        ┌──────────┐  ┌──────────┐  ┌──────────┐
        │engineering│ │ product  │  │  finance │
        │ (domain) │  │ (domain) │  │ (domain) │
        └────┬─────┘  └────┬─────┘  └──────────┘
             │             │
             ▼             ▼
       ┌──────────┐  ┌──────────┐
       │ frontend │  │ roadmaps │
       │ (domain) │  │ (domain) │
       └──────────┘  └──────────┘

When evaluating authorization for a resource in the frontend domain, Housecarl collects policies from:

  1. frontend domain (the target domain)
  2. engineering domain (frontend's superior)
  3. global domain (engineering's superior)

All three sets of policies are evaluated together.

Real-World Example: Team Organization

Let's say Acme Corporation organizes their domains by team:

global domain - Policies that apply to everyone:

  • Policy: "All users can read public documentation"
  • Policy: "All users can update their own profile"

engineering domain (inherits from global) - Engineering team policies:

  • Policy: "Engineering team can read all code repositories"
  • Policy: "Engineering team can deploy to staging"

engineering-platform domain (inherits from engineering) - Platform subteam:

  • Policy: "Platform team can deploy to production"
  • Policy: "Platform team can manage infrastructure"

When a platform engineer tries to access something, Housecarl evaluates ALL these policies:

  • Global policies (via engineering → global)
  • Engineering policies (via engineering-platform → engineering)
  • Platform-specific policies (direct)

This means platform engineers automatically get:

  • Ability to read public docs (from global)
  • Ability to update their profile (from global)
  • Access to code repos (from engineering)
  • Ability to deploy to staging (from engineering)
  • Ability to deploy to production (from platform)
  • Ability to manage infrastructure (from platform)

Creating Domains

Using housectl:

# Create a basic domain in your current tenant
housectl domain create engineering

# Create a domain in a specific tenant
housectl domain create product-team acme-corp

# Create a domain with policies from a directory
housectl domain create api-access acme-corp ./api-policies/

Domains are tenant-scoped, so the same domain name can exist in multiple tenants without conflict.

Level 3: Policy (The Access Rule)

A policy is the actual rule that determines if access is allowed or denied.

Anatomy of a Policy

Here's a real policy from Acme's engineering domain:

name = "engineering-read-code"
description = "Engineering team members can read all code repositories"
engine = "RegEx"
invert = false
deny = false

[[statements]]
team = "engineering"
action = "(read|clone|pull)"
object = "hc://domain/550e8400-e29b-41d4-a716-446655440000/repositories/.*"

Breaking it down:

  • name: Unique identifier within this domain
  • description: What this policy does (write these! Future you will thank present you)
  • engine: How pattern matching works (RegEx lets us use alternation and .*)
  • invert: false means match when rules match (true would invert the logic)
  • deny: false means this is an "allow" policy
  • statements: The actual matching rules

The statement says:

  • IF the user's team attribute is "engineering"
  • AND they're trying to "read", "clone", or "pull"
  • AND the resource matches "hc://domain/550e8400-e29b-41d4-a716-446655440000/repositories/*" (any repository)
  • THEN allow the request

How Policies Combine

When multiple policies exist in a domain hierarchy, Housecarl evaluates them using these rules:

  1. If ANY deny policy matches → Request is DENIED (deny always wins)
  2. If at least one allow policy matches → Request is ALLOWED
  3. If NO policy matches → Request is DENIED (secure by default)

Important: Non-matching policies are simply ignored. You don't need every allow policy to match - just one matching allow policy (with no matching deny policies) is sufficient to grant access.

Example: Let's say we have these policies in the engineering domain:

Policy A (allow): team=engineering can read repositories
Policy B (deny): contract_type=contractor cannot access proprietary repos

Scenario 1: Alice (engineer, employee) reads a proprietary repo

  • Policy A matches: team=engineering ✓
  • Policy B doesn't match: not a contractor ✓
  • Result: ALLOWED

Scenario 2: Bob (engineer, contractor) reads a proprietary repo

  • Policy A matches: team=engineering ✓
  • Policy B matches: contractor + proprietary repo ✓
  • Result: DENIED (deny wins)

Scenario 3: Charlie (finance team) reads a repository

  • Policy A doesn't match: not engineering ✗
  • Policy B doesn't match: not accessing proprietary repo ✗
  • Result: DENIED (no matching policy)

Putting It All Together: A Complete Example

Let's walk through how all three levels work together with a realistic scenario.

The Setup

Acme Corporation (tenant) has this structure:

Tenant: Acme Corp
├── Domain: global
│   ├── Policy: all-users-read-public
│   └── Policy: users-manage-own-profile
├── Domain: engineering (inherits from global)
│   ├── Policy: engineers-read-all-code
│   ├── Policy: engineers-deploy-staging
│   └── Policy: deny-contractors-proprietary
└── Domain: engineering-platform (inherits from engineering)
    ├── Policy: platform-deploy-production
    └── Policy: platform-manage-infrastructure

The Request

Alice is a platform engineer (employee, not contractor). She wants to deploy to production:

{
  "context": {
    "subject": "alice",
    "action": "deploy",
    "object": "hc://domain/550e8400-e29b-41d4-a716-446655440000/environments/production",
    "team": "platform",
    "parent_team": "engineering",
    "contract_type": "employee"
  }
}

Runnable example note: The UUID above is illustrative. Replace it with a real domain UUID before using it with housectl authz can-i, REST, or gRPC examples against a live server.

Note: housectl authz can-i expects the same wrapped context format.

The Evaluation

Step 1: Housecarl identifies this is an Acme Corp resource (from context or JWT)

Step 2: The resource is in the "production" environment, which belongs to the engineering-platform domain

Step 3: Collect all policies from domain hierarchy:

  • From engineering-platform: platform-deploy-production, platform-manage-infrastructure
  • From engineering (superior): engineers-read-all-code, engineers-deploy-staging, deny-contractors-proprietary
  • From global (superior of engineering): all-users-read-public, users-manage-own-profile

Step 4: Evaluate each policy:

  • ✗ all-users-read-public: Doesn't match (action is "deploy", not "read")
  • ✗ users-manage-own-profile: Doesn't match (object is environment, not profile)
  • ✗ engineers-read-all-code: Doesn't match (action is "deploy", not "read")
  • ✗ engineers-deploy-staging: Doesn't match (object is production, not staging)
  • ✗ deny-contractors-proprietary: Doesn't match (Alice is not a contractor)
  • ✓ platform-deploy-production: MATCHES! (team=platform, action=deploy, object=production)

Step 5: Combine results:

  • No deny policies matched
  • One allow policy matched
  • Result: ALLOWED

Alice can deploy to production!

Same Request, Different User

Now let's say Bob (contractor on the platform team) tries the same thing:

Step 4: Evaluate each policy:

  • ✗ all-users-read-public: Doesn't match
  • ✗ users-manage-own-profile: Doesn't match
  • ✗ engineers-read-all-code: Doesn't match
  • ✗ engineers-deploy-staging: Doesn't match
  • ✓ deny-contractors-proprietary: MATCHES! (contractor + proprietary resource)
  • ✓ platform-deploy-production: MATCHES! (team=platform)

Step 5: Combine results:

  • One deny policy matched (deny-contractors-proprietary)
  • One allow policy matched (platform-deploy-production)
  • Result: DENIED (deny wins)

Bob is blocked from deploying to production because the deny policy overrides the allow policy.

Managing the Hierarchy

Adding a New Domain

When you create a new domain, you can specify superior domains:

# Create domain that inherits from global
housectl domain create customer-success --superior-domains global

# Create domain that inherits from multiple superiors (if supported)
housectl domain create mobile-engineering --superior-domains engineering mobile

Note: Check current Housecarl version for multiple superior support. As of this writing, single superior is standard.

Reorganizing Domains

You can change domain hierarchies by updating the superior domain relationships:

# Update a domain's properties (including superior)
housectl domain add-superior frontend-team engineering

Warning: Changing domain hierarchies affects policy evaluation immediately. Test in a dev tenant first.

Best Practices for Domain Organization

1. Start with a global domain

  • Put policies that apply to everyone here
  • "All users can read public docs"
  • "Users can update their own profile"

2. Create domains by team or function

  • engineering, product, finance, operations
  • Each team manages their own domain

3. Use hierarchy for specialization

  • Global → Engineering → Platform
  • Global → Product → Mobile

4. Keep it shallow

  • Deep hierarchies (5+ levels) get confusing
  • Usually 2-3 levels is enough

5. Name domains clearly

  • Good: engineering-backend, product-mobile, finance-reporting
  • Bad: domain1, temp, old-policies

Common Questions

Q: Can a domain belong to multiple tenants?

No. Domains are tenant-scoped. Acme's "engineering" domain is completely separate from Beta's "engineering" domain.

Q: Can a policy be in multiple domains?

No. Each policy belongs to exactly one domain. However, domains can inherit policies from superior domains, so a policy in "global" is effectively available to all child domains.

Q: What happens if I delete a domain that other domains inherit from?

Deleting a domain is permanent and affects all child domains immediately. Child domains will no longer inherit policies from the deleted domain. Use with extreme caution.

Q: Can I have circular domain inheritance? (A → B → C → A)

No. Housecarl enforces a Directed Acyclic Graph (DAG) structure. Circular inheritance would make policy evaluation ambiguous and is prevented.

Q: How many policies can a domain have?

There's no hard limit, but for performance reasons, keep domains focused. If you have 100+ policies in one domain, consider breaking it into multiple domains with a shared superior.

Q: Do I need a separate tenant for dev/staging/prod?

It depends on your security requirements:

  • Separate tenants: Maximum isolation, different policies for each environment
  • Same tenant, different domains: Shared policies with environment-specific overrides

Most teams use separate tenants for production vs. non-production.

Q: What's the difference between a domain and a namespace?

A domain is purely an authorization concept - it groups policies. It doesn't create resource namespaces. Your resources (like hc://domain/<uuid>/documents/...) can reference any domain at evaluation time via the UUID.

Visualizing Your Hierarchy

To see your tenant's domain structure:

# List all domains in your current tenant
housectl domain list

# Get details on a specific domain (including superiors)
housectl domain get engineering

Output example:

Domain: engineering
ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Tenant: acme-corp
Active: true
Superior Domains:
  - global (550e8400-e29b-41d4-a716-446655440000)
Policies: 5

Summary: The Mental Model

Remember this hierarchy:

TENANT (Organization)
  └── DOMAIN (Policy Container)
        └── POLICY (Access Rule)
  • Tenants provide multi-tenancy and isolation
  • Domains organize policies and enable inheritance
  • Policies define the actual authorization rules

When an authorization request comes in:

  1. Identify the tenant (from resource or JWT)
  2. Identify the domain (from resource or request context)
  3. Collect all policies (from domain + all superiors)
  4. Evaluate policies against the request
  5. Combine results (deny wins; otherwise, one matching allow grants access)

Next Steps

Now that you understand the core hierarchy:

The complete DDD perspective and additional details are in the Domain Model Reference.

Ready to put this knowledge into practice? Head to the Quick Start to create your first policy!